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:
1 parent
3b2c6c0deb
commit
a703b7ac30
4 files changed
+160
-15
No files matched your search
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+115
-14
@@ -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");
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user