Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492//! Script element loading and execution.//!//! Walks the DOM for `<script>` elements, fetches external scripts, and//! executes them in a shared JS VM with DOM access.
use crate::loader::{Resource, ResourceLoader};use we_dom::{Document, NodeId};use we_js::compiler;use we_js::parser::Parser;use we_js::vm::Vm;use we_url::Url;
/// Information about a `<script>` element extracted from the DOM.struct ScriptInfo { /// Inline script text (from child text nodes). text: Option<String>, /// `src` attribute value (external script URL). src: Option<String>, /// Whether the `defer` attribute is present. defer: bool, /// Whether the `async` attribute is present. async_attr: bool, /// The `type` attribute value, if any. type_attr: Option<String>,}
/// Returns true if a script's `type` attribute indicates it should execute.////// Per spec, scripts execute if there is no type attribute or if the type/// is a JavaScript MIME type. Unknown types are not executed.fn should_execute(type_attr: &Option<String>) -> bool { match type_attr { None => true, Some(t) => { let t = t.trim().to_ascii_lowercase(); t.is_empty() || t == "text/javascript" || t == "application/javascript" || t == "application/x-javascript" || t == "text/ecmascript" || t == "application/ecmascript" } }}
/// Collect text content from child text nodes of an element.fn collect_text_content(doc: &Document, node: NodeId) -> String { let mut text = String::new(); for child in doc.children(node) { if let Some(data) = doc.text_content(child) { text.push_str(data); } } text}
/// Walk the DOM tree in document order, collecting `<script>` elements.fn collect_script_nodes(doc: &Document, node: NodeId, result: &mut Vec<NodeId>) { if doc.tag_name(node) == Some("script") { result.push(node); } for child in doc.children(node) { collect_script_nodes(doc, child, result); }}
/// Extract script info from a `<script>` element node.fn extract_script_info(doc: &Document, node: NodeId) -> ScriptInfo { let src = doc.get_attribute(node, "src").map(|s| s.to_string()); let defer = doc.get_attribute(node, "defer").is_some(); let async_attr = doc.get_attribute(node, "async").is_some(); let type_attr = doc.get_attribute(node, "type").map(|s| s.to_string());
let text = if src.is_none() { let content = collect_text_content(doc, node); if content.is_empty() { None } else { Some(content) } } else { None };
ScriptInfo { text, src, defer, async_attr, type_attr, }}
/// Fetch the text of an external script.fn fetch_script_text(loader: &mut ResourceLoader, src: &str, base_url: &Url) -> Option<String> { match loader.fetch_url(src, Some(base_url)) { Ok(Resource::Script { text, .. }) => Some(text), Ok(Resource::Other { data, .. }) => { // Try decoding as UTF-8 (servers may not set correct MIME type). String::from_utf8(data).ok() } Ok(_) => { eprintln!("[script] unexpected resource type for {src}"); None } Err(e) => { eprintln!("[script] failed to fetch {src}: {e}"); None } }}
/// Compile and execute a script in the VM. Errors are logged, not propagated.fn execute_script(vm: &mut Vm, source: &str, label: &str) { let ast = match Parser::parse(source) { Ok(ast) => ast, Err(e) => { eprintln!("[script] parse error in {label}: {e}"); return; } };
let func = match compiler::compile(&ast) { Ok(f) => f, Err(e) => { eprintln!("[script] compile error in {label}: {e}"); return; } };
if let Err(e) = vm.execute(&func) { eprintln!("[script] runtime error in {label}: {e}"); }}
/// Execute all `<script>` elements on the page.////// Takes ownership of the DOM `Document`, creates a JS VM with DOM access,/// executes scripts in document order (respecting `defer` and `async`/// attributes), then returns the (possibly modified) document.////// Script execution order:/// 1. Synchronous scripts (no `defer`/`async`): execute in document order,/// blocking further script processing./// 2. Deferred scripts (`defer`): execute after all synchronous scripts,/// in document order./// 3. Async scripts (`async`): execute as soon as fetched; for synchronous/// fetching this is equivalent to document order.////// Scripts with an unrecognized `type` attribute are skipped./// Errors in one script do not prevent other scripts from running.pub fn execute_page_scripts( doc: Document, loader: &mut ResourceLoader, base_url: &Url,) -> Document { // Find all <script> elements in document order. let mut script_nodes = Vec::new(); let root = doc.root(); collect_script_nodes(&doc, root, &mut script_nodes);
// Extract script info and classify. let scripts: Vec<(NodeId, ScriptInfo)> = script_nodes .into_iter() .map(|node| (node, extract_script_info(&doc, node))) .filter(|(_, info)| should_execute(&info.type_attr)) .collect();
if scripts.is_empty() { return doc; }
// Create VM with DOM access. let mut vm = Vm::new(); vm.attach_document(doc);
// Separate into immediate (sync + async) and deferred scripts. let mut deferred_sources: Vec<(String, String)> = Vec::new();
for (_node, info) in &scripts { // Resolve the script source text. let (source, label) = if let Some(ref src) = info.src { // External script: fetch it. match fetch_script_text(loader, src, base_url) { Some(text) => (text, src.clone()), None => continue, } } else if let Some(ref text) = info.text { (text.clone(), "<inline>".to_string()) } else { continue; };
if info.defer && !info.async_attr && info.src.is_some() { // Deferred external scripts: queue for later execution. // Per spec, defer only applies to external scripts and is // ignored when async is also present. deferred_sources.push((source, label)); } else { // Synchronous and async scripts: execute immediately. // Async scripts have no ordering guarantee, but since we // fetch synchronously they execute in document order. execute_script(&mut vm, &source, &label); } }
// Execute deferred scripts in document order. for (source, label) in &deferred_sources { execute_script(&mut vm, source, label); }
// Pump the event loop to handle any pending microtasks/timers. let _ = vm.pump_event_loop();
// Take the document back from the VM. vm.detach_document().unwrap_or_default()}
#[cfg(test)]mod tests { use super::*; use we_html::parse_html;
/// Helper: parse HTML, execute scripts, return the document. fn run_scripts(html: &str) -> Document { let doc = parse_html(html); let mut loader = ResourceLoader::new(); let base_url = Url::parse("about:blank").unwrap(); execute_page_scripts(doc, &mut loader, &base_url) }
#[test] fn test_no_scripts() { let doc = run_scripts("<html><body><p>Hello</p></body></html>"); // Should return the document unchanged. assert!(doc.tag_name(doc.root()).is_none()); // root is Document node }
#[test] fn test_inline_script_executes() { // Script that modifies a DOM element's text content. let html = r#"<html><body> <div id="target">before</div> <script> var el = document.getElementById("target"); el.textContent = "after"; </script> </body></html>"#; let doc = run_scripts(html);
// Find the div and check its text content was modified. let mut found = false; fn find_div(doc: &Document, node: NodeId, found: &mut bool) { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("target") { // Check child text node. for child in doc.children(node) { if let Some(text) = doc.text_content(child) { if text == "after" { *found = true; } } } } } for child in doc.children(node) { find_div(doc, child, found); } } find_div(&doc, doc.root(), &mut found); assert!(found, "script should have modified div text to 'after'"); }
#[test] fn test_multiple_scripts_share_scope() { let html = r#"<html><body> <script>var x = 42;</script> <script>var y = x + 1;</script> <div id="result">placeholder</div> <script> document.getElementById("result").textContent = String(y); </script> </body></html>"#; let doc = run_scripts(html);
fn find_result(doc: &Document, node: NodeId) -> Option<String> { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("result") { for child in doc.children(node) { if let Some(text) = doc.text_content(child) { return Some(text.to_string()); } } } } for child in doc.children(node) { if let Some(result) = find_result(doc, child) { return Some(result); } } None }
let result = find_result(&doc, doc.root()); assert_eq!(result.as_deref(), Some("43")); }
#[test] fn test_unknown_type_not_executed() { let html = r#"<html><body> <div id="target">original</div> <script type="text/template"> document.getElementById("target").textContent = "changed"; </script> </body></html>"#; let doc = run_scripts(html);
fn find_target(doc: &Document, node: NodeId) -> Option<String> { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("target") { for child in doc.children(node) { if let Some(text) = doc.text_content(child) { return Some(text.to_string()); } } } } for child in doc.children(node) { if let Some(result) = find_target(doc, child) { return Some(result); } } None }
let result = find_target(&doc, doc.root()); assert_eq!(result.as_deref(), Some("original")); }
#[test] fn test_type_text_javascript_executes() { let html = r#"<html><body> <div id="target">before</div> <script type="text/javascript"> document.getElementById("target").textContent = "after"; </script> </body></html>"#; let doc = run_scripts(html);
fn find_target(doc: &Document, node: NodeId) -> Option<String> { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("target") { for child in doc.children(node) { if let Some(text) = doc.text_content(child) { return Some(text.to_string()); } } } } for child in doc.children(node) { if let Some(result) = find_target(doc, child) { return Some(result); } } None }
let result = find_target(&doc, doc.root()); assert_eq!(result.as_deref(), Some("after")); }
#[test] fn test_script_error_does_not_crash() { // A script with a runtime error should not prevent subsequent scripts. let html = r#"<html><body> <div id="target">before</div> <script> undefinedFunction(); </script> <script> document.getElementById("target").textContent = "after"; </script> </body></html>"#; let doc = run_scripts(html);
fn find_target(doc: &Document, node: NodeId) -> Option<String> { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("target") { for child in doc.children(node) { if let Some(text) = doc.text_content(child) { return Some(text.to_string()); } } } } for child in doc.children(node) { if let Some(result) = find_target(doc, child) { return Some(result); } } None }
let result = find_target(&doc, doc.root()); assert_eq!(result.as_deref(), Some("after")); }
#[test] fn test_empty_script_no_crash() { let doc = run_scripts("<html><body><script></script></body></html>"); assert!(!doc.is_empty()); }
#[test] fn test_defer_scripts_run_after_sync() { // In our parse-first model, both sync and defer run after parsing. // defer scripts with src are queued; defer inline scripts are treated // as sync. Without external script loading in tests, we verify that // defer inline scripts still execute (treated as sync per spec — // defer only applies to external scripts). let html = r#"<html><body> <div id="result">0</div> <script defer> var counter = 1; </script> <script> document.getElementById("result").textContent = String(counter); </script> </body></html>"#; let doc = run_scripts(html);
fn find_result(doc: &Document, node: NodeId) -> Option<String> { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("result") { for child in doc.children(node) { if let Some(text) = doc.text_content(child) { return Some(text.to_string()); } } } } for child in doc.children(node) { if let Some(result) = find_result(doc, child) { return Some(result); } } None }
let result = find_result(&doc, doc.root()); // defer on inline scripts is ignored per spec, so counter=1 runs first assert_eq!(result.as_deref(), Some("1")); }
#[test] fn test_script_create_element() { let html = r#"<html><body> <div id="container"></div> <script> var div = document.createElement("p"); div.textContent = "dynamic"; document.getElementById("container").appendChild(div); </script> </body></html>"#; let doc = run_scripts(html);
// Check that a <p> was added inside #container. fn find_dynamic(doc: &Document, node: NodeId) -> bool { if doc.tag_name(node) == Some("div") { if doc.get_attribute(node, "id") == Some("container") { for child in doc.children(node) { if doc.tag_name(child) == Some("p") { return true; } } } } for child in doc.children(node) { if find_dynamic(doc, child) { return true; } } false }
assert!( find_dynamic(&doc, doc.root()), "script should have appended a <p> to #container" ); }}