WPT conformance: dom/ranges, FileAPI, detached documents (#236)

* dom: complete detached documents; add ProgressEvent and parseHTMLUnsafe

createHTMLDocument returned a document whose body was null: parsing the
skeleton string as innerHTML of <html> collapsed through the fragment parser
and kept only <title>. Build head>title and body explicitly instead. The
DOMParser doc node also gains appendChild/removeChild/insertBefore and a
doctype getter, and createDocument carries its doctype argument.

This lets dom/common.js setupRangeTests() finish (it builds fixtures with
new Document(), createHTMLDocument(), createDocument()) instead of aborting at
harness setup, so the dom/ranges files run. Range-collapse 186/186,
Range-attributes 1/1, Range-cloneRange 55/62, StaticRange-constructor 14/17.
The large O(ranges x points) matrix files (Range-set, comparePoint,
compareBoundaryPoints) still time out: the boundary-point comparison walks the
tree through per-step DOM ops, which is too slow for those matrices. Speeding
that up (a Rust node-position op) is a separate change.

Also add globalThis.ProgressEvent (Event subclass with lengthComputable/
loaded/total) and the static Document.parseHTMLUnsafe(html).

* perf(dom): compute Range tree-order in Rust ops

The Range boundary-point algorithms compared nodes by walking ancestor chains
and sibling lists from JS, one DOM op per step. The dom/ranges matrices
(testRanges x testPoints, thousands of comparisons) made that time out.

Add node_index, compare_order, and node_root ops that do the walk in Rust over
the DOM tree (slab lookups, no per-step FFI), and use them in _rngNodeIndex,
_rngOrder, and _rngRoot. Range-comparePoint 3534/5580, Range-isPointInRange
5197/5733, Range-compareBoundaryPoints 6441/9313, all completing in under 10s
where they previously did not finish. Not on any engine hot path (querySelector,
mutation, navigation, fetch do not use these).

* fileapi: real byte-backed Blob/File, FileReader state machine, Request.blob

Blob was a one-line stub that string-joined its parts (wrong size for binary
or multibyte input) and exposed only text(). Replace it with a Uint8Array-backed
Blob: normalizes string/ArrayBuffer/typed-array/Blob parts, correct size,
slice(), text()/arrayBuffer()/bytes(), type normalization (lowercased, cleared
when non-ASCII), the endings option (native CRLF normalization), and
Symbol.toStringTag. File adds name/lastModified, the 2-argument requirement, and
its own toStringTag.

FileReader becomes a real state machine: EMPTY/LOADING/DONE, throws
InvalidStateError when a read starts while already LOADING, and fires
loadstart/progress/load/loadend (and abort) to both on* handlers and
addEventListener. readAsText/DataURL/ArrayBuffer/BinaryString produce correct
results. Request gains blob().

FileReader-multiple-reads 6/6, File-constructor-endings 10/11, Blob-slice
141/150.
This commit is contained in:
SGavrl authored and GitHub committed 2026-06-03 21:20:42 +00:00
1 parent 387c74f26a
commit 9c3dd6b21a
2 files changed
+228 -30

No files matched your search

+74
View File
@@ -337,10 +337,84 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri
let other = arg2.parse::<u32>().unwrap_or(0);
dom.descendants(NodeId::new(nid)).contains(&NodeId::new(other)).to_string()
}
// Index of a node among its parent's children. Walks prev siblings in
// Rust, avoiding the per-step JS->op round trips a Range comparison
// would otherwise make.
"node_index" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
node_child_index(dom, NodeId::new(nid)).to_string()
}
// Document (preorder) tree order of two nodes: -1 if a precedes b, 1 if
// a follows b, 0 if equal. Used by the Range boundary-point algorithms.
"compare_order" => {
let a = NodeId::new(arg1.parse::<u32>().unwrap_or(0));
let b = NodeId::new(arg2.parse::<u32>().unwrap_or(0));
compare_node_order(dom, a, b).to_string()
}
// Root (topmost ancestor) of a node, in one op rather than an O(depth)
// walk of parentNode ops from JS.
"node_root" => {
let mut cur = NodeId::new(arg1.parse::<u32>().unwrap_or(0));
while let Some(p) = dom.get_node(cur).and_then(|x| x.parent) {
cur = p;
}
cur.index().to_string()
}
_ => "null".into(),
}
}
/// Index of `n` among its parent's children (0-based).
fn node_child_index(dom: &DomTree, n: NodeId) -> usize {
let mut i = 0usize;
let mut cur = dom.get_node(n).and_then(|x| x.prev_sibling);
while let Some(p) = cur {
i += 1;
cur = dom.get_node(p).and_then(|x| x.prev_sibling);
}
i
}
/// Ancestor chain of `n` from the root down to `n` (root first).
fn node_ancestors_root_first(dom: &DomTree, n: NodeId) -> Vec<NodeId> {
let mut v = vec![n];
let mut cur = n;
while let Some(p) = dom.get_node(cur).and_then(|x| x.parent) {
v.push(p);
cur = p;
}
v.reverse();
v
}
/// Preorder (document) order comparison of two nodes: -1 before, 1 after, 0 same.
fn compare_node_order(dom: &DomTree, a: NodeId, b: NodeId) -> i32 {
if a == b {
return 0;
}
let aa = node_ancestors_root_first(dom, a);
let bb = node_ancestors_root_first(dom, b);
// Different roots: order is undefined per spec; keep it stable by node id.
if aa[0] != bb[0] {
return if a.index() < b.index() { -1 } else { 1 };
}
let mut i = 0usize;
while i < aa.len() && i < bb.len() && aa[i] == bb[i] {
i += 1;
}
if i >= aa.len() {
return -1; // a is an ancestor of b -> a precedes
}
if i >= bb.len() {
return 1; // b is an ancestor of a -> a follows
}
if node_child_index(dom, aa[i]) < node_child_index(dom, bb[i]) {
-1
} else {
1
}
}
#[op2(fast)]
fn op_console_msg(state: &OpState, #[string] level: &str, #[string] msg: &str) {
let _ = state;