we (web engine): Experimental web browser project to understand the limits of Claude
at encoding-sniffing 3342 lines 111 kB view raw
1//! DOM-JS bridge: exposes the DOM `Document` to JavaScript. 2//! 3//! Registers the `document` global object and provides native functions for 4//! element access, query methods, factory methods, and element wrapper 5//! properties. 6 7use crate::builtins::{make_native, set_builtin_prop}; 8use crate::gc::{Gc, GcRef}; 9use crate::vm::*; 10use std::rc::Rc; 11use we_css::parser::Parser as CssParser; 12use we_dom::{Document, NodeData, NodeId}; 13use we_html::parse_html; 14use we_style::matching::matches_selector_list; 15 16/// Native callback type alias. 17type NativeMethod = ( 18 &'static str, 19 fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>, 20); 21 22// ── Internal property key for storing NodeId on wrapper objects ────── 23 24const NODE_ID_KEY: &str = "__node_id__"; 25 26// ── Node wrapper creation ─────────────────────────────────────────── 27 28/// Get or create a JS wrapper object for a DOM node. Returns the same 29/// `GcRef` for the same `NodeId` (wrapper identity). 30fn get_or_create_wrapper( 31 node_id: NodeId, 32 gc: &mut Gc<HeapObject>, 33 bridge: &DomBridge, 34 object_proto: Option<GcRef>, 35) -> GcRef { 36 let idx = node_id.index(); 37 if let Some(&existing) = bridge.node_wrappers.borrow().get(&idx) { 38 // Verify the GcRef is still alive. 39 if gc.get(existing).is_some() { 40 return existing; 41 } 42 } 43 44 let doc = bridge.document.borrow(); 45 let mut data = ObjectData::new(); 46 if let Some(proto) = object_proto { 47 data.prototype = Some(proto); 48 } 49 50 // Store the NodeId index as an internal property. 51 data.properties.insert( 52 NODE_ID_KEY.to_string(), 53 Property::builtin(Value::Number(idx as f64)), 54 ); 55 56 // Populate properties based on node type. 57 match doc.node_data(node_id) { 58 NodeData::Element { 59 tag_name, 60 attributes, 61 .. 62 } => { 63 let upper_tag = tag_name.to_ascii_uppercase(); 64 data.properties.insert( 65 "tagName".to_string(), 66 Property::builtin(Value::String(upper_tag.clone())), 67 ); 68 data.properties.insert( 69 "nodeName".to_string(), 70 Property::builtin(Value::String(upper_tag)), 71 ); 72 data.properties.insert( 73 "nodeType".to_string(), 74 Property::builtin(Value::Number(1.0)), 75 ); 76 77 // id attribute 78 let id_val = attributes 79 .iter() 80 .find(|a| a.name == "id") 81 .map(|a| Value::String(a.value.clone())) 82 .unwrap_or(Value::String(String::new())); 83 data.properties 84 .insert("id".to_string(), Property::builtin(id_val)); 85 86 // className attribute 87 let class_val = attributes 88 .iter() 89 .find(|a| a.name == "class") 90 .map(|a| Value::String(a.value.clone())) 91 .unwrap_or(Value::String(String::new())); 92 data.properties 93 .insert("className".to_string(), Property::builtin(class_val)); 94 } 95 NodeData::Text { .. } => { 96 data.properties.insert( 97 "nodeName".to_string(), 98 Property::builtin(Value::String("#text".to_string())), 99 ); 100 data.properties.insert( 101 "nodeType".to_string(), 102 Property::builtin(Value::Number(3.0)), 103 ); 104 } 105 NodeData::Comment { .. } => { 106 data.properties.insert( 107 "nodeName".to_string(), 108 Property::builtin(Value::String("#comment".to_string())), 109 ); 110 data.properties.insert( 111 "nodeType".to_string(), 112 Property::builtin(Value::Number(8.0)), 113 ); 114 } 115 NodeData::Document => { 116 data.properties.insert( 117 "nodeName".to_string(), 118 Property::builtin(Value::String("#document".to_string())), 119 ); 120 data.properties.insert( 121 "nodeType".to_string(), 122 Property::builtin(Value::Number(9.0)), 123 ); 124 } 125 } 126 127 let gc_ref = gc.alloc(HeapObject::Object(data)); 128 129 // Register DOM methods on the wrapper based on node type. 130 register_node_methods(gc, gc_ref, &doc, node_id); 131 132 bridge.node_wrappers.borrow_mut().insert(idx, gc_ref); 133 gc_ref 134} 135 136// ── Helper: walk DOM tree ─────────────────────────────────────────── 137 138fn walk_tree(doc: &Document, root: NodeId, visitor: &mut dyn FnMut(NodeId) -> bool) { 139 let mut stack = vec![root]; 140 while let Some(node) = stack.pop() { 141 if visitor(node) { 142 return; 143 } 144 // Push children in reverse order so first child is visited first. 145 let mut children: Vec<NodeId> = doc.children(node).collect(); 146 children.reverse(); 147 stack.extend(children); 148 } 149} 150 151// ── Helper: make an array of wrapper Values ───────────────────────── 152 153fn make_wrapper_array( 154 nodes: &[NodeId], 155 gc: &mut Gc<HeapObject>, 156 bridge: &DomBridge, 157 object_proto: Option<GcRef>, 158) -> Value { 159 let mut obj = ObjectData::new(); 160 for (i, &nid) in nodes.iter().enumerate() { 161 let wrapper = get_or_create_wrapper(nid, gc, bridge, object_proto); 162 obj.properties 163 .insert(i.to_string(), Property::data(Value::Object(wrapper))); 164 } 165 obj.properties.insert( 166 "length".to_string(), 167 Property { 168 value: Value::Number(nodes.len() as f64), 169 writable: true, 170 enumerable: false, 171 configurable: false, 172 }, 173 ); 174 Value::Object(gc.alloc(HeapObject::Object(obj))) 175} 176 177// ── Document global setup ─────────────────────────────────────────── 178 179/// Register the `document` global object on the VM. Called from 180/// `Vm::attach_document`. 181pub fn init_document_object(vm: &mut Vm) { 182 let mut data = ObjectData::new(); 183 if let Some(proto) = vm.object_prototype { 184 data.prototype = Some(proto); 185 } 186 187 // nodeType 9 = Document 188 data.properties.insert( 189 "nodeType".to_string(), 190 Property::builtin(Value::Number(9.0)), 191 ); 192 data.properties.insert( 193 "nodeName".to_string(), 194 Property::builtin(Value::String("#document".to_string())), 195 ); 196 197 // Set document.title from the DOM. 198 if let Some(bridge) = &vm.dom_bridge { 199 let doc = bridge.document.borrow(); 200 let title = find_title_text(&doc); 201 data.properties 202 .insert("title".to_string(), Property::builtin(Value::String(title))); 203 } 204 205 let doc_ref = vm.gc.alloc(HeapObject::Object(data)); 206 207 // Set documentElement, head, body as wrapper properties. 208 if let Some(bridge) = &vm.dom_bridge { 209 let (html_id, head_id, body_id) = { 210 let doc = bridge.document.borrow(); 211 find_structural_elements(&doc) 212 }; 213 if let Some(html) = html_id { 214 let wrapper = get_or_create_wrapper(html, &mut vm.gc, bridge, vm.object_prototype); 215 set_builtin_prop( 216 &mut vm.gc, 217 doc_ref, 218 "documentElement", 219 Value::Object(wrapper), 220 ); 221 } 222 if let Some(head) = head_id { 223 let wrapper = get_or_create_wrapper(head, &mut vm.gc, bridge, vm.object_prototype); 224 set_builtin_prop(&mut vm.gc, doc_ref, "head", Value::Object(wrapper)); 225 } 226 if let Some(body) = body_id { 227 let wrapper = get_or_create_wrapper(body, &mut vm.gc, bridge, vm.object_prototype); 228 set_builtin_prop(&mut vm.gc, doc_ref, "body", Value::Object(wrapper)); 229 } 230 } 231 232 // Register methods on the document object. 233 let methods: &[NativeMethod] = &[ 234 ("getElementById", doc_get_element_by_id), 235 ("getElementsByTagName", doc_get_elements_by_tag_name), 236 ("getElementsByClassName", doc_get_elements_by_class_name), 237 ("querySelector", doc_query_selector), 238 ("querySelectorAll", doc_query_selector_all), 239 ("createElement", doc_create_element), 240 ("createTextNode", doc_create_text_node), 241 ]; 242 for &(name, callback) in methods { 243 let func = make_native(&mut vm.gc, name, callback); 244 set_builtin_prop(&mut vm.gc, doc_ref, name, Value::Function(func)); 245 } 246 247 vm.set_global("document", Value::Object(doc_ref)); 248} 249 250/// Find `<html>`, `<head>`, and `<body>` elements in the document. 251fn find_structural_elements(doc: &Document) -> (Option<NodeId>, Option<NodeId>, Option<NodeId>) { 252 let mut html = None; 253 let mut head = None; 254 let mut body = None; 255 256 for child in doc.children(doc.root()) { 257 if let NodeData::Element { tag_name, .. } = doc.node_data(child) { 258 if tag_name.eq_ignore_ascii_case("html") { 259 html = Some(child); 260 for inner in doc.children(child) { 261 if let NodeData::Element { tag_name, .. } = doc.node_data(inner) { 262 if tag_name.eq_ignore_ascii_case("head") { 263 head = Some(inner); 264 } else if tag_name.eq_ignore_ascii_case("body") { 265 body = Some(inner); 266 } 267 } 268 } 269 break; 270 } 271 } 272 } 273 274 (html, head, body) 275} 276 277/// Extract the text content of `<title>` from the document. 278fn find_title_text(doc: &Document) -> String { 279 let mut result = String::new(); 280 walk_tree(doc, doc.root(), &mut |node| { 281 if let NodeData::Element { tag_name, .. } = doc.node_data(node) { 282 if tag_name.eq_ignore_ascii_case("title") { 283 // Collect text children. 284 for child in doc.children(node) { 285 if let Some(text) = doc.text_content(child) { 286 result.push_str(text); 287 } 288 } 289 return true; // stop walking 290 } 291 } 292 false 293 }); 294 result 295} 296 297// ── Document methods ──────────────────────────────────────────────── 298 299fn doc_get_element_by_id(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 300 let id = args 301 .first() 302 .map(|v| v.to_js_string(ctx.gc)) 303 .unwrap_or_default(); 304 305 let bridge = match ctx.dom_bridge { 306 Some(b) => b, 307 None => return Ok(Value::Null), 308 }; 309 310 let found = { 311 let doc = bridge.document.borrow(); 312 let mut found = None; 313 walk_tree(&doc, doc.root(), &mut |node| { 314 if let Some(attr_val) = doc.get_attribute(node, "id") { 315 if attr_val == id { 316 found = Some(node); 317 return true; 318 } 319 } 320 false 321 }); 322 found 323 }; 324 325 match found { 326 Some(node_id) => { 327 let wrapper = get_or_create_wrapper(node_id, ctx.gc, bridge, None); 328 Ok(Value::Object(wrapper)) 329 } 330 None => Ok(Value::Null), 331 } 332} 333 334fn doc_get_elements_by_tag_name( 335 args: &[Value], 336 ctx: &mut NativeContext, 337) -> Result<Value, RuntimeError> { 338 let tag = args 339 .first() 340 .map(|v| v.to_js_string(ctx.gc)) 341 .unwrap_or_default(); 342 343 let bridge = match ctx.dom_bridge { 344 Some(b) => b, 345 None => return Ok(make_empty_array(ctx.gc)), 346 }; 347 348 let matches = { 349 let doc = bridge.document.borrow(); 350 let mut matches = Vec::new(); 351 walk_tree(&doc, doc.root(), &mut |node| { 352 if let NodeData::Element { tag_name: tn, .. } = doc.node_data(node) { 353 if tn.eq_ignore_ascii_case(&tag) || tag == "*" { 354 matches.push(node); 355 } 356 } 357 false 358 }); 359 matches 360 }; 361 362 Ok(make_wrapper_array(&matches, ctx.gc, bridge, None)) 363} 364 365fn doc_get_elements_by_class_name( 366 args: &[Value], 367 ctx: &mut NativeContext, 368) -> Result<Value, RuntimeError> { 369 let class_name = args 370 .first() 371 .map(|v| v.to_js_string(ctx.gc)) 372 .unwrap_or_default(); 373 374 let bridge = match ctx.dom_bridge { 375 Some(b) => b, 376 None => return Ok(make_empty_array(ctx.gc)), 377 }; 378 379 let matches = { 380 let doc = bridge.document.borrow(); 381 let mut matches = Vec::new(); 382 walk_tree(&doc, doc.root(), &mut |node| { 383 if let Some(cls) = doc.get_attribute(node, "class") { 384 if cls.split_whitespace().any(|c| c == class_name) { 385 matches.push(node); 386 } 387 } 388 false 389 }); 390 matches 391 }; 392 393 Ok(make_wrapper_array(&matches, ctx.gc, bridge, None)) 394} 395 396fn doc_query_selector(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 397 let selector_str = args 398 .first() 399 .map(|v| v.to_js_string(ctx.gc)) 400 .unwrap_or_default(); 401 402 let bridge = match ctx.dom_bridge { 403 Some(b) => b, 404 None => return Ok(Value::Null), 405 }; 406 407 let selector_list = CssParser::parse_selectors(&selector_str); 408 if selector_list.selectors.is_empty() { 409 return Ok(Value::Null); 410 } 411 412 let found = { 413 let doc = bridge.document.borrow(); 414 let mut found = None; 415 walk_tree(&doc, doc.root(), &mut |node| { 416 if matches!(doc.node_data(node), NodeData::Element { .. }) 417 && matches_selector_list(&doc, node, &selector_list) 418 { 419 found = Some(node); 420 return true; 421 } 422 false 423 }); 424 found 425 }; 426 427 match found { 428 Some(node_id) => { 429 let wrapper = get_or_create_wrapper(node_id, ctx.gc, bridge, None); 430 Ok(Value::Object(wrapper)) 431 } 432 None => Ok(Value::Null), 433 } 434} 435 436fn doc_query_selector_all(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 437 let selector_str = args 438 .first() 439 .map(|v| v.to_js_string(ctx.gc)) 440 .unwrap_or_default(); 441 442 let bridge = match ctx.dom_bridge { 443 Some(b) => b, 444 None => return Ok(make_empty_array(ctx.gc)), 445 }; 446 447 let selector_list = CssParser::parse_selectors(&selector_str); 448 449 let matches = { 450 let doc = bridge.document.borrow(); 451 let mut matches = Vec::new(); 452 walk_tree(&doc, doc.root(), &mut |node| { 453 if matches!(doc.node_data(node), NodeData::Element { .. }) 454 && matches_selector_list(&doc, node, &selector_list) 455 { 456 matches.push(node); 457 } 458 false 459 }); 460 matches 461 }; 462 463 Ok(make_wrapper_array(&matches, ctx.gc, bridge, None)) 464} 465 466fn doc_create_element(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 467 let tag = args 468 .first() 469 .map(|v| v.to_js_string(ctx.gc)) 470 .unwrap_or_default(); 471 472 let bridge = match ctx.dom_bridge { 473 Some(b) => b, 474 None => return Err(RuntimeError::type_error("no document attached")), 475 }; 476 477 let node_id = bridge.document.borrow_mut().create_element(&tag); 478 let wrapper = get_or_create_wrapper(node_id, ctx.gc, bridge, None); 479 Ok(Value::Object(wrapper)) 480} 481 482fn doc_create_text_node(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 483 let text = args 484 .first() 485 .map(|v| v.to_js_string(ctx.gc)) 486 .unwrap_or_default(); 487 488 let bridge = match ctx.dom_bridge { 489 Some(b) => b, 490 None => return Err(RuntimeError::type_error("no document attached")), 491 }; 492 493 let node_id = bridge.document.borrow_mut().create_text(&text); 494 let wrapper = get_or_create_wrapper(node_id, ctx.gc, bridge, None); 495 Ok(Value::Object(wrapper)) 496} 497 498/// Create an empty JS array. 499fn make_empty_array(gc: &mut Gc<HeapObject>) -> Value { 500 let mut obj = ObjectData::new(); 501 obj.properties.insert( 502 "length".to_string(), 503 Property { 504 value: Value::Number(0.0), 505 writable: true, 506 enumerable: false, 507 configurable: false, 508 }, 509 ); 510 Value::Object(gc.alloc(HeapObject::Object(obj))) 511} 512 513// ── Method registration on wrappers ────────────────────────────────── 514 515/// Register DOM methods on a wrapper object based on node type. 516fn register_node_methods(gc: &mut Gc<HeapObject>, wrapper: GcRef, doc: &Document, node_id: NodeId) { 517 // Methods available on all node types. 518 let node_methods: &[NativeMethod] = &[ 519 ("appendChild", node_append_child), 520 ("removeChild", node_remove_child), 521 ("insertBefore", node_insert_before), 522 ("replaceChild", node_replace_child), 523 ("cloneNode", node_clone_node), 524 ("hasChildNodes", node_has_child_nodes), 525 ("addEventListener", event_target_add_listener), 526 ("removeEventListener", event_target_remove_listener), 527 ("dispatchEvent", event_target_dispatch_event), 528 ]; 529 for &(name, callback) in node_methods { 530 let func = make_native(gc, name, callback); 531 set_builtin_prop(gc, wrapper, name, Value::Function(func)); 532 } 533 534 // Methods available only on Element nodes. 535 if matches!(doc.node_data(node_id), NodeData::Element { .. }) { 536 let element_methods: &[NativeMethod] = &[ 537 ("getAttribute", element_get_attribute), 538 ("setAttribute", element_set_attribute), 539 ("removeAttribute", element_remove_attribute), 540 ("hasAttribute", element_has_attribute), 541 ]; 542 for &(name, callback) in element_methods { 543 let func = make_native(gc, name, callback); 544 set_builtin_prop(gc, wrapper, name, Value::Function(func)); 545 } 546 } 547} 548 549// ── Helper: extract NodeId from a wrapper ─────────────────────────── 550 551fn get_node_id(gc: &Gc<HeapObject>, wrapper: GcRef) -> Option<NodeId> { 552 match gc.get(wrapper) { 553 Some(HeapObject::Object(data)) => match data.properties.get(NODE_ID_KEY) { 554 Some(Property { 555 value: Value::Number(n), 556 .. 557 }) => Some(NodeId::from_index(*n as usize)), 558 _ => None, 559 }, 560 _ => None, 561 } 562} 563 564/// Extract a `NodeId` from a JS Value that should be a DOM wrapper object. 565fn value_to_node_id(gc: &Gc<HeapObject>, val: &Value) -> Option<NodeId> { 566 match val { 567 Value::Object(r) => get_node_id(gc, *r), 568 _ => None, 569 } 570} 571 572// ── Node manipulation methods ─────────────────────────────────────── 573 574fn node_append_child(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 575 let bridge = ctx 576 .dom_bridge 577 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 578 let parent_id = match &ctx.this { 579 Value::Object(r) => get_node_id(ctx.gc, *r), 580 _ => None, 581 } 582 .ok_or_else(|| RuntimeError::type_error("appendChild called on non-node"))?; 583 let child_val = args 584 .first() 585 .ok_or_else(|| RuntimeError::type_error("appendChild requires an argument"))?; 586 let child_id = value_to_node_id(ctx.gc, child_val) 587 .ok_or_else(|| RuntimeError::type_error("appendChild argument is not a node"))?; 588 589 bridge 590 .document 591 .borrow_mut() 592 .append_child(parent_id, child_id); 593 Ok(child_val.clone()) 594} 595 596fn node_remove_child(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 597 let bridge = ctx 598 .dom_bridge 599 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 600 let parent_id = match &ctx.this { 601 Value::Object(r) => get_node_id(ctx.gc, *r), 602 _ => None, 603 } 604 .ok_or_else(|| RuntimeError::type_error("removeChild called on non-node"))?; 605 let child_val = args 606 .first() 607 .ok_or_else(|| RuntimeError::type_error("removeChild requires an argument"))?; 608 let child_id = value_to_node_id(ctx.gc, child_val) 609 .ok_or_else(|| RuntimeError::type_error("removeChild argument is not a node"))?; 610 611 bridge 612 .document 613 .borrow_mut() 614 .remove_child(parent_id, child_id); 615 Ok(child_val.clone()) 616} 617 618fn node_insert_before(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 619 let bridge = ctx 620 .dom_bridge 621 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 622 let parent_id = match &ctx.this { 623 Value::Object(r) => get_node_id(ctx.gc, *r), 624 _ => None, 625 } 626 .ok_or_else(|| RuntimeError::type_error("insertBefore called on non-node"))?; 627 let new_node_val = args 628 .first() 629 .ok_or_else(|| RuntimeError::type_error("insertBefore requires two arguments"))?; 630 let new_node_id = value_to_node_id(ctx.gc, new_node_val) 631 .ok_or_else(|| RuntimeError::type_error("insertBefore: first argument is not a node"))?; 632 633 let ref_val = args.get(1).cloned().unwrap_or(Value::Null); 634 if matches!(ref_val, Value::Null) { 635 // insertBefore with null reference = appendChild 636 bridge 637 .document 638 .borrow_mut() 639 .append_child(parent_id, new_node_id); 640 } else { 641 let ref_id = value_to_node_id(ctx.gc, &ref_val).ok_or_else(|| { 642 RuntimeError::type_error("insertBefore: second argument is not a node") 643 })?; 644 bridge 645 .document 646 .borrow_mut() 647 .insert_before(parent_id, new_node_id, ref_id); 648 } 649 Ok(new_node_val.clone()) 650} 651 652fn node_replace_child(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 653 let bridge = ctx 654 .dom_bridge 655 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 656 let parent_id = match &ctx.this { 657 Value::Object(r) => get_node_id(ctx.gc, *r), 658 _ => None, 659 } 660 .ok_or_else(|| RuntimeError::type_error("replaceChild called on non-node"))?; 661 let new_child_val = args 662 .first() 663 .ok_or_else(|| RuntimeError::type_error("replaceChild requires two arguments"))?; 664 let new_child_id = value_to_node_id(ctx.gc, new_child_val) 665 .ok_or_else(|| RuntimeError::type_error("replaceChild: first argument is not a node"))?; 666 let old_child_val = args 667 .get(1) 668 .ok_or_else(|| RuntimeError::type_error("replaceChild requires two arguments"))?; 669 let old_child_id = value_to_node_id(ctx.gc, old_child_val) 670 .ok_or_else(|| RuntimeError::type_error("replaceChild: second argument is not a node"))?; 671 672 bridge 673 .document 674 .borrow_mut() 675 .replace_child(parent_id, new_child_id, old_child_id); 676 Ok(old_child_val.clone()) 677} 678 679fn node_clone_node(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 680 let bridge = ctx 681 .dom_bridge 682 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 683 let node_id = match &ctx.this { 684 Value::Object(r) => get_node_id(ctx.gc, *r), 685 _ => None, 686 } 687 .ok_or_else(|| RuntimeError::type_error("cloneNode called on non-node"))?; 688 689 let deep = args.first().map(|v| v.to_boolean()).unwrap_or(false); 690 691 let cloned_id = bridge.document.borrow_mut().clone_node(node_id, deep); 692 let wrapper = get_or_create_wrapper(cloned_id, ctx.gc, bridge, None); 693 Ok(Value::Object(wrapper)) 694} 695 696fn node_has_child_nodes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 697 let _ = args; 698 let bridge = ctx 699 .dom_bridge 700 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 701 let node_id = match &ctx.this { 702 Value::Object(r) => get_node_id(ctx.gc, *r), 703 _ => None, 704 } 705 .ok_or_else(|| RuntimeError::type_error("hasChildNodes called on non-node"))?; 706 707 let has = bridge.document.borrow().has_child_nodes(node_id); 708 Ok(Value::Boolean(has)) 709} 710 711// ── Attribute methods ─────────────────────────────────────────────── 712 713fn element_get_attribute(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 714 let bridge = ctx 715 .dom_bridge 716 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 717 let node_id = match &ctx.this { 718 Value::Object(r) => get_node_id(ctx.gc, *r), 719 _ => None, 720 } 721 .ok_or_else(|| RuntimeError::type_error("getAttribute called on non-element"))?; 722 let name = args 723 .first() 724 .map(|v| v.to_js_string(ctx.gc)) 725 .unwrap_or_default(); 726 727 let doc = bridge.document.borrow(); 728 match doc.get_attribute(node_id, &name) { 729 Some(val) => Ok(Value::String(val.to_string())), 730 None => Ok(Value::Null), 731 } 732} 733 734fn element_set_attribute(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 735 let bridge = ctx 736 .dom_bridge 737 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 738 let node_id = match &ctx.this { 739 Value::Object(r) => get_node_id(ctx.gc, *r), 740 _ => None, 741 } 742 .ok_or_else(|| RuntimeError::type_error("setAttribute called on non-element"))?; 743 let name = args 744 .first() 745 .map(|v| v.to_js_string(ctx.gc)) 746 .unwrap_or_default(); 747 let value = args 748 .get(1) 749 .map(|v| v.to_js_string(ctx.gc)) 750 .unwrap_or_default(); 751 752 bridge 753 .document 754 .borrow_mut() 755 .set_attribute(node_id, &name, &value); 756 757 // Sync special attributes to wrapper properties. 758 if let Value::Object(wrapper) = &ctx.this { 759 if name == "id" { 760 set_builtin_prop(ctx.gc, *wrapper, "id", Value::String(value.clone())); 761 } else if name == "class" { 762 set_builtin_prop(ctx.gc, *wrapper, "className", Value::String(value.clone())); 763 } 764 } 765 766 Ok(Value::Undefined) 767} 768 769fn element_remove_attribute( 770 args: &[Value], 771 ctx: &mut NativeContext, 772) -> Result<Value, RuntimeError> { 773 let bridge = ctx 774 .dom_bridge 775 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 776 let node_id = match &ctx.this { 777 Value::Object(r) => get_node_id(ctx.gc, *r), 778 _ => None, 779 } 780 .ok_or_else(|| RuntimeError::type_error("removeAttribute called on non-element"))?; 781 let name = args 782 .first() 783 .map(|v| v.to_js_string(ctx.gc)) 784 .unwrap_or_default(); 785 786 bridge 787 .document 788 .borrow_mut() 789 .remove_attribute(node_id, &name); 790 791 // Sync to wrapper. 792 if let Value::Object(wrapper) = &ctx.this { 793 if name == "id" { 794 set_builtin_prop(ctx.gc, *wrapper, "id", Value::String(String::new())); 795 } else if name == "class" { 796 set_builtin_prop(ctx.gc, *wrapper, "className", Value::String(String::new())); 797 } 798 } 799 800 Ok(Value::Undefined) 801} 802 803fn element_has_attribute(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 804 let bridge = ctx 805 .dom_bridge 806 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 807 let node_id = match &ctx.this { 808 Value::Object(r) => get_node_id(ctx.gc, *r), 809 _ => None, 810 } 811 .ok_or_else(|| RuntimeError::type_error("hasAttribute called on non-element"))?; 812 let name = args 813 .first() 814 .map(|v| v.to_js_string(ctx.gc)) 815 .unwrap_or_default(); 816 817 let doc = bridge.document.borrow(); 818 let has = doc.get_attribute(node_id, &name).is_some(); 819 Ok(Value::Boolean(has)) 820} 821 822// ── HTML serialization ────────────────────────────────────────────── 823 824/// Serialize the children of a node to HTML (for innerHTML getter). 825fn serialize_children(doc: &Document, node: NodeId) -> String { 826 let mut html = String::new(); 827 for child in doc.children(node) { 828 serialize_node_to(doc, child, &mut html); 829 } 830 html 831} 832 833/// Serialize a node and its subtree to HTML (for outerHTML getter). 834fn serialize_node_html(doc: &Document, node: NodeId) -> String { 835 let mut html = String::new(); 836 serialize_node_to(doc, node, &mut html); 837 html 838} 839 840fn serialize_node_to(doc: &Document, node: NodeId, out: &mut String) { 841 match doc.node_data(node) { 842 NodeData::Element { 843 tag_name, 844 attributes, 845 .. 846 } => { 847 out.push('<'); 848 out.push_str(tag_name); 849 for attr in attributes { 850 out.push(' '); 851 out.push_str(&attr.name); 852 out.push_str("=\""); 853 escape_attr(&attr.value, out); 854 out.push('"'); 855 } 856 out.push('>'); 857 858 // Void elements don't have closing tags. 859 if !is_void_element(tag_name) { 860 for child in doc.children(node) { 861 serialize_node_to(doc, child, out); 862 } 863 out.push_str("</"); 864 out.push_str(tag_name); 865 out.push('>'); 866 } 867 } 868 NodeData::Text { data } => { 869 escape_text(data, out); 870 } 871 NodeData::Comment { data } => { 872 out.push_str("<!--"); 873 out.push_str(data); 874 out.push_str("-->"); 875 } 876 NodeData::Document => { 877 for child in doc.children(node) { 878 serialize_node_to(doc, child, out); 879 } 880 } 881 } 882} 883 884fn escape_text(s: &str, out: &mut String) { 885 for c in s.chars() { 886 match c { 887 '&' => out.push_str("&amp;"), 888 '<' => out.push_str("&lt;"), 889 '>' => out.push_str("&gt;"), 890 _ => out.push(c), 891 } 892 } 893} 894 895fn escape_attr(s: &str, out: &mut String) { 896 for c in s.chars() { 897 match c { 898 '&' => out.push_str("&amp;"), 899 '"' => out.push_str("&quot;"), 900 _ => out.push(c), 901 } 902 } 903} 904 905fn is_void_element(tag: &str) -> bool { 906 matches!( 907 tag.to_ascii_lowercase().as_str(), 908 "area" 909 | "base" 910 | "br" 911 | "col" 912 | "embed" 913 | "hr" 914 | "img" 915 | "input" 916 | "link" 917 | "meta" 918 | "param" 919 | "source" 920 | "track" 921 | "wbr" 922 ) 923} 924 925// ── Dynamic DOM property resolution ───────────────────────────────── 926 927/// Resolve a dynamic DOM property for a wrapper object. 928/// Called from the VM when a property is not found in the static properties. 929/// Returns `Some(value)` if the key is a recognized DOM property, `None` otherwise. 930pub fn resolve_dom_get( 931 gc: &mut Gc<HeapObject>, 932 bridge: &Rc<DomBridge>, 933 gc_ref: GcRef, 934 key: &str, 935) -> Option<Value> { 936 let node_id = get_node_id(gc, gc_ref)?; 937 let doc = bridge.document.borrow(); 938 939 match key { 940 // ── Navigation properties ──────────────── 941 "parentNode" => { 942 let parent = doc.parent(node_id); 943 drop(doc); 944 Some(match parent { 945 Some(p) => Value::Object(get_or_create_wrapper(p, gc, bridge, None)), 946 None => Value::Null, 947 }) 948 } 949 "parentElement" => { 950 let parent = doc.parent(node_id); 951 let is_element = parent 952 .map(|p| matches!(doc.node_data(p), NodeData::Element { .. })) 953 .unwrap_or(false); 954 drop(doc); 955 Some(if is_element { 956 Value::Object(get_or_create_wrapper(parent.unwrap(), gc, bridge, None)) 957 } else { 958 Value::Null 959 }) 960 } 961 "childNodes" => { 962 let children: Vec<NodeId> = doc.children(node_id).collect(); 963 drop(doc); 964 Some(make_wrapper_array(&children, gc, bridge, None)) 965 } 966 "children" => { 967 let children: Vec<NodeId> = doc 968 .children(node_id) 969 .filter(|&c| matches!(doc.node_data(c), NodeData::Element { .. })) 970 .collect(); 971 drop(doc); 972 Some(make_wrapper_array(&children, gc, bridge, None)) 973 } 974 "firstChild" => { 975 let fc = doc.first_child(node_id); 976 drop(doc); 977 Some(match fc { 978 Some(c) => Value::Object(get_or_create_wrapper(c, gc, bridge, None)), 979 None => Value::Null, 980 }) 981 } 982 "lastChild" => { 983 let lc = doc.last_child(node_id); 984 drop(doc); 985 Some(match lc { 986 Some(c) => Value::Object(get_or_create_wrapper(c, gc, bridge, None)), 987 None => Value::Null, 988 }) 989 } 990 "firstElementChild" => { 991 let fc = doc 992 .children(node_id) 993 .find(|&c| matches!(doc.node_data(c), NodeData::Element { .. })); 994 drop(doc); 995 Some(match fc { 996 Some(c) => Value::Object(get_or_create_wrapper(c, gc, bridge, None)), 997 None => Value::Null, 998 }) 999 } 1000 "lastElementChild" => { 1001 let children: Vec<NodeId> = doc 1002 .children(node_id) 1003 .filter(|&c| matches!(doc.node_data(c), NodeData::Element { .. })) 1004 .collect(); 1005 let last = children.last().copied(); 1006 drop(doc); 1007 Some(match last { 1008 Some(c) => Value::Object(get_or_create_wrapper(c, gc, bridge, None)), 1009 None => Value::Null, 1010 }) 1011 } 1012 "nextSibling" => { 1013 let ns = doc.next_sibling(node_id); 1014 drop(doc); 1015 Some(match ns { 1016 Some(s) => Value::Object(get_or_create_wrapper(s, gc, bridge, None)), 1017 None => Value::Null, 1018 }) 1019 } 1020 "previousSibling" => { 1021 let ps = doc.prev_sibling(node_id); 1022 drop(doc); 1023 Some(match ps { 1024 Some(s) => Value::Object(get_or_create_wrapper(s, gc, bridge, None)), 1025 None => Value::Null, 1026 }) 1027 } 1028 "nextElementSibling" => { 1029 let mut current = doc.next_sibling(node_id); 1030 while let Some(s) = current { 1031 if matches!(doc.node_data(s), NodeData::Element { .. }) { 1032 drop(doc); 1033 return Some(Value::Object(get_or_create_wrapper(s, gc, bridge, None))); 1034 } 1035 current = doc.next_sibling(s); 1036 } 1037 Some(Value::Null) 1038 } 1039 "previousElementSibling" => { 1040 let mut current = doc.prev_sibling(node_id); 1041 while let Some(s) = current { 1042 if matches!(doc.node_data(s), NodeData::Element { .. }) { 1043 drop(doc); 1044 return Some(Value::Object(get_or_create_wrapper(s, gc, bridge, None))); 1045 } 1046 current = doc.prev_sibling(s); 1047 } 1048 Some(Value::Null) 1049 } 1050 1051 // ── Content properties ─────────────────── 1052 "textContent" => { 1053 let text = doc.deep_text_content(node_id); 1054 Some(Value::String(text)) 1055 } 1056 "innerHTML" => { 1057 if !matches!(doc.node_data(node_id), NodeData::Element { .. }) { 1058 return None; 1059 } 1060 let html = serialize_children(&doc, node_id); 1061 Some(Value::String(html)) 1062 } 1063 "outerHTML" => { 1064 if !matches!(doc.node_data(node_id), NodeData::Element { .. }) { 1065 return None; 1066 } 1067 let html = serialize_node_html(&doc, node_id); 1068 Some(Value::String(html)) 1069 } 1070 1071 // ── Attribute array ────────────────────── 1072 "attributes" => { 1073 if let Some(attrs) = doc.attributes(node_id) { 1074 let mut obj = ObjectData::new(); 1075 for (i, attr) in attrs.iter().enumerate() { 1076 let mut attr_obj = ObjectData::new(); 1077 attr_obj.properties.insert( 1078 "name".to_string(), 1079 Property::data(Value::String(attr.name.clone())), 1080 ); 1081 attr_obj.properties.insert( 1082 "value".to_string(), 1083 Property::data(Value::String(attr.value.clone())), 1084 ); 1085 let attr_ref = gc.alloc(HeapObject::Object(attr_obj)); 1086 obj.properties 1087 .insert(i.to_string(), Property::data(Value::Object(attr_ref))); 1088 } 1089 obj.properties.insert( 1090 "length".to_string(), 1091 Property { 1092 value: Value::Number(doc.attributes(node_id).map_or(0, |a| a.len()) as f64), 1093 writable: false, 1094 enumerable: false, 1095 configurable: false, 1096 }, 1097 ); 1098 drop(doc); 1099 Some(Value::Object(gc.alloc(HeapObject::Object(obj)))) 1100 } else { 1101 None 1102 } 1103 } 1104 1105 // ── classList ──────────────────────────── 1106 "classList" => { 1107 if !matches!(doc.node_data(node_id), NodeData::Element { .. }) { 1108 return None; 1109 } 1110 drop(doc); 1111 Some(create_class_list(gc, bridge, node_id)) 1112 } 1113 1114 // ── style ──────────────────────────────── 1115 "style" => { 1116 if !matches!(doc.node_data(node_id), NodeData::Element { .. }) { 1117 return None; 1118 } 1119 let style_str = doc 1120 .get_attribute(node_id, "style") 1121 .unwrap_or("") 1122 .to_string(); 1123 drop(doc); 1124 Some(create_style_object(gc, bridge, node_id, &style_str)) 1125 } 1126 1127 _ => None, 1128 } 1129} 1130 1131/// Handle a DOM property set on a wrapper object. 1132/// Returns `true` if the key was handled (caller should skip normal property set). 1133pub fn handle_dom_set( 1134 gc: &mut Gc<HeapObject>, 1135 bridge: &Rc<DomBridge>, 1136 gc_ref: GcRef, 1137 key: &str, 1138 val: &Value, 1139) -> bool { 1140 let node_id = match get_node_id(gc, gc_ref) { 1141 Some(id) => id, 1142 None => return false, 1143 }; 1144 1145 match key { 1146 "textContent" => { 1147 let text = val.to_js_string(gc); 1148 bridge 1149 .document 1150 .borrow_mut() 1151 .set_element_text_content(node_id, &text); 1152 true 1153 } 1154 "innerHTML" => { 1155 let html_str = val.to_js_string(gc); 1156 let mut doc = bridge.document.borrow_mut(); 1157 // Remove existing children. 1158 while let Some(child) = doc.first_child(node_id) { 1159 doc.remove_child(node_id, child); 1160 } 1161 drop(doc); 1162 // Parse the HTML fragment and adopt children into the real document. 1163 let fragment_doc = parse_html(&format!("<body>{html_str}</body>")); 1164 let frag_root = fragment_doc.root(); 1165 // Find the <body> element in the parsed fragment. 1166 let body = fragment_doc.children(frag_root).find(|&c| { 1167 matches!(fragment_doc.node_data(c), NodeData::Element { tag_name, .. } if tag_name.eq_ignore_ascii_case("html")) 1168 }).and_then(|html| { 1169 fragment_doc.children(html).find(|&c| { 1170 matches!(fragment_doc.node_data(c), NodeData::Element { tag_name, .. } if tag_name.eq_ignore_ascii_case("body")) 1171 }) 1172 }); 1173 if let Some(body_id) = body { 1174 adopt_children( 1175 &fragment_doc, 1176 body_id, 1177 &mut bridge.document.borrow_mut(), 1178 node_id, 1179 ); 1180 } 1181 true 1182 } 1183 "id" => { 1184 let id_val = val.to_js_string(gc); 1185 bridge 1186 .document 1187 .borrow_mut() 1188 .set_attribute(node_id, "id", &id_val); 1189 // Also update the wrapper's static `id` property. 1190 set_builtin_prop(gc, gc_ref, "id", Value::String(id_val)); 1191 true 1192 } 1193 "className" => { 1194 let class_val = val.to_js_string(gc); 1195 bridge 1196 .document 1197 .borrow_mut() 1198 .set_attribute(node_id, "class", &class_val); 1199 set_builtin_prop(gc, gc_ref, "className", Value::String(class_val)); 1200 true 1201 } 1202 _ => false, 1203 } 1204} 1205 1206/// Recursively copy children from a parsed fragment document into the real document. 1207fn adopt_children( 1208 src_doc: &Document, 1209 src_parent: NodeId, 1210 dst_doc: &mut Document, 1211 dst_parent: NodeId, 1212) { 1213 for child in src_doc.children(src_parent) { 1214 let new_node = match src_doc.node_data(child) { 1215 NodeData::Element { 1216 tag_name, 1217 attributes, 1218 .. 1219 } => { 1220 let el = dst_doc.create_element(tag_name); 1221 for attr in attributes { 1222 dst_doc.set_attribute(el, &attr.name, &attr.value); 1223 } 1224 el 1225 } 1226 NodeData::Text { data } => dst_doc.create_text(data), 1227 NodeData::Comment { data } => dst_doc.create_comment(data), 1228 NodeData::Document => continue, 1229 }; 1230 dst_doc.append_child(dst_parent, new_node); 1231 // Recurse for element children. 1232 if matches!(src_doc.node_data(child), NodeData::Element { .. }) { 1233 adopt_children(src_doc, child, dst_doc, new_node); 1234 } 1235 } 1236} 1237 1238/// Check if a DOM property set should be intercepted for a style proxy object. 1239/// Returns `true` if handled. 1240pub fn handle_style_set( 1241 gc: &mut Gc<HeapObject>, 1242 bridge: &Rc<DomBridge>, 1243 gc_ref: GcRef, 1244 key: &str, 1245 val: &Value, 1246) -> bool { 1247 // Check for __style_node_id__ marker. 1248 let node_id = match gc.get(gc_ref) { 1249 Some(HeapObject::Object(data)) => match data.properties.get("__style_node_id__") { 1250 Some(Property { 1251 value: Value::Number(n), 1252 .. 1253 }) => NodeId::from_index(*n as usize), 1254 _ => return false, 1255 }, 1256 _ => return false, 1257 }; 1258 1259 // Set the property on the style object normally. 1260 if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) { 1261 data.properties 1262 .insert(key.to_string(), Property::data(val.clone())); 1263 } 1264 1265 // Serialize all style properties back to the element's style attribute. 1266 let style_str = serialize_style_object(gc, gc_ref); 1267 bridge 1268 .document 1269 .borrow_mut() 1270 .set_attribute(node_id, "style", &style_str); 1271 1272 true 1273} 1274 1275// ── classList helpers ─────────────────────────────────────────────── 1276 1277fn create_class_list(gc: &mut Gc<HeapObject>, _bridge: &DomBridge, node_id: NodeId) -> Value { 1278 let mut data = ObjectData::new(); 1279 // Store the node ID for method callbacks. 1280 data.properties.insert( 1281 NODE_ID_KEY.to_string(), 1282 Property::builtin(Value::Number(node_id.index() as f64)), 1283 ); 1284 1285 let gc_ref = gc.alloc(HeapObject::Object(data)); 1286 1287 let methods: &[NativeMethod] = &[ 1288 ("add", class_list_add), 1289 ("remove", class_list_remove), 1290 ("toggle", class_list_toggle), 1291 ("contains", class_list_contains), 1292 ]; 1293 for &(name, callback) in methods { 1294 let func = make_native(gc, name, callback); 1295 set_builtin_prop(gc, gc_ref, name, Value::Function(func)); 1296 } 1297 1298 Value::Object(gc_ref) 1299} 1300 1301fn get_class_list_node_id(gc: &Gc<HeapObject>, this: &Value) -> Option<NodeId> { 1302 match this { 1303 Value::Object(r) => get_node_id(gc, *r), 1304 _ => None, 1305 } 1306} 1307 1308fn class_list_add(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1309 let bridge = ctx 1310 .dom_bridge 1311 .ok_or_else(|| RuntimeError::type_error("no document"))?; 1312 let node_id = get_class_list_node_id(ctx.gc, &ctx.this) 1313 .ok_or_else(|| RuntimeError::type_error("invalid classList"))?; 1314 1315 for arg in args { 1316 let class_name = arg.to_js_string(ctx.gc); 1317 let doc = bridge.document.borrow(); 1318 let current = doc 1319 .get_attribute(node_id, "class") 1320 .unwrap_or("") 1321 .to_string(); 1322 drop(doc); 1323 if !current.split_whitespace().any(|c| c == class_name) { 1324 let new_val = if current.is_empty() { 1325 class_name 1326 } else { 1327 format!("{current} {class_name}") 1328 }; 1329 bridge 1330 .document 1331 .borrow_mut() 1332 .set_attribute(node_id, "class", &new_val); 1333 } 1334 } 1335 Ok(Value::Undefined) 1336} 1337 1338fn class_list_remove(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1339 let bridge = ctx 1340 .dom_bridge 1341 .ok_or_else(|| RuntimeError::type_error("no document"))?; 1342 let node_id = get_class_list_node_id(ctx.gc, &ctx.this) 1343 .ok_or_else(|| RuntimeError::type_error("invalid classList"))?; 1344 1345 for arg in args { 1346 let class_name = arg.to_js_string(ctx.gc); 1347 let doc = bridge.document.borrow(); 1348 let current = doc 1349 .get_attribute(node_id, "class") 1350 .unwrap_or("") 1351 .to_string(); 1352 drop(doc); 1353 let new_val: Vec<&str> = current 1354 .split_whitespace() 1355 .filter(|&c| c != class_name) 1356 .collect(); 1357 bridge 1358 .document 1359 .borrow_mut() 1360 .set_attribute(node_id, "class", &new_val.join(" ")); 1361 } 1362 Ok(Value::Undefined) 1363} 1364 1365fn class_list_toggle(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1366 let bridge = ctx 1367 .dom_bridge 1368 .ok_or_else(|| RuntimeError::type_error("no document"))?; 1369 let node_id = get_class_list_node_id(ctx.gc, &ctx.this) 1370 .ok_or_else(|| RuntimeError::type_error("invalid classList"))?; 1371 let class_name = args 1372 .first() 1373 .map(|v| v.to_js_string(ctx.gc)) 1374 .unwrap_or_default(); 1375 1376 let doc = bridge.document.borrow(); 1377 let current = doc 1378 .get_attribute(node_id, "class") 1379 .unwrap_or("") 1380 .to_string(); 1381 drop(doc); 1382 1383 let has_class = current.split_whitespace().any(|c| c == class_name); 1384 if has_class { 1385 let new_val: Vec<&str> = current 1386 .split_whitespace() 1387 .filter(|&c| c != class_name) 1388 .collect(); 1389 bridge 1390 .document 1391 .borrow_mut() 1392 .set_attribute(node_id, "class", &new_val.join(" ")); 1393 Ok(Value::Boolean(false)) 1394 } else { 1395 let new_val = if current.is_empty() { 1396 class_name 1397 } else { 1398 format!("{current} {class_name}") 1399 }; 1400 bridge 1401 .document 1402 .borrow_mut() 1403 .set_attribute(node_id, "class", &new_val); 1404 Ok(Value::Boolean(true)) 1405 } 1406} 1407 1408fn class_list_contains(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1409 let bridge = ctx 1410 .dom_bridge 1411 .ok_or_else(|| RuntimeError::type_error("no document"))?; 1412 let node_id = get_class_list_node_id(ctx.gc, &ctx.this) 1413 .ok_or_else(|| RuntimeError::type_error("invalid classList"))?; 1414 let class_name = args 1415 .first() 1416 .map(|v| v.to_js_string(ctx.gc)) 1417 .unwrap_or_default(); 1418 1419 let doc = bridge.document.borrow(); 1420 let current = doc.get_attribute(node_id, "class").unwrap_or(""); 1421 let has = current.split_whitespace().any(|c| c == class_name); 1422 Ok(Value::Boolean(has)) 1423} 1424 1425// ── Style object helpers ──────────────────────────────────────────── 1426 1427fn create_style_object( 1428 gc: &mut Gc<HeapObject>, 1429 _bridge: &DomBridge, 1430 node_id: NodeId, 1431 style_str: &str, 1432) -> Value { 1433 let mut data = ObjectData::new(); 1434 // Marker for style proxy interception. 1435 data.properties.insert( 1436 "__style_node_id__".to_string(), 1437 Property::builtin(Value::Number(node_id.index() as f64)), 1438 ); 1439 1440 // Parse existing inline style into camelCase properties. 1441 for decl in style_str.split(';') { 1442 let decl = decl.trim(); 1443 if decl.is_empty() { 1444 continue; 1445 } 1446 if let Some((prop, val)) = decl.split_once(':') { 1447 let camel = kebab_to_camel(prop.trim()); 1448 data.properties 1449 .insert(camel, Property::data(Value::String(val.trim().to_string()))); 1450 } 1451 } 1452 1453 Value::Object(gc.alloc(HeapObject::Object(data))) 1454} 1455 1456fn serialize_style_object(gc: &Gc<HeapObject>, style_ref: GcRef) -> String { 1457 let mut parts = Vec::new(); 1458 if let Some(HeapObject::Object(data)) = gc.get(style_ref) { 1459 for (key, prop) in &data.properties { 1460 if key.starts_with("__") { 1461 continue; 1462 } 1463 let kebab = camel_to_kebab(key); 1464 let val = match &prop.value { 1465 Value::String(s) => s.clone(), 1466 Value::Number(n) => format!("{n}"), 1467 _ => continue, 1468 }; 1469 parts.push(format!("{kebab}: {val}")); 1470 } 1471 } 1472 parts.join("; ") 1473} 1474 1475fn kebab_to_camel(s: &str) -> String { 1476 let mut result = String::new(); 1477 let mut next_upper = false; 1478 for c in s.chars() { 1479 if c == '-' { 1480 next_upper = true; 1481 } else if next_upper { 1482 result.extend(c.to_uppercase()); 1483 next_upper = false; 1484 } else { 1485 result.push(c); 1486 } 1487 } 1488 result 1489} 1490 1491fn camel_to_kebab(s: &str) -> String { 1492 let mut result = String::new(); 1493 for c in s.chars() { 1494 if c.is_ascii_uppercase() { 1495 result.push('-'); 1496 result.push(c.to_ascii_lowercase()); 1497 } else { 1498 result.push(c); 1499 } 1500 } 1501 result 1502} 1503 1504// ── Event system ──────────────────────────────────────────────────── 1505 1506// Internal property keys for Event objects. 1507const EVENT_TYPE_KEY: &str = "__event_type__"; 1508const EVENT_BUBBLES_KEY: &str = "__event_bubbles__"; 1509const EVENT_CANCELABLE_KEY: &str = "__event_cancelable__"; 1510const EVENT_STOP_PROP_KEY: &str = "__event_stop_prop__"; 1511const EVENT_STOP_IMMEDIATE_KEY: &str = "__event_stop_immediate__"; 1512const EVENT_DEFAULT_PREVENTED_KEY: &str = "__event_default_prevented__"; 1513const EVENT_PHASE_KEY: &str = "__event_phase__"; 1514 1515/// Create an Event JS object with the given type string and options. 1516fn create_event_object( 1517 gc: &mut Gc<HeapObject>, 1518 event_type: &str, 1519 bubbles: bool, 1520 cancelable: bool, 1521) -> GcRef { 1522 let mut data = ObjectData::new(); 1523 1524 // Public properties. 1525 data.properties.insert( 1526 "type".to_string(), 1527 Property::data(Value::String(event_type.to_string())), 1528 ); 1529 data.properties.insert( 1530 "bubbles".to_string(), 1531 Property::data(Value::Boolean(bubbles)), 1532 ); 1533 data.properties.insert( 1534 "cancelable".to_string(), 1535 Property::data(Value::Boolean(cancelable)), 1536 ); 1537 data.properties.insert( 1538 "defaultPrevented".to_string(), 1539 Property::data(Value::Boolean(false)), 1540 ); 1541 data.properties 1542 .insert("eventPhase".to_string(), Property::data(Value::Number(0.0))); 1543 data.properties 1544 .insert("target".to_string(), Property::data(Value::Null)); 1545 data.properties 1546 .insert("currentTarget".to_string(), Property::data(Value::Null)); 1547 data.properties 1548 .insert("timeStamp".to_string(), Property::data(Value::Number(0.0))); 1549 1550 // Internal state. 1551 data.properties.insert( 1552 EVENT_TYPE_KEY.to_string(), 1553 Property::builtin(Value::String(event_type.to_string())), 1554 ); 1555 data.properties.insert( 1556 EVENT_BUBBLES_KEY.to_string(), 1557 Property::builtin(Value::Boolean(bubbles)), 1558 ); 1559 data.properties.insert( 1560 EVENT_CANCELABLE_KEY.to_string(), 1561 Property::builtin(Value::Boolean(cancelable)), 1562 ); 1563 data.properties.insert( 1564 EVENT_STOP_PROP_KEY.to_string(), 1565 Property::builtin(Value::Boolean(false)), 1566 ); 1567 data.properties.insert( 1568 EVENT_STOP_IMMEDIATE_KEY.to_string(), 1569 Property::builtin(Value::Boolean(false)), 1570 ); 1571 data.properties.insert( 1572 EVENT_DEFAULT_PREVENTED_KEY.to_string(), 1573 Property::builtin(Value::Boolean(false)), 1574 ); 1575 data.properties.insert( 1576 EVENT_PHASE_KEY.to_string(), 1577 Property::builtin(Value::Number(0.0)), 1578 ); 1579 1580 // Event phase constants. 1581 data.properties 1582 .insert("NONE".to_string(), Property::builtin(Value::Number(0.0))); 1583 data.properties.insert( 1584 "CAPTURING_PHASE".to_string(), 1585 Property::builtin(Value::Number(1.0)), 1586 ); 1587 data.properties.insert( 1588 "AT_TARGET".to_string(), 1589 Property::builtin(Value::Number(2.0)), 1590 ); 1591 data.properties.insert( 1592 "BUBBLING_PHASE".to_string(), 1593 Property::builtin(Value::Number(3.0)), 1594 ); 1595 1596 let event_ref = gc.alloc(HeapObject::Object(data)); 1597 1598 // Register methods. 1599 let methods: &[NativeMethod] = &[ 1600 ("preventDefault", event_prevent_default), 1601 ("stopPropagation", event_stop_propagation), 1602 ("stopImmediatePropagation", event_stop_immediate_propagation), 1603 ]; 1604 for &(name, callback) in methods { 1605 let func = make_native(gc, name, callback); 1606 set_builtin_prop(gc, event_ref, name, Value::Function(func)); 1607 } 1608 1609 event_ref 1610} 1611 1612/// `Event` constructor: `new Event(type, options)`. 1613fn event_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1614 let event_type = args 1615 .first() 1616 .map(|v| v.to_js_string(ctx.gc)) 1617 .unwrap_or_default(); 1618 1619 let mut bubbles = false; 1620 let mut cancelable = false; 1621 1622 // Parse options object if provided. 1623 if let Some(Value::Object(opts_ref)) = args.get(1) { 1624 if let Some(HeapObject::Object(opts)) = ctx.gc.get(*opts_ref) { 1625 if let Some(prop) = opts.properties.get("bubbles") { 1626 bubbles = prop.value.to_boolean(); 1627 } 1628 if let Some(prop) = opts.properties.get("cancelable") { 1629 cancelable = prop.value.to_boolean(); 1630 } 1631 } 1632 } 1633 1634 let event_ref = create_event_object(ctx.gc, &event_type, bubbles, cancelable); 1635 Ok(Value::Object(event_ref)) 1636} 1637 1638fn event_prevent_default(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1639 if let Value::Object(r) = &ctx.this { 1640 // Only prevent if cancelable. 1641 let cancelable = match ctx.gc.get(*r) { 1642 Some(HeapObject::Object(data)) => data 1643 .properties 1644 .get(EVENT_CANCELABLE_KEY) 1645 .map(|p| p.value.to_boolean()) 1646 .unwrap_or(false), 1647 _ => false, 1648 }; 1649 if cancelable { 1650 set_builtin_prop( 1651 ctx.gc, 1652 *r, 1653 EVENT_DEFAULT_PREVENTED_KEY, 1654 Value::Boolean(true), 1655 ); 1656 set_builtin_prop(ctx.gc, *r, "defaultPrevented", Value::Boolean(true)); 1657 } 1658 } 1659 Ok(Value::Undefined) 1660} 1661 1662fn event_stop_propagation(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1663 if let Value::Object(r) = &ctx.this { 1664 set_builtin_prop(ctx.gc, *r, EVENT_STOP_PROP_KEY, Value::Boolean(true)); 1665 } 1666 Ok(Value::Undefined) 1667} 1668 1669fn event_stop_immediate_propagation( 1670 _args: &[Value], 1671 ctx: &mut NativeContext, 1672) -> Result<Value, RuntimeError> { 1673 if let Value::Object(r) = &ctx.this { 1674 set_builtin_prop(ctx.gc, *r, EVENT_STOP_PROP_KEY, Value::Boolean(true)); 1675 set_builtin_prop(ctx.gc, *r, EVENT_STOP_IMMEDIATE_KEY, Value::Boolean(true)); 1676 } 1677 Ok(Value::Undefined) 1678} 1679 1680// ── EventTarget methods ───────────────────────────────────────────── 1681 1682fn event_target_add_listener( 1683 args: &[Value], 1684 ctx: &mut NativeContext, 1685) -> Result<Value, RuntimeError> { 1686 let bridge = ctx 1687 .dom_bridge 1688 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 1689 let node_id = match &ctx.this { 1690 Value::Object(r) => get_node_id(ctx.gc, *r), 1691 _ => None, 1692 } 1693 .ok_or_else(|| RuntimeError::type_error("addEventListener called on non-node"))?; 1694 1695 let event_type = args 1696 .first() 1697 .map(|v| v.to_js_string(ctx.gc)) 1698 .unwrap_or_default(); 1699 1700 let callback_ref = match args.get(1) { 1701 Some(Value::Function(r)) => *r, 1702 _ => return Ok(Value::Undefined), // Silently ignore non-function 1703 }; 1704 1705 let mut capture = false; 1706 let mut once = false; 1707 1708 // Third arg: boolean (capture) or options object. 1709 if let Some(arg) = args.get(2) { 1710 match arg { 1711 Value::Boolean(b) => capture = *b, 1712 Value::Object(opts_ref) => { 1713 if let Some(HeapObject::Object(opts)) = ctx.gc.get(*opts_ref) { 1714 if let Some(prop) = opts.properties.get("capture") { 1715 capture = prop.value.to_boolean(); 1716 } 1717 if let Some(prop) = opts.properties.get("once") { 1718 once = prop.value.to_boolean(); 1719 } 1720 } 1721 } 1722 _ => {} 1723 } 1724 } 1725 1726 let idx = node_id.index(); 1727 let mut listeners = bridge.event_listeners.borrow_mut(); 1728 let list = listeners.entry(idx).or_default(); 1729 1730 // Don't add duplicate: same type, same callback ref, same capture. 1731 let already_exists = list 1732 .iter() 1733 .any(|l| l.event_type == event_type && l.callback == callback_ref && l.capture == capture); 1734 if !already_exists { 1735 list.push(EventListener { 1736 event_type, 1737 callback: callback_ref, 1738 capture, 1739 once, 1740 }); 1741 } 1742 1743 Ok(Value::Undefined) 1744} 1745 1746fn event_target_remove_listener( 1747 args: &[Value], 1748 ctx: &mut NativeContext, 1749) -> Result<Value, RuntimeError> { 1750 let bridge = ctx 1751 .dom_bridge 1752 .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 1753 let node_id = match &ctx.this { 1754 Value::Object(r) => get_node_id(ctx.gc, *r), 1755 _ => None, 1756 } 1757 .ok_or_else(|| RuntimeError::type_error("removeEventListener called on non-node"))?; 1758 1759 let event_type = args 1760 .first() 1761 .map(|v| v.to_js_string(ctx.gc)) 1762 .unwrap_or_default(); 1763 1764 let callback_ref = match args.get(1) { 1765 Some(Value::Function(r)) => *r, 1766 _ => return Ok(Value::Undefined), 1767 }; 1768 1769 let mut capture = false; 1770 if let Some(arg) = args.get(2) { 1771 match arg { 1772 Value::Boolean(b) => capture = *b, 1773 Value::Object(opts_ref) => { 1774 if let Some(HeapObject::Object(opts)) = ctx.gc.get(*opts_ref) { 1775 if let Some(prop) = opts.properties.get("capture") { 1776 capture = prop.value.to_boolean(); 1777 } 1778 } 1779 } 1780 _ => {} 1781 } 1782 } 1783 1784 let idx = node_id.index(); 1785 let mut listeners = bridge.event_listeners.borrow_mut(); 1786 if let Some(list) = listeners.get_mut(&idx) { 1787 list.retain(|l| { 1788 !(l.event_type == event_type && l.callback == callback_ref && l.capture == capture) 1789 }); 1790 } 1791 1792 Ok(Value::Undefined) 1793} 1794 1795/// Native `dispatchEvent` — returns a marker for the VM to handle. 1796fn event_target_dispatch_event( 1797 args: &[Value], 1798 ctx: &mut NativeContext, 1799) -> Result<Value, RuntimeError> { 1800 let node_id = match &ctx.this { 1801 Value::Object(r) => get_node_id(ctx.gc, *r), 1802 _ => None, 1803 } 1804 .ok_or_else(|| RuntimeError::type_error("dispatchEvent called on non-node"))?; 1805 1806 let event_ref = match args.first() { 1807 Some(Value::Object(r)) => *r, 1808 _ => { 1809 return Err(RuntimeError::type_error( 1810 "dispatchEvent requires an Event argument", 1811 )) 1812 } 1813 }; 1814 1815 // Build a marker object for the VM to process. 1816 let mut marker = ObjectData::new(); 1817 marker.properties.insert( 1818 "__event_dispatch__".to_string(), 1819 Property::builtin(Value::Boolean(true)), 1820 ); 1821 marker.properties.insert( 1822 "__target_id__".to_string(), 1823 Property::builtin(Value::Number(node_id.index() as f64)), 1824 ); 1825 marker.properties.insert( 1826 "__event_ref__".to_string(), 1827 Property::builtin(Value::Object(event_ref)), 1828 ); 1829 1830 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(marker)))) 1831} 1832 1833/// Register the `Event` constructor and event target methods on the VM. 1834pub fn init_event_system(vm: &mut Vm) { 1835 // Register `Event` as a global constructor. 1836 let ctor = make_native(&mut vm.gc, "Event", event_constructor); 1837 vm.set_global("Event", Value::Function(ctor)); 1838 1839 // Add event target methods to the document object. 1840 if let Some(Value::Object(doc_ref)) = vm.get_global("document").cloned() { 1841 let methods: &[NativeMethod] = &[ 1842 ("addEventListener", event_target_add_listener), 1843 ("removeEventListener", event_target_remove_listener), 1844 ("dispatchEvent", event_target_dispatch_event), 1845 ]; 1846 for &(name, callback) in methods { 1847 let func = make_native(&mut vm.gc, name, callback); 1848 set_builtin_prop(&mut vm.gc, doc_ref, name, Value::Function(func)); 1849 } 1850 1851 // Ensure the document object has __node_id__ set to the root node. 1852 if let Some(bridge) = &vm.dom_bridge { 1853 let root_idx = bridge.document.borrow().root().index(); 1854 set_builtin_prop( 1855 &mut vm.gc, 1856 doc_ref, 1857 NODE_ID_KEY, 1858 Value::Number(root_idx as f64), 1859 ); 1860 // Also cache the document wrapper. 1861 bridge.node_wrappers.borrow_mut().insert(root_idx, doc_ref); 1862 } 1863 } 1864} 1865 1866/// Run the event dispatch algorithm. Called from the VM when it detects an 1867/// event dispatch marker returned by the native `dispatchEvent`. 1868/// 1869/// Returns the result of `!event.defaultPrevented`. 1870pub fn run_event_dispatch(vm: &mut Vm, target_idx: usize, event_ref: GcRef) -> Value { 1871 let bridge = match vm.dom_bridge.clone() { 1872 Some(b) => b, 1873 None => return Value::Boolean(true), 1874 }; 1875 1876 let target_id = NodeId::from_index(target_idx); 1877 1878 // Build propagation path: target -> ... -> root. 1879 let path = { 1880 let doc = bridge.document.borrow(); 1881 let mut path = vec![target_id]; 1882 let mut current = target_id; 1883 while let Some(parent) = doc.parent(current) { 1884 path.push(parent); 1885 current = parent; 1886 } 1887 path.reverse(); // Now root -> ... -> target. 1888 path 1889 }; 1890 1891 // Set event.target to the target wrapper. 1892 let target_wrapper = get_or_create_wrapper(target_id, &mut vm.gc, &bridge, vm.object_prototype); 1893 set_builtin_prop( 1894 &mut vm.gc, 1895 event_ref, 1896 "target", 1897 Value::Object(target_wrapper), 1898 ); 1899 1900 let target_pos = path.len() - 1; 1901 1902 // --- Capture phase (root to target, excluding target) --- 1903 set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(1.0)); 1904 set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(1.0)); 1905 1906 for &node_id in &path[..target_pos] { 1907 if is_propagation_stopped(&vm.gc, event_ref) { 1908 break; 1909 } 1910 let wrapper = get_or_create_wrapper(node_id, &mut vm.gc, &bridge, vm.object_prototype); 1911 set_builtin_prop( 1912 &mut vm.gc, 1913 event_ref, 1914 "currentTarget", 1915 Value::Object(wrapper), 1916 ); 1917 invoke_listeners(vm, &bridge, node_id, event_ref, wrapper, true); 1918 } 1919 1920 // --- At-target phase --- 1921 if !is_propagation_stopped(&vm.gc, event_ref) { 1922 set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(2.0)); 1923 set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(2.0)); 1924 set_builtin_prop( 1925 &mut vm.gc, 1926 event_ref, 1927 "currentTarget", 1928 Value::Object(target_wrapper), 1929 ); 1930 // At target, fire BOTH capture and bubble listeners in registration order. 1931 invoke_listeners(vm, &bridge, target_id, event_ref, target_wrapper, false); 1932 } 1933 1934 // --- Bubble phase (target to root, excluding target) --- 1935 let bubbles = match vm.gc.get(event_ref) { 1936 Some(HeapObject::Object(data)) => data 1937 .properties 1938 .get(EVENT_BUBBLES_KEY) 1939 .map(|p| p.value.to_boolean()) 1940 .unwrap_or(false), 1941 _ => false, 1942 }; 1943 1944 if bubbles && !is_propagation_stopped(&vm.gc, event_ref) { 1945 set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(3.0)); 1946 set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(3.0)); 1947 1948 for &node_id in path[..target_pos].iter().rev() { 1949 if is_propagation_stopped(&vm.gc, event_ref) { 1950 break; 1951 } 1952 let wrapper = get_or_create_wrapper(node_id, &mut vm.gc, &bridge, vm.object_prototype); 1953 set_builtin_prop( 1954 &mut vm.gc, 1955 event_ref, 1956 "currentTarget", 1957 Value::Object(wrapper), 1958 ); 1959 invoke_listeners(vm, &bridge, node_id, event_ref, wrapper, false); 1960 } 1961 } 1962 1963 // Reset event state. 1964 set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(0.0)); 1965 set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(0.0)); 1966 set_builtin_prop(&mut vm.gc, event_ref, "currentTarget", Value::Null); 1967 1968 // Return !defaultPrevented. 1969 let default_prevented = match vm.gc.get(event_ref) { 1970 Some(HeapObject::Object(data)) => data 1971 .properties 1972 .get(EVENT_DEFAULT_PREVENTED_KEY) 1973 .map(|p| p.value.to_boolean()) 1974 .unwrap_or(false), 1975 _ => false, 1976 }; 1977 Value::Boolean(!default_prevented) 1978} 1979 1980fn is_propagation_stopped(gc: &Gc<HeapObject>, event_ref: GcRef) -> bool { 1981 match gc.get(event_ref) { 1982 Some(HeapObject::Object(data)) => data 1983 .properties 1984 .get(EVENT_STOP_PROP_KEY) 1985 .map(|p| p.value.to_boolean()) 1986 .unwrap_or(false), 1987 _ => false, 1988 } 1989} 1990 1991fn is_immediate_stopped(gc: &Gc<HeapObject>, event_ref: GcRef) -> bool { 1992 match gc.get(event_ref) { 1993 Some(HeapObject::Object(data)) => data 1994 .properties 1995 .get(EVENT_STOP_IMMEDIATE_KEY) 1996 .map(|p| p.value.to_boolean()) 1997 .unwrap_or(false), 1998 _ => false, 1999 } 2000} 2001 2002/// Invoke matching listeners on a node for the given event. 2003/// 2004/// When `capture_only` is true (capture phase), only capture listeners fire. 2005/// When false (at-target or bubble), at-target fires all listeners and 2006/// bubble fires only non-capture listeners. We distinguish by the event phase: 2007/// at-target (phase 2) fires all, bubble (phase 3) fires only non-capture. 2008fn invoke_listeners( 2009 vm: &mut Vm, 2010 bridge: &DomBridge, 2011 node_id: NodeId, 2012 event_ref: GcRef, 2013 current_target_wrapper: GcRef, 2014 capture_only: bool, 2015) { 2016 let event_type = match vm.gc.get(event_ref) { 2017 Some(HeapObject::Object(data)) => match data.properties.get(EVENT_TYPE_KEY) { 2018 Some(Property { 2019 value: Value::String(s), 2020 .. 2021 }) => s.clone(), 2022 _ => return, 2023 }, 2024 _ => return, 2025 }; 2026 2027 let phase = match vm.gc.get(event_ref) { 2028 Some(HeapObject::Object(data)) => match data.properties.get(EVENT_PHASE_KEY) { 2029 Some(Property { 2030 value: Value::Number(n), 2031 .. 2032 }) => *n as u8, 2033 _ => 0, 2034 }, 2035 _ => 0, 2036 }; 2037 2038 let idx = node_id.index(); 2039 2040 // Collect matching listeners (snapshot the list to avoid borrow issues). 2041 let matching: Vec<(GcRef, bool)> = { 2042 let listeners = bridge.event_listeners.borrow(); 2043 match listeners.get(&idx) { 2044 Some(list) => list 2045 .iter() 2046 .filter(|l| { 2047 if l.event_type != event_type { 2048 return false; 2049 } 2050 if capture_only { 2051 // Capture phase: only capture listeners. 2052 l.capture 2053 } else if phase == 2 { 2054 // At-target: all listeners fire. 2055 true 2056 } else { 2057 // Bubble phase: only non-capture listeners. 2058 !l.capture 2059 } 2060 }) 2061 .map(|l| (l.callback, l.once)) 2062 .collect(), 2063 None => return, 2064 } 2065 }; 2066 2067 // Invoke each listener. 2068 for (callback_ref, once) in &matching { 2069 if is_immediate_stopped(&vm.gc, event_ref) { 2070 break; 2071 } 2072 // Set `this` to currentTarget for the callback. 2073 let old_this = vm.get_global("this").cloned(); 2074 vm.set_global("this", Value::Object(current_target_wrapper)); 2075 2076 let _ = vm.call_function(*callback_ref, &[Value::Object(event_ref)]); 2077 2078 // Restore `this`. 2079 match old_this { 2080 Some(v) => { 2081 vm.set_global("this", v); 2082 } 2083 None => { 2084 vm.remove_global("this"); 2085 } 2086 } 2087 2088 // Remove if once. 2089 if *once { 2090 let mut listeners = bridge.event_listeners.borrow_mut(); 2091 if let Some(list) = listeners.get_mut(&idx) { 2092 list.retain(|l| { 2093 !(l.event_type == event_type && l.callback == *callback_ref && l.once) 2094 }); 2095 } 2096 } 2097 } 2098} 2099 2100// ── Tests ─────────────────────────────────────────────────────────── 2101 2102#[cfg(test)] 2103mod tests { 2104 use super::*; 2105 use crate::compiler; 2106 use crate::parser::Parser; 2107 use we_html::parse_html; 2108 2109 /// Build a Document from HTML source by parsing it with the HTML parser. 2110 fn doc_from_html(html: &str) -> we_dom::Document { 2111 parse_html(html) 2112 } 2113 2114 /// Evaluate JS with a DOM document attached. 2115 fn eval_with_doc(html: &str, js: &str) -> Result<Value, RuntimeError> { 2116 let doc = doc_from_html(html); 2117 let program = Parser::parse(js).expect("parse failed"); 2118 let func = compiler::compile(&program).expect("compile failed"); 2119 let mut vm = Vm::new(); 2120 vm.attach_document(doc); 2121 vm.execute(&func) 2122 } 2123 2124 #[test] 2125 fn test_document_is_global() { 2126 let result = eval_with_doc("<html><body></body></html>", "typeof document").unwrap(); 2127 assert_eq!(result.to_js_string(&crate::gc::Gc::new()), "object"); 2128 } 2129 2130 #[test] 2131 fn test_document_node_type() { 2132 let result = eval_with_doc("<html><body></body></html>", "document.nodeType").unwrap(); 2133 match result { 2134 Value::Number(n) => assert_eq!(n, 9.0), 2135 v => panic!("expected 9, got {v:?}"), 2136 } 2137 } 2138 2139 #[test] 2140 fn test_document_body() { 2141 let result = eval_with_doc("<html><body></body></html>", "document.body.tagName").unwrap(); 2142 match result { 2143 Value::String(s) => assert_eq!(s, "BODY"), 2144 v => panic!("expected 'BODY', got {v:?}"), 2145 } 2146 } 2147 2148 #[test] 2149 fn test_document_head() { 2150 let result = eval_with_doc( 2151 "<html><head></head><body></body></html>", 2152 "document.head.tagName", 2153 ) 2154 .unwrap(); 2155 match result { 2156 Value::String(s) => assert_eq!(s, "HEAD"), 2157 v => panic!("expected 'HEAD', got {v:?}"), 2158 } 2159 } 2160 2161 #[test] 2162 fn test_document_document_element() { 2163 let result = eval_with_doc( 2164 "<html><body></body></html>", 2165 "document.documentElement.tagName", 2166 ) 2167 .unwrap(); 2168 match result { 2169 Value::String(s) => assert_eq!(s, "HTML"), 2170 v => panic!("expected 'HTML', got {v:?}"), 2171 } 2172 } 2173 2174 #[test] 2175 fn test_get_element_by_id_found() { 2176 let result = eval_with_doc( 2177 r#"<html><body><div id="foo">hello</div></body></html>"#, 2178 r#"document.getElementById("foo").tagName"#, 2179 ) 2180 .unwrap(); 2181 match result { 2182 Value::String(s) => assert_eq!(s, "DIV"), 2183 v => panic!("expected 'DIV', got {v:?}"), 2184 } 2185 } 2186 2187 #[test] 2188 fn test_get_element_by_id_not_found() { 2189 let result = eval_with_doc( 2190 "<html><body></body></html>", 2191 r#"document.getElementById("nope")"#, 2192 ) 2193 .unwrap(); 2194 assert!(matches!(result, Value::Null)); 2195 } 2196 2197 #[test] 2198 fn test_get_element_by_id_returns_correct_id() { 2199 let result = eval_with_doc( 2200 r#"<html><body><div id="myid"></div></body></html>"#, 2201 r#"document.getElementById("myid").id"#, 2202 ) 2203 .unwrap(); 2204 match result { 2205 Value::String(s) => assert_eq!(s, "myid"), 2206 v => panic!("expected 'myid', got {v:?}"), 2207 } 2208 } 2209 2210 #[test] 2211 fn test_create_element() { 2212 let result = eval_with_doc( 2213 "<html><body></body></html>", 2214 r#"document.createElement("div").tagName"#, 2215 ) 2216 .unwrap(); 2217 match result { 2218 Value::String(s) => assert_eq!(s, "DIV"), 2219 v => panic!("expected 'DIV', got {v:?}"), 2220 } 2221 } 2222 2223 #[test] 2224 fn test_create_text_node() { 2225 let result = eval_with_doc( 2226 "<html><body></body></html>", 2227 r#"document.createTextNode("hello").nodeType"#, 2228 ) 2229 .unwrap(); 2230 match result { 2231 Value::Number(n) => assert_eq!(n, 3.0), 2232 v => panic!("expected 3, got {v:?}"), 2233 } 2234 } 2235 2236 #[test] 2237 fn test_element_node_type() { 2238 let result = eval_with_doc("<html><body></body></html>", "document.body.nodeType").unwrap(); 2239 match result { 2240 Value::Number(n) => assert_eq!(n, 1.0), 2241 v => panic!("expected 1, got {v:?}"), 2242 } 2243 } 2244 2245 #[test] 2246 fn test_query_selector_by_class() { 2247 let result = eval_with_doc( 2248 r#"<html><body><p class="intro">hi</p></body></html>"#, 2249 r#"document.querySelector(".intro").tagName"#, 2250 ) 2251 .unwrap(); 2252 match result { 2253 Value::String(s) => assert_eq!(s, "P"), 2254 v => panic!("expected 'P', got {v:?}"), 2255 } 2256 } 2257 2258 #[test] 2259 fn test_query_selector_not_found() { 2260 let result = eval_with_doc( 2261 "<html><body></body></html>", 2262 r#"document.querySelector(".missing")"#, 2263 ) 2264 .unwrap(); 2265 assert!(matches!(result, Value::Null)); 2266 } 2267 2268 #[test] 2269 fn test_query_selector_all() { 2270 let result = eval_with_doc( 2271 r#"<html><body><p>a</p><p>b</p><p>c</p></body></html>"#, 2272 r#"document.querySelectorAll("p").length"#, 2273 ) 2274 .unwrap(); 2275 match result { 2276 Value::Number(n) => assert_eq!(n, 3.0), 2277 v => panic!("expected 3, got {v:?}"), 2278 } 2279 } 2280 2281 #[test] 2282 fn test_get_elements_by_tag_name() { 2283 let result = eval_with_doc( 2284 r#"<html><body><div>a</div><div>b</div></body></html>"#, 2285 r#"document.getElementsByTagName("div").length"#, 2286 ) 2287 .unwrap(); 2288 match result { 2289 Value::Number(n) => assert_eq!(n, 2.0), 2290 v => panic!("expected 2, got {v:?}"), 2291 } 2292 } 2293 2294 #[test] 2295 fn test_get_elements_by_class_name() { 2296 let result = eval_with_doc( 2297 r#"<html><body><span class="x">a</span><span class="x">b</span></body></html>"#, 2298 r#"document.getElementsByClassName("x").length"#, 2299 ) 2300 .unwrap(); 2301 match result { 2302 Value::Number(n) => assert_eq!(n, 2.0), 2303 v => panic!("expected 2, got {v:?}"), 2304 } 2305 } 2306 2307 #[test] 2308 fn test_wrapper_identity() { 2309 let result = eval_with_doc( 2310 r#"<html><body><div id="x"></div></body></html>"#, 2311 r#"document.getElementById("x") === document.getElementById("x")"#, 2312 ) 2313 .unwrap(); 2314 match result { 2315 Value::Boolean(b) => assert!(b, "same node should return same wrapper object"), 2316 v => panic!("expected true, got {v:?}"), 2317 } 2318 } 2319 2320 #[test] 2321 fn test_document_title() { 2322 let result = eval_with_doc( 2323 "<html><head><title>Test Page</title></head><body></body></html>", 2324 "document.title", 2325 ) 2326 .unwrap(); 2327 match result { 2328 Value::String(s) => assert_eq!(s, "Test Page"), 2329 v => panic!("expected 'Test Page', got {v:?}"), 2330 } 2331 } 2332 2333 #[test] 2334 fn test_element_class_name() { 2335 let result = eval_with_doc( 2336 r#"<html><body><div id="d" class="foo bar"></div></body></html>"#, 2337 r#"document.getElementById("d").className"#, 2338 ) 2339 .unwrap(); 2340 match result { 2341 Value::String(s) => assert_eq!(s, "foo bar"), 2342 v => panic!("expected 'foo bar', got {v:?}"), 2343 } 2344 } 2345 2346 #[test] 2347 fn test_missing_element_returns_null() { 2348 let result = eval_with_doc( 2349 "<html><body></body></html>", 2350 r#"document.getElementById("nope") === null"#, 2351 ) 2352 .unwrap(); 2353 match result { 2354 Value::Boolean(b) => assert!(b), 2355 v => panic!("expected true, got {v:?}"), 2356 } 2357 } 2358 2359 // ── appendChild tests ──────────────────────────────── 2360 2361 #[test] 2362 fn test_append_child_moves_node() { 2363 let result = eval_with_doc( 2364 r#"<html><body><div id="parent"></div></body></html>"#, 2365 r#" 2366 var parent = document.getElementById("parent"); 2367 var child = document.createElement("span"); 2368 parent.appendChild(child); 2369 parent.hasChildNodes() 2370 "#, 2371 ) 2372 .unwrap(); 2373 assert!(matches!(result, Value::Boolean(true))); 2374 } 2375 2376 #[test] 2377 fn test_append_child_returns_child() { 2378 let result = eval_with_doc( 2379 r#"<html><body><div id="p"></div></body></html>"#, 2380 r#" 2381 var p = document.getElementById("p"); 2382 var c = document.createElement("span"); 2383 var returned = p.appendChild(c); 2384 returned === c 2385 "#, 2386 ) 2387 .unwrap(); 2388 assert!(matches!(result, Value::Boolean(true))); 2389 } 2390 2391 // ── removeChild tests ──────────────────────────────── 2392 2393 #[test] 2394 fn test_remove_child() { 2395 let result = eval_with_doc( 2396 r#"<html><body><div id="p"><span id="c"></span></div></body></html>"#, 2397 r#" 2398 var p = document.getElementById("p"); 2399 var c = document.getElementById("c"); 2400 p.removeChild(c); 2401 p.hasChildNodes() 2402 "#, 2403 ) 2404 .unwrap(); 2405 assert!(matches!(result, Value::Boolean(false))); 2406 } 2407 2408 // ── insertBefore tests ─────────────────────────────── 2409 2410 #[test] 2411 fn test_insert_before() { 2412 let result = eval_with_doc( 2413 r#"<html><body><div id="p"><span id="ref"></span></div></body></html>"#, 2414 r#" 2415 var p = document.getElementById("p"); 2416 var ref = document.getElementById("ref"); 2417 var newNode = document.createElement("em"); 2418 p.insertBefore(newNode, ref); 2419 p.firstChild.tagName 2420 "#, 2421 ) 2422 .unwrap(); 2423 match result { 2424 Value::String(s) => assert_eq!(s, "EM"), 2425 v => panic!("expected 'EM', got {v:?}"), 2426 } 2427 } 2428 2429 #[test] 2430 fn test_insert_before_null_appends() { 2431 let result = eval_with_doc( 2432 r#"<html><body><div id="p"></div></body></html>"#, 2433 r#" 2434 var p = document.getElementById("p"); 2435 var newNode = document.createElement("em"); 2436 p.insertBefore(newNode, null); 2437 p.firstChild.tagName 2438 "#, 2439 ) 2440 .unwrap(); 2441 match result { 2442 Value::String(s) => assert_eq!(s, "EM"), 2443 v => panic!("expected 'EM', got {v:?}"), 2444 } 2445 } 2446 2447 // ── replaceChild tests ─────────────────────────────── 2448 2449 #[test] 2450 fn test_replace_child() { 2451 let result = eval_with_doc( 2452 r#"<html><body><div id="p"><span id="old"></span></div></body></html>"#, 2453 r#" 2454 var p = document.getElementById("p"); 2455 var old = document.getElementById("old"); 2456 var newEl = document.createElement("em"); 2457 p.replaceChild(newEl, old); 2458 p.firstChild.tagName 2459 "#, 2460 ) 2461 .unwrap(); 2462 match result { 2463 Value::String(s) => assert_eq!(s, "EM"), 2464 v => panic!("expected 'EM', got {v:?}"), 2465 } 2466 } 2467 2468 // ── cloneNode tests ────────────────────────────────── 2469 2470 #[test] 2471 fn test_clone_node_shallow() { 2472 let result = eval_with_doc( 2473 r#"<html><body><div id="orig"><span>child</span></div></body></html>"#, 2474 r#" 2475 var orig = document.getElementById("orig"); 2476 var clone = orig.cloneNode(false); 2477 clone.hasChildNodes() 2478 "#, 2479 ) 2480 .unwrap(); 2481 assert!(matches!(result, Value::Boolean(false))); 2482 } 2483 2484 #[test] 2485 fn test_clone_node_deep() { 2486 let result = eval_with_doc( 2487 r#"<html><body><div id="orig"><span>child</span></div></body></html>"#, 2488 r#" 2489 var orig = document.getElementById("orig"); 2490 var clone = orig.cloneNode(true); 2491 clone.hasChildNodes() 2492 "#, 2493 ) 2494 .unwrap(); 2495 assert!(matches!(result, Value::Boolean(true))); 2496 } 2497 2498 // ── textContent tests ──────────────────────────────── 2499 2500 #[test] 2501 fn test_text_content_get() { 2502 let result = eval_with_doc( 2503 r#"<html><body><div id="d">hello <span>world</span></div></body></html>"#, 2504 r#"document.getElementById("d").textContent"#, 2505 ) 2506 .unwrap(); 2507 match result { 2508 Value::String(s) => assert_eq!(s, "hello world"), 2509 v => panic!("expected 'hello world', got {v:?}"), 2510 } 2511 } 2512 2513 #[test] 2514 fn test_text_content_set() { 2515 let result = eval_with_doc( 2516 r#"<html><body><div id="d"><span>old</span></div></body></html>"#, 2517 r#" 2518 var d = document.getElementById("d"); 2519 d.textContent = "new text"; 2520 d.textContent 2521 "#, 2522 ) 2523 .unwrap(); 2524 match result { 2525 Value::String(s) => assert_eq!(s, "new text"), 2526 v => panic!("expected 'new text', got {v:?}"), 2527 } 2528 } 2529 2530 // ── innerHTML tests ────────────────────────────────── 2531 2532 #[test] 2533 fn test_inner_html_get() { 2534 let result = eval_with_doc( 2535 r#"<html><body><div id="d"><span>hi</span></div></body></html>"#, 2536 r#"document.getElementById("d").innerHTML"#, 2537 ) 2538 .unwrap(); 2539 match result { 2540 Value::String(s) => assert_eq!(s, "<span>hi</span>"), 2541 v => panic!("expected '<span>hi</span>', got {v:?}"), 2542 } 2543 } 2544 2545 #[test] 2546 fn test_inner_html_set() { 2547 let result = eval_with_doc( 2548 r#"<html><body><div id="d"></div></body></html>"#, 2549 r#" 2550 var d = document.getElementById("d"); 2551 d.innerHTML = "<em>new</em>"; 2552 d.firstChild.tagName 2553 "#, 2554 ) 2555 .unwrap(); 2556 match result { 2557 Value::String(s) => assert_eq!(s, "EM"), 2558 v => panic!("expected 'EM', got {v:?}"), 2559 } 2560 } 2561 2562 // ── outerHTML test ─────────────────────────────────── 2563 2564 #[test] 2565 fn test_outer_html() { 2566 let result = eval_with_doc( 2567 r#"<html><body><div id="d"><span>hi</span></div></body></html>"#, 2568 r#"document.getElementById("d").outerHTML"#, 2569 ) 2570 .unwrap(); 2571 match result { 2572 Value::String(s) => assert!( 2573 s.contains("<div") && s.contains("</div>") && s.contains("<span>hi</span>"), 2574 "unexpected outerHTML: {s}" 2575 ), 2576 v => panic!("expected string, got {v:?}"), 2577 } 2578 } 2579 2580 // ── getAttribute / setAttribute tests ──────────────── 2581 2582 #[test] 2583 fn test_get_attribute() { 2584 let result = eval_with_doc( 2585 r#"<html><body><a id="link" href="https://example.com">x</a></body></html>"#, 2586 r#"document.getElementById("link").getAttribute("href")"#, 2587 ) 2588 .unwrap(); 2589 match result { 2590 Value::String(s) => assert_eq!(s, "https://example.com"), 2591 v => panic!("expected URL, got {v:?}"), 2592 } 2593 } 2594 2595 #[test] 2596 fn test_get_attribute_missing() { 2597 let result = eval_with_doc( 2598 r#"<html><body><div id="d"></div></body></html>"#, 2599 r#"document.getElementById("d").getAttribute("nope") === null"#, 2600 ) 2601 .unwrap(); 2602 assert!(matches!(result, Value::Boolean(true))); 2603 } 2604 2605 #[test] 2606 fn test_set_attribute() { 2607 let result = eval_with_doc( 2608 r#"<html><body><div id="d"></div></body></html>"#, 2609 r#" 2610 var d = document.getElementById("d"); 2611 d.setAttribute("data-x", "123"); 2612 d.getAttribute("data-x") 2613 "#, 2614 ) 2615 .unwrap(); 2616 match result { 2617 Value::String(s) => assert_eq!(s, "123"), 2618 v => panic!("expected '123', got {v:?}"), 2619 } 2620 } 2621 2622 #[test] 2623 fn test_has_attribute() { 2624 let result = eval_with_doc( 2625 r#"<html><body><div id="d" class="foo"></div></body></html>"#, 2626 r#" 2627 var d = document.getElementById("d"); 2628 d.hasAttribute("class") && !d.hasAttribute("nope") 2629 "#, 2630 ) 2631 .unwrap(); 2632 assert!(matches!(result, Value::Boolean(true))); 2633 } 2634 2635 #[test] 2636 fn test_remove_attribute() { 2637 let result = eval_with_doc( 2638 r#"<html><body><div id="d" class="foo"></div></body></html>"#, 2639 r#" 2640 var d = document.getElementById("d"); 2641 d.removeAttribute("class"); 2642 d.hasAttribute("class") 2643 "#, 2644 ) 2645 .unwrap(); 2646 assert!(matches!(result, Value::Boolean(false))); 2647 } 2648 2649 // ── Navigation property tests ──────────────────────── 2650 2651 #[test] 2652 fn test_parent_node() { 2653 let result = eval_with_doc( 2654 r#"<html><body><div id="child"></div></body></html>"#, 2655 r#"document.getElementById("child").parentNode.tagName"#, 2656 ) 2657 .unwrap(); 2658 match result { 2659 Value::String(s) => assert_eq!(s, "BODY"), 2660 v => panic!("expected 'BODY', got {v:?}"), 2661 } 2662 } 2663 2664 #[test] 2665 fn test_child_nodes() { 2666 let result = eval_with_doc( 2667 r#"<html><body><div id="p"><span>a</span><span>b</span></div></body></html>"#, 2668 r#"document.getElementById("p").childNodes.length"#, 2669 ) 2670 .unwrap(); 2671 match result { 2672 Value::Number(n) => assert_eq!(n, 2.0), 2673 v => panic!("expected 2, got {v:?}"), 2674 } 2675 } 2676 2677 #[test] 2678 fn test_children_element_only() { 2679 let result = eval_with_doc( 2680 r#"<html><body><div id="p"><span>a</span>text<span>b</span></div></body></html>"#, 2681 r#"document.getElementById("p").children.length"#, 2682 ) 2683 .unwrap(); 2684 match result { 2685 Value::Number(n) => assert_eq!(n, 2.0), 2686 v => panic!("expected 2, got {v:?}"), 2687 } 2688 } 2689 2690 #[test] 2691 fn test_first_child() { 2692 let result = eval_with_doc( 2693 r#"<html><body><div id="p"><span>a</span><em>b</em></div></body></html>"#, 2694 r#"document.getElementById("p").firstChild.tagName"#, 2695 ) 2696 .unwrap(); 2697 match result { 2698 Value::String(s) => assert_eq!(s, "SPAN"), 2699 v => panic!("expected 'SPAN', got {v:?}"), 2700 } 2701 } 2702 2703 #[test] 2704 fn test_last_child() { 2705 let result = eval_with_doc( 2706 r#"<html><body><div id="p"><span>a</span><em>b</em></div></body></html>"#, 2707 r#"document.getElementById("p").lastChild.tagName"#, 2708 ) 2709 .unwrap(); 2710 match result { 2711 Value::String(s) => assert_eq!(s, "EM"), 2712 v => panic!("expected 'EM', got {v:?}"), 2713 } 2714 } 2715 2716 #[test] 2717 fn test_next_sibling() { 2718 let result = eval_with_doc( 2719 r#"<html><body><span id="a">1</span><em id="b">2</em></body></html>"#, 2720 r#"document.getElementById("a").nextSibling.tagName"#, 2721 ) 2722 .unwrap(); 2723 match result { 2724 Value::String(s) => assert_eq!(s, "EM"), 2725 v => panic!("expected 'EM', got {v:?}"), 2726 } 2727 } 2728 2729 #[test] 2730 fn test_previous_sibling() { 2731 let result = eval_with_doc( 2732 r#"<html><body><span id="a">1</span><em id="b">2</em></body></html>"#, 2733 r#"document.getElementById("b").previousSibling.tagName"#, 2734 ) 2735 .unwrap(); 2736 match result { 2737 Value::String(s) => assert_eq!(s, "SPAN"), 2738 v => panic!("expected 'SPAN', got {v:?}"), 2739 } 2740 } 2741 2742 #[test] 2743 fn test_null_navigation() { 2744 let result = eval_with_doc( 2745 r#"<html><body><div id="only"></div></body></html>"#, 2746 r#"document.getElementById("only").nextSibling === null"#, 2747 ) 2748 .unwrap(); 2749 assert!(matches!(result, Value::Boolean(true))); 2750 } 2751 2752 // ── style property tests ───────────────────────────── 2753 2754 #[test] 2755 fn test_style_set_and_get() { 2756 let result = eval_with_doc( 2757 r#"<html><body><div id="d"></div></body></html>"#, 2758 r#" 2759 var d = document.getElementById("d"); 2760 d.style.color = "red"; 2761 d.style.color 2762 "#, 2763 ) 2764 .unwrap(); 2765 match result { 2766 Value::String(s) => assert_eq!(s, "red"), 2767 v => panic!("expected 'red', got {v:?}"), 2768 } 2769 } 2770 2771 #[test] 2772 fn test_style_read_existing() { 2773 let result = eval_with_doc( 2774 r#"<html><body><div id="d" style="color: blue"></div></body></html>"#, 2775 r#"document.getElementById("d").style.color"#, 2776 ) 2777 .unwrap(); 2778 match result { 2779 Value::String(s) => assert_eq!(s, "blue"), 2780 v => panic!("expected 'blue', got {v:?}"), 2781 } 2782 } 2783 2784 // ── classList tests ────────────────────────────────── 2785 2786 #[test] 2787 fn test_class_list_add() { 2788 let result = eval_with_doc( 2789 r#"<html><body><div id="d"></div></body></html>"#, 2790 r#" 2791 var d = document.getElementById("d"); 2792 d.classList.add("foo"); 2793 d.getAttribute("class") 2794 "#, 2795 ) 2796 .unwrap(); 2797 match result { 2798 Value::String(s) => assert_eq!(s, "foo"), 2799 v => panic!("expected 'foo', got {v:?}"), 2800 } 2801 } 2802 2803 #[test] 2804 fn test_class_list_remove() { 2805 let result = eval_with_doc( 2806 r#"<html><body><div id="d" class="foo bar"></div></body></html>"#, 2807 r#" 2808 var d = document.getElementById("d"); 2809 d.classList.remove("foo"); 2810 d.getAttribute("class") 2811 "#, 2812 ) 2813 .unwrap(); 2814 match result { 2815 Value::String(s) => assert_eq!(s, "bar"), 2816 v => panic!("expected 'bar', got {v:?}"), 2817 } 2818 } 2819 2820 #[test] 2821 fn test_class_list_toggle() { 2822 let result = eval_with_doc( 2823 r#"<html><body><div id="d" class="foo"></div></body></html>"#, 2824 r#" 2825 var d = document.getElementById("d"); 2826 var removed = d.classList.toggle("foo"); 2827 var added = d.classList.toggle("bar"); 2828 !removed && added 2829 "#, 2830 ) 2831 .unwrap(); 2832 assert!(matches!(result, Value::Boolean(true))); 2833 } 2834 2835 #[test] 2836 fn test_class_list_contains() { 2837 let result = eval_with_doc( 2838 r#"<html><body><div id="d" class="foo bar"></div></body></html>"#, 2839 r#" 2840 var d = document.getElementById("d"); 2841 d.classList.contains("foo") && !d.classList.contains("baz") 2842 "#, 2843 ) 2844 .unwrap(); 2845 assert!(matches!(result, Value::Boolean(true))); 2846 } 2847 2848 // ── setAttribute syncs id/className ────────────────── 2849 2850 #[test] 2851 fn test_set_attribute_syncs_id() { 2852 let result = eval_with_doc( 2853 r#"<html><body><div id="d"></div></body></html>"#, 2854 r#" 2855 var d = document.getElementById("d"); 2856 d.setAttribute("id", "new_id"); 2857 d.id 2858 "#, 2859 ) 2860 .unwrap(); 2861 match result { 2862 Value::String(s) => assert_eq!(s, "new_id"), 2863 v => panic!("expected 'new_id', got {v:?}"), 2864 } 2865 } 2866 2867 // ── id and className setters ───────────────────────── 2868 2869 #[test] 2870 fn test_set_id_syncs_attribute() { 2871 let result = eval_with_doc( 2872 r#"<html><body><div id="d"></div></body></html>"#, 2873 r#" 2874 var d = document.getElementById("d"); 2875 d.id = "new_id"; 2876 d.getAttribute("id") 2877 "#, 2878 ) 2879 .unwrap(); 2880 match result { 2881 Value::String(s) => assert_eq!(s, "new_id"), 2882 v => panic!("expected 'new_id', got {v:?}"), 2883 } 2884 } 2885 2886 #[test] 2887 fn test_set_class_name_syncs_attribute() { 2888 let result = eval_with_doc( 2889 r#"<html><body><div id="d"></div></body></html>"#, 2890 r#" 2891 var d = document.getElementById("d"); 2892 d.className = "a b"; 2893 d.getAttribute("class") 2894 "#, 2895 ) 2896 .unwrap(); 2897 match result { 2898 Value::String(s) => assert_eq!(s, "a b"), 2899 v => panic!("expected 'a b', got {v:?}"), 2900 } 2901 } 2902 2903 // ── Navigation after DOM modification ──────────────── 2904 2905 #[test] 2906 fn test_navigation_after_append() { 2907 let result = eval_with_doc( 2908 r#"<html><body><div id="p"></div></body></html>"#, 2909 r#" 2910 var p = document.getElementById("p"); 2911 var a = document.createElement("span"); 2912 var b = document.createElement("em"); 2913 p.appendChild(a); 2914 p.appendChild(b); 2915 p.firstChild.tagName + "," + p.lastChild.tagName 2916 "#, 2917 ) 2918 .unwrap(); 2919 match result { 2920 Value::String(s) => assert_eq!(s, "SPAN,EM"), 2921 v => panic!("expected 'SPAN,EM', got {v:?}"), 2922 } 2923 } 2924 2925 // ── Removed nodes are detached ─────────────────────── 2926 2927 #[test] 2928 fn test_removed_node_detached() { 2929 let result = eval_with_doc( 2930 r#"<html><body><div id="p"><span id="c"></span></div></body></html>"#, 2931 r#" 2932 var p = document.getElementById("p"); 2933 var c = document.getElementById("c"); 2934 p.removeChild(c); 2935 c.parentNode === null 2936 "#, 2937 ) 2938 .unwrap(); 2939 assert!(matches!(result, Value::Boolean(true))); 2940 } 2941 2942 // ── Event system tests ────────────────────────────────── 2943 2944 #[test] 2945 fn test_event_constructor() { 2946 let result = eval_with_doc( 2947 "<html><body></body></html>", 2948 r#" 2949 var e = new Event("click"); 2950 e.type 2951 "#, 2952 ) 2953 .unwrap(); 2954 match result { 2955 Value::String(s) => assert_eq!(s, "click"), 2956 v => panic!("expected 'click', got {v:?}"), 2957 } 2958 } 2959 2960 #[test] 2961 fn test_event_constructor_options() { 2962 let result = eval_with_doc( 2963 "<html><body></body></html>", 2964 r#" 2965 var e = new Event("click", { bubbles: true, cancelable: true }); 2966 e.bubbles + "," + e.cancelable 2967 "#, 2968 ) 2969 .unwrap(); 2970 match result { 2971 Value::String(s) => assert_eq!(s, "true,true"), 2972 v => panic!("expected 'true,true', got {v:?}"), 2973 } 2974 } 2975 2976 #[test] 2977 fn test_event_default_properties() { 2978 let result = eval_with_doc( 2979 "<html><body></body></html>", 2980 r#" 2981 var e = new Event("test"); 2982 e.bubbles + "," + e.cancelable + "," + e.defaultPrevented + "," + e.eventPhase 2983 "#, 2984 ) 2985 .unwrap(); 2986 match result { 2987 Value::String(s) => assert_eq!(s, "false,false,false,0"), 2988 v => panic!("expected 'false,false,false,0', got {v:?}"), 2989 } 2990 } 2991 2992 #[test] 2993 fn test_add_event_listener_and_dispatch() { 2994 let result = eval_with_doc( 2995 r#"<html><body><div id="d"></div></body></html>"#, 2996 r#" 2997 var called = false; 2998 var d = document.getElementById("d"); 2999 d.addEventListener("click", function(e) { 3000 called = true; 3001 }); 3002 d.dispatchEvent(new Event("click")); 3003 called 3004 "#, 3005 ) 3006 .unwrap(); 3007 assert!(matches!(result, Value::Boolean(true))); 3008 } 3009 3010 #[test] 3011 fn test_event_handler_receives_event_object() { 3012 let result = eval_with_doc( 3013 r#"<html><body><div id="d"></div></body></html>"#, 3014 r#" 3015 var eventType = ""; 3016 var d = document.getElementById("d"); 3017 d.addEventListener("myevent", function(e) { 3018 eventType = e.type; 3019 }); 3020 d.dispatchEvent(new Event("myevent")); 3021 eventType 3022 "#, 3023 ) 3024 .unwrap(); 3025 match result { 3026 Value::String(s) => assert_eq!(s, "myevent"), 3027 v => panic!("expected 'myevent', got {v:?}"), 3028 } 3029 } 3030 3031 #[test] 3032 fn test_event_target_set_correctly() { 3033 let result = eval_with_doc( 3034 r#"<html><body><div id="d"></div></body></html>"#, 3035 r#" 3036 var targetTag = ""; 3037 var d = document.getElementById("d"); 3038 d.addEventListener("click", function(e) { 3039 targetTag = e.target.tagName; 3040 }); 3041 d.dispatchEvent(new Event("click")); 3042 targetTag 3043 "#, 3044 ) 3045 .unwrap(); 3046 match result { 3047 Value::String(s) => assert_eq!(s, "DIV"), 3048 v => panic!("expected 'DIV', got {v:?}"), 3049 } 3050 } 3051 3052 #[test] 3053 fn test_event_bubbling() { 3054 let result = eval_with_doc( 3055 r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3056 r#" 3057 var order = ""; 3058 var parent = document.getElementById("parent"); 3059 var child = document.getElementById("child"); 3060 parent.addEventListener("click", function(e) { 3061 order = order + "parent"; 3062 }); 3063 child.addEventListener("click", function(e) { 3064 order = order + "child,"; 3065 }); 3066 child.dispatchEvent(new Event("click", { bubbles: true })); 3067 order 3068 "#, 3069 ) 3070 .unwrap(); 3071 match result { 3072 Value::String(s) => assert_eq!(s, "child,parent"), 3073 v => panic!("expected 'child,parent', got {v:?}"), 3074 } 3075 } 3076 3077 #[test] 3078 fn test_event_no_bubbling_by_default() { 3079 let result = eval_with_doc( 3080 r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3081 r#" 3082 var parentCalled = false; 3083 var parent = document.getElementById("parent"); 3084 var child = document.getElementById("child"); 3085 parent.addEventListener("test", function(e) { 3086 parentCalled = true; 3087 }); 3088 child.dispatchEvent(new Event("test")); 3089 parentCalled 3090 "#, 3091 ) 3092 .unwrap(); 3093 assert!(matches!(result, Value::Boolean(false))); 3094 } 3095 3096 #[test] 3097 fn test_event_capture_phase() { 3098 let result = eval_with_doc( 3099 r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3100 r#" 3101 var order = ""; 3102 var parent = document.getElementById("parent"); 3103 var child = document.getElementById("child"); 3104 parent.addEventListener("click", function(e) { 3105 order = order + "capture,"; 3106 }, true); 3107 parent.addEventListener("click", function(e) { 3108 order = order + "bubble"; 3109 }); 3110 child.addEventListener("click", function(e) { 3111 order = order + "target,"; 3112 }); 3113 child.dispatchEvent(new Event("click", { bubbles: true })); 3114 order 3115 "#, 3116 ) 3117 .unwrap(); 3118 match result { 3119 Value::String(s) => assert_eq!(s, "capture,target,bubble"), 3120 v => panic!("expected 'capture,target,bubble', got {v:?}"), 3121 } 3122 } 3123 3124 #[test] 3125 fn test_stop_propagation() { 3126 let result = eval_with_doc( 3127 r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3128 r#" 3129 var parentCalled = false; 3130 var parent = document.getElementById("parent"); 3131 var child = document.getElementById("child"); 3132 parent.addEventListener("click", function(e) { 3133 parentCalled = true; 3134 }); 3135 child.addEventListener("click", function(e) { 3136 e.stopPropagation(); 3137 }); 3138 child.dispatchEvent(new Event("click", { bubbles: true })); 3139 parentCalled 3140 "#, 3141 ) 3142 .unwrap(); 3143 assert!(matches!(result, Value::Boolean(false))); 3144 } 3145 3146 #[test] 3147 fn test_stop_immediate_propagation() { 3148 let result = eval_with_doc( 3149 r#"<html><body><div id="d"></div></body></html>"#, 3150 r#" 3151 var count = 0; 3152 var d = document.getElementById("d"); 3153 d.addEventListener("click", function(e) { 3154 count = count + 1; 3155 e.stopImmediatePropagation(); 3156 }); 3157 d.addEventListener("click", function(e) { 3158 count = count + 1; 3159 }); 3160 d.dispatchEvent(new Event("click")); 3161 count 3162 "#, 3163 ) 3164 .unwrap(); 3165 match result { 3166 Value::Number(n) => assert_eq!(n, 1.0), 3167 v => panic!("expected 1, got {v:?}"), 3168 } 3169 } 3170 3171 #[test] 3172 fn test_prevent_default() { 3173 let result = eval_with_doc( 3174 r#"<html><body><div id="d"></div></body></html>"#, 3175 r#" 3176 var d = document.getElementById("d"); 3177 d.addEventListener("click", function(e) { 3178 e.preventDefault(); 3179 }); 3180 var result = d.dispatchEvent(new Event("click", { cancelable: true })); 3181 result 3182 "#, 3183 ) 3184 .unwrap(); 3185 // dispatchEvent returns false when preventDefault was called. 3186 assert!(matches!(result, Value::Boolean(false))); 3187 } 3188 3189 #[test] 3190 fn test_prevent_default_not_cancelable() { 3191 let result = eval_with_doc( 3192 r#"<html><body><div id="d"></div></body></html>"#, 3193 r#" 3194 var d = document.getElementById("d"); 3195 d.addEventListener("click", function(e) { 3196 e.preventDefault(); 3197 }); 3198 var result = d.dispatchEvent(new Event("click")); 3199 result 3200 "#, 3201 ) 3202 .unwrap(); 3203 // Non-cancelable: preventDefault has no effect, returns true. 3204 assert!(matches!(result, Value::Boolean(true))); 3205 } 3206 3207 #[test] 3208 fn test_remove_event_listener() { 3209 let result = eval_with_doc( 3210 r#"<html><body><div id="d"></div></body></html>"#, 3211 r#" 3212 var count = 0; 3213 var d = document.getElementById("d"); 3214 var handler = function(e) { count = count + 1; }; 3215 d.addEventListener("click", handler); 3216 d.dispatchEvent(new Event("click")); 3217 d.removeEventListener("click", handler); 3218 d.dispatchEvent(new Event("click")); 3219 count 3220 "#, 3221 ) 3222 .unwrap(); 3223 match result { 3224 Value::Number(n) => assert_eq!(n, 1.0), 3225 v => panic!("expected 1, got {v:?}"), 3226 } 3227 } 3228 3229 #[test] 3230 fn test_once_option() { 3231 let result = eval_with_doc( 3232 r#"<html><body><div id="d"></div></body></html>"#, 3233 r#" 3234 var count = 0; 3235 var d = document.getElementById("d"); 3236 d.addEventListener("click", function(e) { 3237 count = count + 1; 3238 }, { once: true }); 3239 d.dispatchEvent(new Event("click")); 3240 d.dispatchEvent(new Event("click")); 3241 count 3242 "#, 3243 ) 3244 .unwrap(); 3245 match result { 3246 Value::Number(n) => assert_eq!(n, 1.0), 3247 v => panic!("expected 1, got {v:?}"), 3248 } 3249 } 3250 3251 #[test] 3252 fn test_dispatch_event_returns_true_normally() { 3253 let result = eval_with_doc( 3254 r#"<html><body><div id="d"></div></body></html>"#, 3255 r#" 3256 var d = document.getElementById("d"); 3257 d.dispatchEvent(new Event("click")) 3258 "#, 3259 ) 3260 .unwrap(); 3261 assert!(matches!(result, Value::Boolean(true))); 3262 } 3263 3264 #[test] 3265 fn test_event_current_target() { 3266 let result = eval_with_doc( 3267 r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3268 r#" 3269 var currentTag = ""; 3270 var parent = document.getElementById("parent"); 3271 var child = document.getElementById("child"); 3272 parent.addEventListener("click", function(e) { 3273 currentTag = e.currentTarget.tagName; 3274 }); 3275 child.dispatchEvent(new Event("click", { bubbles: true })); 3276 currentTag 3277 "#, 3278 ) 3279 .unwrap(); 3280 match result { 3281 Value::String(s) => assert_eq!(s, "DIV"), 3282 v => panic!("expected 'DIV', got {v:?}"), 3283 } 3284 } 3285 3286 #[test] 3287 fn test_multiple_listeners_same_type() { 3288 let result = eval_with_doc( 3289 r#"<html><body><div id="d"></div></body></html>"#, 3290 r#" 3291 var order = ""; 3292 var d = document.getElementById("d"); 3293 d.addEventListener("click", function(e) { order = order + "a,"; }); 3294 d.addEventListener("click", function(e) { order = order + "b"; }); 3295 d.dispatchEvent(new Event("click")); 3296 order 3297 "#, 3298 ) 3299 .unwrap(); 3300 match result { 3301 Value::String(s) => assert_eq!(s, "a,b"), 3302 v => panic!("expected 'a,b', got {v:?}"), 3303 } 3304 } 3305 3306 #[test] 3307 fn test_capture_option_object() { 3308 let result = eval_with_doc( 3309 r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3310 r#" 3311 var capturePhase = false; 3312 var parent = document.getElementById("parent"); 3313 var child = document.getElementById("child"); 3314 parent.addEventListener("click", function(e) { 3315 capturePhase = (e.eventPhase === 1); 3316 }, { capture: true }); 3317 child.dispatchEvent(new Event("click", { bubbles: true })); 3318 capturePhase 3319 "#, 3320 ) 3321 .unwrap(); 3322 assert!(matches!(result, Value::Boolean(true))); 3323 } 3324 3325 #[test] 3326 fn test_document_add_event_listener() { 3327 let result = eval_with_doc( 3328 r#"<html><body><div id="d"></div></body></html>"#, 3329 r#" 3330 var docCalled = false; 3331 document.addEventListener("click", function(e) { 3332 docCalled = true; 3333 }); 3334 var d = document.getElementById("d"); 3335 d.dispatchEvent(new Event("click", { bubbles: true })); 3336 docCalled 3337 "#, 3338 ) 3339 .unwrap(); 3340 assert!(matches!(result, Value::Boolean(true))); 3341 } 3342}