WPT conformance + perf: O(1) querySelector('#id'), HTML attribute folding, compareDocumentPosition, URLSearchParams, DOMParser (#238)

* 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.

* encoding: TextDecoder supports legacy charsets via encoding_rs

TextDecoder was a UTF-8-only JS shim that ignored its label, so
new TextDecoder('shift_jis'|'euc-jp'|'big5'|'gbk'|'windows-1252'|'iso-8859-1')
.decode() returned garbage. That matters for scraping: pages and scripts decode
legacy-encoded bytes (XHR/fetch responses, File/Blob text) all the time.

Back it with encoding_rs (already used by the net layer for response bodies):
obscura-net gains label_name() and decode_with_label(), exposed to JS as
op_encoding_for_label and op_text_decode. The constructor validates the label
(RangeError) and sets the canonical .encoding/.fatal/.ignoreBOM; decode()
supports fatal mode (TypeError on invalid input) and BOM handling.

Plain UTF-8 non-fatal decoding keeps a pure-JS fast path (no op, no JSON), so
Response.text()/Blob.text() on large bodies are unchanged; the op only runs for
legacy encodings or fatal mode.

textdecoder-labels 222/222, textdecoder-fatal 36/36, byte-order-marks 3/3,
utf16-surrogates 10/10, single-byte-decoder 168/504.

* selection: real Selection over Range (single instance per document)

getSelection() returned a fresh plain object each call, so it was not a Selection
instance, had no identity (window.getSelection() !== document.getSelection()),
and was missing removeRange/empty/collapseToStart/collapseToEnd/setPosition/
setBaseAndExtent/modify while collapse/extend/getRangeAt were broken stubs. That
was ~4862 failing 'selection[method] is not a function' subtests.

Replace it with a real Selection class built on the live Range: one cached
instance per document (window.getSelection() === document.getSelection(), null
for documents with no defaultView), an internal range + direction, derived
anchor/focus/isCollapsed/type/rangeCount, and the full method set
(getRangeAt with IndexSizeError, addRange, removeRange, removeAllRanges/empty,
collapse/setPosition, collapseToStart/End, extend, setBaseAndExtent,
selectAllChildren, containsNode, toString) with the spec DOMException names.
modify() stays a no-op (needs visual layout). No ops.rs changes; not on any hot
path.

setBaseAndExtent 120/120, removeAllRanges 104/104, collapseToStartEnd 51/51,
type 26/26, extend-exception 1/1.

* forms: HTMLInputElement valueAsNumber/valueAsDate/stepUp/stepDown

These were absent. Implement the number/date conversion API for the applicable
types (number, range, date, month, week, time, datetime-local): parsers and
formatters per type (Date.UTC for date/datetime-local, ISO-week Monday math for
week, months-since-epoch for month), valueAsNumber get/set, valueAsDate get/set,
and stepUp/stepDown(n) with the step scale/default per type, step-base snapping,
and min/max clamping. Non-applicable types and step="any" throw InvalidStateError;
non-finite valueAsNumber throws TypeError; range with empty value reads as the
midpoint (50 for 0..100). Plus min/max/step attribute reflection.

All on Element accessors gated by input type, only evaluated on explicit access;
not on any hot path.

input-stepup 51/53, input-valueasnumber 60/64, input-valueasdate 27/30.

* perf(encoding): skip label op for no-arg TextDecoder (Response/Blob text path)

new TextDecoder() with no argument (what Response.text()/Blob.text() and most
pages use) is UTF-8; resolve that without the op_encoding_for_label round trip.
Explicit labels still validate via the op.

* core: HTMLDialogElement, Node.normalize, Range.createContextualFragment

HTMLDialogElement was a bare alias of Element, so ~40 Core files crashed at the
first dialog.show()/showModal(). Add the dialog members on Element.prototype
(matching the popover/input convention): open reflection, returnValue (internal
slot, String coercion), oncancel/onclose defaulting to null, show()/showModal()
/close()/requestClose() with the open-state transitions, InvalidStateError
throwing (show-on-modal, showModal-on-open, showModal-disconnected), the async
non-bubbling close event, the synchronous cancelable cancel event (no re-entrant
cancel), beforetoggle/toggle, and closedBy. matches(':modal') reports a modally
shown dialog (like :popover-open). Modal top-layer/focus/render stays unhandled
(layout).

Node.normalize() now merges adjacent Text nodes and drops empty ones (was a
no-op). Range.createContextualFragment(html) parses a fragment.

createContextualFragment 34/35, dialog-open 3/3, dialog-return-value 1/1,
dialog-showModal 5/10, dialog-requestclose 22/75. Not on hot paths.

* custom-elements: CustomElementRegistry class, getName, ElementInternals, HTMLUnknownElement

customElements was a bare object literal, so 'CustomElementRegistry is not
defined' crashed ~22 Core files at top level (idlharness/feature-detect), and
HTMLUnknownElement/ElementInternals/attachInternals/getName were missing.

Promote the registry to a real CustomElementRegistry class (window.customElements
is now an instance, so 'define in CustomElementRegistry.prototype' works and the
class is exposed). Add spec define() validation/throw order (TypeError for a
non-constructor, SyntaxError for an invalid name, NotSupportedError for a
duplicate name/constructor or reentrant define), getName(), and a SyntaxError
reject in whenDefined for invalid names. The existing define -> upgrade ->
connectedCallback path (used by lit/Polymer on real pages) is preserved.

Add HTMLUnknownElement, ElementInternals (setFormValue/setValidity/checkValidity/
validity/validationMessage/form/states), and Element.attachInternals() with its
NotSupportedError cases.

Does NOT include the [[HTMLConstructor]] (new.target) construction rework or the
reaction wiring into appendChild/setAttribute (hot paths) - deferred to avoid
element-model and perf risk. getName 4/4.

* shadow-dom: ShadowRoot.adoptedStyleSheets (mirror Document)

ShadowRoot lacked adoptedStyleSheets; constructible-stylesheet adoption into a
shadow root threw. Add the getter/setter on ShadowRoot.prototype, matching
Document.adoptedStyleSheets.

* conformance: charset/URL encoding override, IDL reflection, Range and Storage fixes

Encoding: track the detected document character encoding and expose it as
document.characterSet/charset/inputEncoding instead of hardcoding UTF-8. Add the
WHATWG URL encoding override for <a>/<area> hrefs, so a legacy-charset document
percent-encodes the query in its own encoding (EUC-JP U+8108 becomes %CC%AE).
ASCII delimiters stay literal so structured queries are unaffected, and
unmappable code points become %26%23NNN%3B. UTF-8 documents take an early-return
so the href getter hot path is unchanged. fetch()/XHR text() now decode using
the response Content-Type charset instead of always UTF-8. Adds op_url_encode_query
and a document_encoding op; encoding state is plumbed through Page and the runtime.

Reflection: install the reflected IDL attribute layer once on Element.prototype
(no per-element defineProperty, so element creation and query paths are
untouched): global attributes (title, lang, dir, accessKey, slot, autofocus,
hidden, tabIndex) with correct type coercion and enum canonicalization, plus the
ARIAMixin (aria-* reflected as the ariaXxx nullable DOMString properties).

Range: compareBoundaryPoints ToUint16-converts the how argument (NaN/Infinity
no longer false-throw) and raises a real WrongDocumentError DOMException for
ranges that do not share a root.

Storage: reimplement localStorage/sessionStorage as a WHATWG Proxy so property
access (localStorage.foo = x, delete, in, Object.keys) stays in sync with
length, key() and iteration.

* conformance: HTML attribute folding, compareDocumentPosition, URLSearchParams, DOMParser, fetch UA

Next WPT Core batch, all verified live and benchmarked for no hot-path regression.

DOM:
- compareDocumentPosition rewritten to spec: real PRECEDING/FOLLOWING via the
  compare_order op, CONTAINS/CONTAINED_BY, and DISCONNECTED|IMPLEMENTATION_SPECIFIC
  for different roots. Adds the DOCUMENT_POSITION_* and missing nodeType constants
  on Node and its prototype. WPT getPosition() depends on this, so it cascades into
  the Range comparePoint/compareBoundaryPoints/isPointInRange expectations.
- HTML elements now ASCII-lowercase attribute names in getAttribute/setAttribute/
  removeAttribute/hasAttribute, so el.setAttribute('accessKey') reflects via
  getAttribute('accesskey'). The fold is gated behind a cheap uppercase scan and a
  namespaceURI HTML guard (SVG/foreign attributes keep their case), and getAttribute
  only folds on a miss, so the all-lowercase hot path is unchanged (benchmarked flat).
- Range comparePoint/isPointInRange ToUint32-coerce the offset (negative throws
  IndexSizeError); createTreeWalker/createNodeIterator keep an explicit whatToShow of 0.

URL:
- URLSearchParams uses a proper form-urlencoded byte decoder (partial/invalid escapes
  stay literal instead of decodeURIComponent throwing on the whole string), 2-arg
  has(name,value)/delete(name,value), and a sequence-pair TypeError. Adds URL.parse
  and URL.canParse statics.

Other:
- fetch()/XHR send a default Chrome User-Agent (the navigation path set one but
  scripted requests went out with none, so UA-gated servers rejected them); honors an
  explicit override.
- DOMParser documents expose URL/compatMode/characterSet/readyState/styleSheets/
  defaultView and createTreeWalker/createNodeIterator; XMLDocument is defined globally.

* perf(dom): O(1) querySelector('#id') via the id index

querySelector('#id') went through the full selector engine, scanning descendants
in tree order until the first match (O(n), ~68us per call over a 2000-element
document in the micro-bench). A bare ASCII id selector now resolves through the
existing id index in O(1): ~68us -> ~1.25us per call, about 55x faster, with no
change to any other path (create/getAttribute/setAttribute/querySelectorAll/href
all flat). querySelector by id is one of the hottest scraping operations.

The fast path is conservative: only a bare `#name` selector with an ASCII
letter/underscore-led identifier takes it; escapes, combinators, commas,
whitespace, compounds, and non-ASCII fall through to the full engine unchanged.
For a scoped element.querySelector it returns the indexed element only when it is
a strict descendant of the scope, and falls back to the scan otherwise, so
duplicate-id scoped queries stay correct.

Also makes the id index keep the FIRST element in tree order per id (or_insert at
parse time) instead of the last, so getElementById and querySelector('#id') return
the first-in-tree-order element on duplicate ids, per spec (previously last).
This commit is contained in:
SGavrl authored and GitHub committed 2026-06-04 17:28:42 +00:00
1 parent 3b2c6c0deb
commit a703b7ac30
4 files changed
+160 -15

No files matched your search

+31
View File
@@ -531,6 +531,22 @@ pub fn parse_selector(selector: &str) -> Result<SelectorList<ObscuraSelector>, S
Ok(parsed) Ok(parsed)
} }
/// If `selector` is a bare ASCII id selector like `#main`, return the id (without
/// the `#`). Conservative: escapes, combinators, commas, whitespace, non-ASCII, or
/// a non-letter/underscore first character fall through to the full selector engine.
fn simple_id_selector(selector: &str) -> Option<&str> {
let id = selector.trim().strip_prefix('#')?;
let first = id.as_bytes().first().copied()?;
if !(first.is_ascii_alphabetic() || first == b'_') {
return None;
}
if id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') {
Some(id)
} else {
None
}
}
impl DomTree { impl DomTree {
pub fn query_selector(&self, selector: &str) -> Result<Option<NodeId>, String> { pub fn query_selector(&self, selector: &str) -> Result<Option<NodeId>, String> {
self.query_selector_from(self.document(), selector) self.query_selector_from(self.document(), selector)
@@ -541,6 +557,21 @@ impl DomTree {
} }
pub fn query_selector_from(&self, root: NodeId, selector: &str) -> Result<Option<NodeId>, String> { pub fn query_selector_from(&self, root: NodeId, selector: &str) -> Result<Option<NodeId>, String> {
// Fast path: a bare "#id" selector resolves through the id index in O(1)
// instead of scanning every descendant. The index holds the first element
// in tree order per id, which is exactly what the full scan would return.
if let Some(id) = simple_id_selector(selector) {
match self.get_element_by_id(id) {
// querySelector matches strict descendants of root only, so the
// indexed element must have root among its ancestors.
Some(nid) if self.ancestors(nid).contains(&root) => return Ok(Some(nid)),
// No element has this id at all: a bare id selector cannot match.
None => return Ok(None),
// Indexed (first) element is not under a scoped root; a later
// duplicate could still be a descendant, so fall through to scan.
Some(_) => {}
}
}
let selector_list = parse_selector(selector)?; let selector_list = parse_selector(selector)?;
let mut caches = selectors::context::SelectorCaches::default(); let mut caches = selectors::context::SelectorCaches::default();
let mut context = MatchingContext::new( let mut context = MatchingContext::new(
+4 -1
View File
@@ -186,7 +186,10 @@ impl DomTree {
if let NodeData::Element { ref attrs, .. } = data { if let NodeData::Element { ref attrs, .. } = data {
if let Some(id_attr) = attrs.iter().find(|a| a.name.local.as_ref() == "id") { if let Some(id_attr) = attrs.iter().find(|a| a.name.local.as_ref() == "id") {
inner.id_index.insert(id_attr.value.clone(), id); // Keep the FIRST element created with a given id. Parse order is
// document order, so getElementById / querySelector('#id') return
// the first-in-tree-order element on duplicate ids, per spec.
inner.id_index.entry(id_attr.value.clone()).or_insert(id);
} }
} }
+115 -14
View File
@@ -261,10 +261,23 @@ const _styleProxy = (decl) => new Proxy(decl, {
class Node { class Node {
static ELEMENT_NODE = 1; static ELEMENT_NODE = 1;
static ATTRIBUTE_NODE = 2;
static TEXT_NODE = 3; static TEXT_NODE = 3;
static CDATA_SECTION_NODE = 4;
static ENTITY_REFERENCE_NODE = 5;
static ENTITY_NODE = 6;
static PROCESSING_INSTRUCTION_NODE = 7;
static COMMENT_NODE = 8; static COMMENT_NODE = 8;
static DOCUMENT_NODE = 9; static DOCUMENT_NODE = 9;
static DOCUMENT_TYPE_NODE = 10;
static DOCUMENT_FRAGMENT_NODE = 11; static DOCUMENT_FRAGMENT_NODE = 11;
static NOTATION_NODE = 12;
static DOCUMENT_POSITION_DISCONNECTED = 1;
static DOCUMENT_POSITION_PRECEDING = 2;
static DOCUMENT_POSITION_FOLLOWING = 4;
static DOCUMENT_POSITION_CONTAINS = 8;
static DOCUMENT_POSITION_CONTAINED_BY = 16;
static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 32;
constructor(nid) { this._nid = nid; } constructor(nid) { this._nid = nid; }
get nodeType() { return +_dom("node_type", this._nid); } get nodeType() { return +_dom("node_type", this._nid); }
@@ -417,9 +430,16 @@ class Node {
compareDocumentPosition(other) { compareDocumentPosition(other) {
if (!other) return 0; if (!other) return 0;
if (this._nid === other._nid) return 0; if (this._nid === other._nid) return 0;
if (this.contains(other)) return 16 | 4; // DOCUMENT_POSITION_CONTAINED_BY | FOLLOWING // Different roots: DISCONNECTED | IMPLEMENTATION_SPECIFIC plus a stable
if (other.contains && other.contains(this)) return 8 | 2; // DOCUMENT_POSITION_CONTAINS | PRECEDING // (consistent across calls) PRECEDING/FOLLOWING bit, chosen by node-id order.
return 4; // DOCUMENT_POSITION_FOLLOWING if (+_dom("node_root", this._nid) !== +_dom("node_root", other._nid)) {
return 1 | 32 | ((this._nid < other._nid) ? 4 : 2);
}
if (this.contains(other)) return 16 | 4; // CONTAINED_BY | FOLLOWING
if (other.contains && other.contains(this)) return 8 | 2; // CONTAINS | PRECEDING
// Same root, neither contains the other: real tree order (compare_order op:
// -1 => this precedes other => other FOLLOWS this(4); +1 => this PRECEDING(2)).
return (+_dom("compare_order", this._nid, other._nid) < 0) ? 4 : 2;
} }
getRootNode() { return globalThis.document; } getRootNode() { return globalThis.document; }
normalize() { normalize() {
@@ -745,6 +765,33 @@ function _inputFormatNumber(type, n) {
return String(n); return String(n);
} }
// WebIDL interface constants live on both the interface object and the interface
// prototype object (instances inherit; idlharness checks Node.prototype).
Object.assign(Node.prototype, {
ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11,
NOTATION_NODE: 12, DOCUMENT_POSITION_DISCONNECTED: 1, DOCUMENT_POSITION_PRECEDING: 2,
DOCUMENT_POSITION_FOLLOWING: 4, DOCUMENT_POSITION_CONTAINS: 8,
DOCUMENT_POSITION_CONTAINED_BY: 16, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32,
});
// HTML elements ASCII-lowercase attribute names (setAttribute('accessKey') is
// stored as 'accesskey'). The toLowerCase is gated behind a cheap uppercase
// charCode scan so the all-lowercase common case (href, class, id, data-*)
// allocates nothing and never consults the namespace; only when an uppercase
// ASCII letter is present do we check the element is HTML before folding.
function _htmlAttrName(el, n) {
n = typeof n === "string" ? n : String(n);
for (let i = 0; i < n.length; i++) {
const c = n.charCodeAt(i);
if (c >= 65 && c <= 90) {
return el.namespaceURI === "http://www.w3.org/1999/xhtml" ? n.toLowerCase() : n;
}
}
return n;
}
class Element extends Node { class Element extends Node {
constructor(nid) { constructor(nid) {
super(nid); super(nid);
@@ -865,16 +912,24 @@ class Element extends Node {
} }
get style() { return this._style; } get style() { return this._style; }
set style(v) { if (typeof v === "string") this._style.cssText = v; } set style(v) { if (typeof v === "string") this._style.cssText = v; }
getAttribute(n) { return _domParse("get_attribute", this._nid, n); } getAttribute(n) {
// Fast path: HTML attributes are stored lowercase, so a direct hit needs no
// case folding. Only on a miss do we lowercase (gated) and retry, so the hot
// case (reading an existing lowercase attribute) pays zero scan.
let v = _domParse("get_attribute", this._nid, n);
if (v === null) { const ln = _htmlAttrName(this, n); if (ln !== n) v = _domParse("get_attribute", this._nid, ln); }
return v;
}
setAttribute(n, v) { setAttribute(n, v) {
const popoverPrev = (n === "popover" || (typeof n === "string" && n.length === 7 && n.toLowerCase() === "popover")) ? this.popover : undefined; n = _htmlAttrName(this, n);
const popoverPrev = (n === "popover") ? this.popover : undefined;
_dom("set_attribute", this._nid, n + "\0" + String(v)); _dom("set_attribute", this._nid, n + "\0" + String(v));
if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev);
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n); if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n);
} }
setAttributeNS(ns, n, v) { this.setAttribute(n, v); } // Simplified NS handling setAttributeNS(ns, n, v) { _dom("set_attribute", this._nid, String(n) + "\0" + String(v)); } // exact name, no HTML folding
removeAttribute(n) { const popoverPrev = (n === "popover" || (typeof n === "string" && n.length === 7 && n.toLowerCase() === "popover")) ? this.popover : undefined; _dom("remove_attribute", this._nid, n); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); } removeAttribute(n) { n = _htmlAttrName(this, n); const popoverPrev = (n === "popover") ? this.popover : undefined; _dom("remove_attribute", this._nid, n); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); }
removeAttributeNS(ns, n) { this.removeAttribute(n); } removeAttributeNS(ns, n) { _dom("remove_attribute", this._nid, String(n)); }
hasAttribute(n) { return this.getAttribute(n) !== null; } hasAttribute(n) { return this.getAttribute(n) !== null; }
hasAttributes() { return true; } // Simplified hasAttributes() { return true; } // Simplified
get attributes() { get attributes() {
@@ -902,7 +957,7 @@ class Element extends Node {
} }
return list; return list;
} }
getAttributeNS(ns, n) { return this.getAttribute(n); } getAttributeNS(ns, n) { return _domParse("get_attribute", this._nid, String(n)); }
querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); } querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); }
querySelectorAll(s) { querySelectorAll(s) {
const ids = _domParse("query_selector_all_scoped", this._nid, s) || []; const ids = _domParse("query_selector_all_scoped", this._nid, s) || [];
@@ -1893,7 +1948,9 @@ class Document extends Node {
return !event.defaultPrevented; return !event.defaultPrevented;
} }
createTreeWalker(root, whatToShow, filter) { createTreeWalker(root, whatToShow, filter) {
whatToShow = whatToShow || 0xFFFFFFFF; // NodeFilter.SHOW_ALL // whatToShow is unsigned long; default SHOW_ALL only when the arg is omitted.
// An explicit 0 (show nothing) must stay 0, not become SHOW_ALL.
whatToShow = (whatToShow === undefined) ? 0xFFFFFFFF : (whatToShow >>> 0);
const walker = { const walker = {
root: root, root: root,
currentNode: root, currentNode: root,
@@ -2793,6 +2850,9 @@ if (typeof URL === 'undefined' || !URL.prototype || !URL.__obscura) {
toJSON() { return this._c.href; } toJSON() { return this._c.href; }
static createObjectURL() { return 'blob:null/fake-' + Math.random().toString(36).slice(2); } static createObjectURL() { return 'blob:null/fake-' + Math.random().toString(36).slice(2); }
static revokeObjectURL() {} static revokeObjectURL() {}
// WHATWG URL.parse: like the constructor but returns null instead of throwing.
static parse(url, base) { const c = _urlParseOp(url, base); if (!c) return null; const u = Object.create(_URL.prototype); u._c = c; u._sp = null; return u; }
static canParse(url, base) { return _urlParseOp(url, base) !== null; }
}; };
_URL.__obscura = true; _URL.__obscura = true;
globalThis.URL = _URL; globalThis.URL = _URL;
@@ -3717,6 +3777,7 @@ if (typeof FormData === "undefined") globalThis.FormData = class FormData { cons
function _formEncode(s){ function _formEncode(s){
return encodeURIComponent(String(s)).replace(/%20/g,'+').replace(/[!'()~]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); return encodeURIComponent(String(s)).replace(/%20/g,'+').replace(/[!'()~]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
} }
function _hexv(c){ if(c>=48&&c<=57)return c-48; if(c>=65&&c<=70)return c-55; if(c>=97&&c<=102)return c-87; return -1; }
if (typeof URLSearchParams === "undefined") globalThis.URLSearchParams = class URLSearchParams { if (typeof URLSearchParams === "undefined") globalThis.URLSearchParams = class URLSearchParams {
constructor(init=""){ constructor(init=""){
this._p=[]; this._p=[];
@@ -3726,12 +3787,32 @@ if (typeof URLSearchParams === "undefined") globalThis.URLSearchParams = class U
} else if(typeof init==="string"){ } else if(typeof init==="string"){
this._parseString(init); this._parseString(init);
} else if (init && typeof init[Symbol.iterator] === 'function') { } else if (init && typeof init[Symbol.iterator] === 'function') {
for (const pair of init) { const a = Array.from(pair); if (a.length >= 2) this._p.push([String(a[0]), String(a[1])]); } for (const pair of init) {
const a = Array.from(pair);
if (a.length !== 2) throw new TypeError("Failed to construct 'URLSearchParams': Each query pair must be an iterable [name, value] tuple");
this._p.push([String(a[0]), String(a[1])]);
}
} else if (init && typeof init === 'object') { } else if (init && typeof init === 'object') {
Object.keys(init).forEach(k => this._p.push([String(k), String(init[k])])); Object.keys(init).forEach(k => this._p.push([String(k), String(init[k])]));
} }
} }
_decode(s){ try { return decodeURIComponent(String(s).replace(/\+/g, ' ')); } catch(e) { return String(s); } } _decode(s){
// application/x-www-form-urlencoded percent-decoding: decode each valid %XX
// byte, leave invalid escapes literal (decodeURIComponent throws on the whole
// string instead), '+' -> space, then UTF-8 decode the resulting bytes.
s = String(s);
const out = [];
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c === 0x2B) { out.push(0x20); }
else if (c === 0x25 && i + 2 < s.length) {
const a = _hexv(s.charCodeAt(i + 1)), b = _hexv(s.charCodeAt(i + 2));
if (a >= 0 && b >= 0) { out.push(a * 16 + b); i += 2; } else { out.push(c); }
} else if (c < 0x80) { out.push(c); }
else { const e = new TextEncoder().encode(s[i]); for (let j = 0; j < e.length; j++) out.push(e[j]); }
}
try { return new TextDecoder().decode(new Uint8Array(out)); } catch (e) { return s; }
}
_parseString(s){ _parseString(s){
s = String(s).replace(/^\?/, ""); s = String(s).replace(/^\?/, "");
if (s === "") return; if (s === "") return;
@@ -3749,8 +3830,8 @@ if (typeof URLSearchParams === "undefined") globalThis.URLSearchParams = class U
get(k){k=String(k); const p=this._p.find(([key])=>key===k); return p?p[1]:null;} get(k){k=String(k); const p=this._p.find(([key])=>key===k); return p?p[1]:null;}
getAll(k){k=String(k); return this._p.filter(([key])=>key===k).map(pair=>pair[1]);} getAll(k){k=String(k); return this._p.filter(([key])=>key===k).map(pair=>pair[1]);}
set(k,v){k=String(k); v=String(v); let done=false; const out=[]; for (const pair of this._p){ if(pair[0]===k){ if(!done){ out.push([k,v]); done=true; } } else out.push(pair); } if(!done) out.push([k,v]); this._p=out; this._notify(); } set(k,v){k=String(k); v=String(v); let done=false; const out=[]; for (const pair of this._p){ if(pair[0]===k){ if(!done){ out.push([k,v]); done=true; } } else out.push(pair); } if(!done) out.push([k,v]); this._p=out; this._notify(); }
delete(k){k=String(k); this._p=this._p.filter(([key])=>key!==k); this._notify();} delete(k,v){k=String(k); const hv=(v!==undefined); v=String(v); this._p=this._p.filter(([key,val])=> hv ? !(key===k&&val===v) : key!==k); this._notify();}
has(k){k=String(k); return this._p.some(([key])=>key===k);} has(k,v){k=String(k); const hv=(v!==undefined); v=String(v); return this._p.some(([key,val])=> hv ? (key===k&&val===v) : key===k);}
sort(){ this._p.sort((a,b)=> a[0]<b[0]?-1:(a[0]>b[0]?1:0)); this._notify(); } sort(){ this._p.sort((a,b)=> a[0]<b[0]?-1:(a[0]>b[0]?1:0)); this._notify(); }
get size(){ return this._p.length; } get size(){ return this._p.length; }
toString(){return this._p.map(pair=>_formEncode(pair[0])+"="+_formEncode(pair[1])).join("&");} toString(){return this._p.map(pair=>_formEncode(pair[0])+"="+_formEncode(pair[1])).join("&");}
@@ -3809,6 +3890,21 @@ globalThis.DOMParser = class DOMParser {
get lastChild() { return root; }, get lastChild() { return root; },
get children() { return [root]; }, get children() { return [root]; },
get childNodes() { return [root]; }, get childNodes() { return [root]; },
// Document metadata the WHATWG interface exposes; DOMParser documents have
// URL about:blank, are already fully parsed, and carry no stylesheets.
get URL() { return "about:blank"; },
get documentURI() { return "about:blank"; },
get baseURI() { return "about:blank"; },
get compatMode() { return "CSS1Compat"; },
get characterSet() { return "UTF-8"; },
get charset() { return "UTF-8"; },
get inputEncoding() { return "UTF-8"; },
get readyState() { return "complete"; },
get styleSheets() { return { length: 0, item() { return null; }, [Symbol.iterator]: function* () {} }; },
get defaultView() { return null; },
get ownerDocument() { return null; },
createTreeWalker(r, ws, f) { return document.createTreeWalker(r || root, ws, f); },
createNodeIterator(r, ws, f) { return document.createNodeIterator(r || root, ws, f); },
querySelector(s) { return root.querySelector(s); }, querySelector(s) { return root.querySelector(s); },
querySelectorAll(s) { return root.querySelectorAll(s); }, querySelectorAll(s) { return root.querySelectorAll(s); },
getElementById(id) { getElementById(id) {
@@ -4142,6 +4238,9 @@ globalThis.DocumentType = DocumentType;
globalThis.Node = Node; globalThis.Node = Node;
globalThis.Element = Element; globalThis.Element = Element;
globalThis.Document = Document; globalThis.Document = Document;
// XMLDocument is a subclass of Document (DOMParser of an XML type and
// implementation.createDocument produce one). The interface must exist globally.
if (typeof XMLDocument === "undefined") globalThis.XMLDocument = class XMLDocument extends Document {};
// ParentNode mixin: Document and DocumentFragment are ParentNodes too, so they // ParentNode mixin: Document and DocumentFragment are ParentNodes too, so they
// share Element's append / prepend / replaceChildren. // share Element's append / prepend / replaceChildren.
for (const _proto of [Document.prototype, DocumentFragment.prototype]) { for (const _proto of [Document.prototype, DocumentFragment.prototype]) {
@@ -4281,6 +4380,7 @@ globalThis.Range = class Range {
selectNode(n) { const p = n.parentNode; if (!p) throw new DOMException("node has no parent", "InvalidNodeTypeError"); const i = _rngNodeIndex(n); this._sc = p; this._so = i; this._ec = p; this._eo = i + 1; } selectNode(n) { const p = n.parentNode; if (!p) throw new DOMException("node has no parent", "InvalidNodeTypeError"); const i = _rngNodeIndex(n); this._sc = p; this._so = i; this._ec = p; this._eo = i + 1; }
selectNodeContents(n) { if (n && n.nodeType === 10) throw new DOMException("cannot select a DocumentType", "InvalidNodeTypeError"); const len = _rngNodeLength(n); this._sc = n; this._so = 0; this._ec = n; this._eo = len; } selectNodeContents(n) { if (n && n.nodeType === 10) throw new DOMException("cannot select a DocumentType", "InvalidNodeTypeError"); const len = _rngNodeLength(n); this._sc = n; this._so = 0; this._ec = n; this._eo = len; }
comparePoint(n, o) { comparePoint(n, o) {
o = o >>> 0; // offset is a WebIDL unsigned long: -1 -> 4294967295 -> IndexSizeError
if (_rngRoot(n)._nid !== _rngRoot(this._sc)._nid) throw new DOMException("nodes are in different trees", "WrongDocumentError"); if (_rngRoot(n)._nid !== _rngRoot(this._sc)._nid) throw new DOMException("nodes are in different trees", "WrongDocumentError");
if (n.nodeType === 10) throw new DOMException("node is a DocumentType", "InvalidNodeTypeError"); if (n.nodeType === 10) throw new DOMException("node is a DocumentType", "InvalidNodeTypeError");
if (o > _rngNodeLength(n)) throw new DOMException("offset out of bounds", "IndexSizeError"); if (o > _rngNodeLength(n)) throw new DOMException("offset out of bounds", "IndexSizeError");
@@ -4289,6 +4389,7 @@ globalThis.Range = class Range {
return 0; return 0;
} }
isPointInRange(n, o) { isPointInRange(n, o) {
o = o >>> 0;
if (!this._sc || _rngRoot(n)._nid !== _rngRoot(this._sc)._nid) return false; if (!this._sc || _rngRoot(n)._nid !== _rngRoot(this._sc)._nid) return false;
if (n.nodeType === 10) throw new DOMException("node is a DocumentType", "InvalidNodeTypeError"); if (n.nodeType === 10) throw new DOMException("node is a DocumentType", "InvalidNodeTypeError");
if (o > _rngNodeLength(n)) throw new DOMException("offset out of bounds", "IndexSizeError"); if (o > _rngNodeLength(n)) throw new DOMException("offset out of bounds", "IndexSizeError");
+10
View File
@@ -684,6 +684,16 @@ async fn op_fetch_url(
} }
} }
// Send a default User-Agent on fetch()/XHR requests (the navigation path
// sets one, but this op did not, so scripted requests went out with no UA
// and UA-gated servers rejected them). Honor an explicit override.
if !custom_headers.keys().any(|k| k.eq_ignore_ascii_case("user-agent")) {
req = req.header(
"User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
);
}
for (k, v) in &custom_headers { for (k, v) in &custom_headers {
req = req.header(k.as_str(), v.as_str()); req = req.header(k.as_str(), v.as_str());
} }