8985 lines
411 KiB
JavaScript
8985 lines
411 KiB
JavaScript
"use strict";
|
|
(function () {
|
|
|
|
// Pre-declare all internal globals as non-enumerable so they are invisible
|
|
// to Object.keys(window) / for-in enumeration. Must run before any var
|
|
// declarations or property assignments below: once a property is defined
|
|
// with enumerable:false here, subsequent `var x = value` assignments will
|
|
// find the property already exists and only update the value, leaving the
|
|
// descriptor intact. Direct globalThis.x = value assignments also only
|
|
// update the value without touching enumerable when the property is
|
|
// writable:true and configurable:true.
|
|
(function _preHideInternals() {
|
|
var _names = [
|
|
// runtime-set by Rust (runtime.rs / page.rs)
|
|
'__obscura_errors', '__obscura_init', '__obscura_hide_list',
|
|
'__obscura_objects', '__obscura_oid', '__obscura_ua',
|
|
'__obscura_platform', '__obscura_ua_platform', '__obscura_ua_platform_version',
|
|
'__obscura_stealth', '__obscura_markTrusted',
|
|
'__obscura_hw', '__obscura_mem',
|
|
'__documentReadyState__', '__currentUrl',
|
|
// internal helpers (var-declared throughout the file)
|
|
'__processDynScriptQueue', '_decodeDataScriptUrl', '_markNative', '_fpRand', '_fpNoise',
|
|
'_fpCache', '_getFp', '_fp', '_splitAsciiWhitespace',
|
|
'_getElementsByClassName', '_docEncoding', '_docIsUtf8',
|
|
'_isSpecialScheme', '_applyDocQueryEncoding', '_anchorBase',
|
|
'_elemHrefURL', '_setElemHrefPart', '_pad', '_daysInMonth',
|
|
'_isoWeek1Monday', '_inputParseNumber', '_inputFormatNumber',
|
|
'_htmlAttrName', '_convertNodes', '_parseHTMLFragment', '_elementClassFor', '_wrap', '_wrapEl',
|
|
'_resolveUrl', '_registerIframe', '_base64ToUint8Array',
|
|
'_bodyToUint8Array', '_arrayBufferFromBytes',
|
|
'_installWasmStreamingFallback', '_urlParseOp', '_urlSetOp',
|
|
'_urlResolveOp', '_decodeBodyWithCharset', '_utf8DecodeBytes',
|
|
'_selectionFor', '_isConstructorCE', '_isValidCustomElementName',
|
|
'_blobPartToBytes', '_bytesToBinaryString', '_formEncode', '_hexv',
|
|
'_commonFonts', '_isXMLDocument', '_isValidPITarget', '_isHTMLEl',
|
|
'_nodeList', '_rngNodeLength', '_rngNodeIndex', '_rngSame', '_rngRoot',
|
|
'_rngAncestors', '_rngOrder', '_rngCmp', '_rngCheckOffset',
|
|
'_idbRequest', '_idbObjectStore', '_idbTransaction', '_idbDatabase',
|
|
'_makeListenerBox',
|
|
// WebIDL interfaces. A real browser exposes these on the global as
|
|
// enumerable:false; here they were assigned with `globalThis.X = X`, which
|
|
// defaults to enumerable:true and is detectable in one line:
|
|
// Object.getOwnPropertyDescriptor(window, 'Node').enumerable
|
|
// Pre-declaring them non-enumerable here is enough -- per the note above,
|
|
// the later `globalThis.X = X` assignments only update the value.
|
|
'Node', 'Element', 'Document', 'DocumentFragment', 'DocumentType',
|
|
'Text', 'Comment', 'CDATASection', 'ProcessingInstruction', 'CharacterData',
|
|
'CSSStyleDeclaration', 'DOMTokenList', 'Screen', 'NetworkInformation',
|
|
'MessageChannel', 'MessagePort', 'CustomElementRegistry',
|
|
'XMLHttpRequestEventTarget', 'HTMLMediaElement', 'HTMLVideoElement',
|
|
'HTMLAudioElement', 'WebGL2RenderingContext',
|
|
];
|
|
var _desc = { value: undefined, writable: true, enumerable: false, configurable: true };
|
|
for (var _i = 0; _i < _names.length; _i++) {
|
|
try { Object.defineProperty(globalThis, _names[_i], _desc); } catch (_e) {}
|
|
}
|
|
})();
|
|
|
|
globalThis.__obscura_errors = [];
|
|
|
|
globalThis.addEventListener = globalThis.addEventListener || function(){};
|
|
globalThis.onunhandledrejection = function(e) { if (e?.preventDefault) e.preventDefault(); };
|
|
|
|
globalThis.onerror = function(msg, src, line, col, error) {
|
|
globalThis.__obscura_errors.push({msg: String(msg), src: String(src||""), line, error: String(error||"")});
|
|
};
|
|
globalThis.__windowListeners = {};
|
|
globalThis.addEventListener = function(type, fn) {
|
|
if (!globalThis.__windowListeners[type]) globalThis.__windowListeners[type] = [];
|
|
globalThis.__windowListeners[type].push(fn);
|
|
};
|
|
globalThis.removeEventListener = function(type, fn) {
|
|
if (globalThis.__windowListeners[type]) {
|
|
globalThis.__windowListeners[type] = globalThis.__windowListeners[type].filter(h => h !== fn);
|
|
}
|
|
};
|
|
globalThis.dispatchEvent = function(event) {
|
|
if (!event) return true;
|
|
const handlers = globalThis.__windowListeners[event.type] || [];
|
|
for (const h of handlers) { try { h.call(globalThis, event); } catch(e) { console.error(e); } }
|
|
return !event.defaultPrevented;
|
|
};
|
|
|
|
const _dom = (cmd, a1, a2) => Deno.core.ops.op_dom(cmd, String(a1 ?? ""), String(a2 ?? ""));
|
|
|
|
const _nativeFns = new Set();
|
|
// Exact toString override for members whose native form is not just
|
|
// `function <name>()`, e.g. accessors (`function get x() { [native code] }`)
|
|
// or functions whose `.name` does not match the real builtin.
|
|
const _nativeStr = new Map();
|
|
const _origToString = Function.prototype.toString;
|
|
Function.prototype.toString = function toString() {
|
|
if (_nativeStr.has(this)) { return _nativeStr.get(this); }
|
|
if (_nativeFns.has(this)) {
|
|
return `function ${this.name || ''}() { [native code] }`;
|
|
}
|
|
return _origToString.call(this);
|
|
};
|
|
function _markNative(fn) { if (typeof fn === 'function') _nativeFns.add(fn); return fn; }
|
|
// Mark a function with an exact native-code toString (used for accessors).
|
|
function _markNativeAs(fn, str) { if (typeof fn === 'function') _nativeStr.set(fn, str); return fn; }
|
|
_nativeFns.add(Function.prototype.toString);
|
|
|
|
// unusualWindowProperties: obscura's internal globals are made non-enumerable
|
|
// (see _preHideInternals and __obscura_init), which hides them from
|
|
// Object.keys / for-in. But fingerprinting scripts enumerate the global object
|
|
// with Object.getOwnPropertyNames and Reflect.ownKeys, which return
|
|
// non-enumerable properties too, so the internals still leak (pixelscan's
|
|
// unusualWindowProperties check). Filter the engine's own globals out of the
|
|
// reflection APIs when they target the global object. The canonical name set is
|
|
// __obscura_hide_list, precomputed at snapshot-build time; referencing it lazily
|
|
// means the list is already populated by the time any page calls these.
|
|
(function _hideInternalsFromReflection() {
|
|
var _cache = null, _cacheLen = -1;
|
|
function _set() {
|
|
var list = globalThis.__obscura_hide_list;
|
|
if (!list) { return null; }
|
|
if (_cache && _cacheLen === list.length) { return _cache; }
|
|
_cache = new Set(list);
|
|
_cache.add('__obscura_hide_list');
|
|
_cacheLen = list.length;
|
|
return _cache;
|
|
}
|
|
function _isGlobal(t) { return t === globalThis; }
|
|
function _filter(t, names) {
|
|
if (!_isGlobal(t)) { return names; }
|
|
var set = _set();
|
|
if (!set) { return names; }
|
|
var out = [];
|
|
for (var i = 0; i < names.length; i++) { if (!set.has(names[i])) { out.push(names[i]); } }
|
|
return out;
|
|
}
|
|
var _oGOPN = Object.getOwnPropertyNames;
|
|
var _oOwnKeys = Reflect.ownKeys;
|
|
var _oKeys = Object.keys;
|
|
var _oGOPDs = Object.getOwnPropertyDescriptors;
|
|
function define(obj, prop, impl) {
|
|
try { Object.defineProperty(obj, prop, { value: _markNative(impl), writable: true, enumerable: false, configurable: true }); } catch (e) {}
|
|
}
|
|
define(Object, 'getOwnPropertyNames', function getOwnPropertyNames(t) { return _filter(t, _oGOPN(t)); });
|
|
define(Reflect, 'ownKeys', function ownKeys(t) { return _filter(t, _oOwnKeys(t)); });
|
|
define(Object, 'keys', function keys(t) { return _filter(t, _oKeys(t)); });
|
|
define(Object, 'getOwnPropertyDescriptors', function getOwnPropertyDescriptors(t) {
|
|
var all = _oGOPDs(t);
|
|
if (_isGlobal(t)) {
|
|
var set = _set();
|
|
if (set) { var ks = _oGOPN(all); for (var i = 0; i < ks.length; i++) { if (set.has(ks[i])) { delete all[ks[i]]; } } }
|
|
}
|
|
return all;
|
|
});
|
|
})();
|
|
|
|
[Error, TypeError, ReferenceError, SyntaxError, RangeError, URIError, EvalError].forEach(E => {
|
|
try {
|
|
Object.defineProperty(E.prototype, 'name', {
|
|
value: E.name, writable: true, enumerable: false, configurable: false,
|
|
});
|
|
} catch(e) {}
|
|
});
|
|
|
|
const _stackCache = new WeakMap();
|
|
const _origStackDesc = Object.getOwnPropertyDescriptor(Error.prototype, 'stack');
|
|
if (_origStackDesc && _origStackDesc.get) {
|
|
Object.defineProperty(Error.prototype, 'stack', {
|
|
configurable: false, enumerable: false,
|
|
get: function() {
|
|
if (!_stackCache.has(this)) _stackCache.set(this, _origStackDesc.get.call(this));
|
|
return _stackCache.get(this);
|
|
}
|
|
});
|
|
}
|
|
|
|
let _fpSeed = 0;
|
|
// Dynamic script import queue — serializes concurrent import() calls
|
|
// to prevent re-entrant RefCell panic in deno_core's futures_unordered_driver
|
|
// when SPAs dynamically insert multiple <script module> tags at once.
|
|
let __dynScriptQueue = [];
|
|
let __dynScriptBusy = false;
|
|
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
|
value: function() { return __dynScriptBusy || __dynScriptQueue.length > 0; },
|
|
writable: false,
|
|
enumerable: false,
|
|
configurable: false,
|
|
});
|
|
function _decodeDataScriptUrl(url) {
|
|
const comma = url.indexOf(',');
|
|
if (!url.startsWith('data:') || comma < 5) {
|
|
throw new TypeError('Invalid dynamic script data URL');
|
|
}
|
|
|
|
const meta = url.slice(5, comma);
|
|
const fragment = url.indexOf('#', comma + 1);
|
|
const payload = url.slice(comma + 1, fragment < 0 ? url.length : fragment);
|
|
if (meta.split(';').some(part => part.toLowerCase() === 'base64')) {
|
|
let encoded = payload.replace(/[\r\n\t\f ]/g, '');
|
|
const remainder = encoded.length % 4;
|
|
if (remainder === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || /=/.test(encoded.slice(0, -2))) {
|
|
throw new TypeError('Invalid dynamic script data URL base64');
|
|
}
|
|
if (remainder > 0) encoded += '='.repeat(4 - remainder);
|
|
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
|
|
throw new TypeError('Invalid dynamic script data URL base64');
|
|
}
|
|
return new TextDecoder().decode(_base64ToUint8Array(encoded));
|
|
}
|
|
|
|
const bytes = [];
|
|
for (let i = 0; i < payload.length; i++) {
|
|
const code = payload.charCodeAt(i);
|
|
if (code === 0x25 && i + 2 < payload.length) {
|
|
const hi = _hexv(payload.charCodeAt(i + 1));
|
|
const lo = _hexv(payload.charCodeAt(i + 2));
|
|
if (hi >= 0 && lo >= 0) {
|
|
bytes.push(hi * 16 + lo);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
if (code < 0x80) {
|
|
bytes.push(code);
|
|
} else {
|
|
const character = String.fromCodePoint(payload.codePointAt(i));
|
|
if (character.length === 2) i++;
|
|
const encoded = new TextEncoder().encode(character);
|
|
for (let j = 0; j < encoded.length; j++) bytes.push(encoded[j]);
|
|
}
|
|
}
|
|
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
}
|
|
async function __processDynScriptQueue() {
|
|
if (__dynScriptBusy) return;
|
|
__dynScriptBusy = true;
|
|
// try/finally so the busy flag is always cleared even if a task throws
|
|
// outside its own guard; otherwise the queue would wedge and silently
|
|
// block every later dynamic script on the page.
|
|
try {
|
|
while (__dynScriptQueue.length > 0) {
|
|
const task = __dynScriptQueue.shift();
|
|
try {
|
|
if (task.isModule) {
|
|
await import(task.url);
|
|
} else {
|
|
let body;
|
|
if (task.url.startsWith('data:')) {
|
|
body = _decodeDataScriptUrl(task.url);
|
|
} else {
|
|
const raw = await Deno.core.ops.op_fetch_url(task.url, "GET", "{}", "", task.pageOrigin, "no-cors");
|
|
body = JSON.parse(raw).body;
|
|
}
|
|
if (body) {
|
|
globalThis.__currentScriptNid = task.nid;
|
|
try { (0, eval)(body); }
|
|
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
|
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
|
}
|
|
}
|
|
// Fire load via dispatchEvent only: it invokes the element's onload
|
|
// property handler and any addEventListener('load') listeners, read
|
|
// live off the element. Calling onload separately would double-fire it.
|
|
try { task.dispatchEvent(new Event('load')); } catch(e) {}
|
|
} catch(e) {
|
|
console.error('Dynamic script fetch error:', e.message);
|
|
try { task.dispatchEvent(new Event('error')); } catch(ex) {}
|
|
}
|
|
}
|
|
} finally {
|
|
__dynScriptBusy = false;
|
|
}
|
|
}
|
|
// Resolve a resource URL (script src / link href) against <base href> or the
|
|
// document URL, the way the inline dynamic-script path does. Guarded so a bad
|
|
// base or href never throws into appendChild.
|
|
function _resolveResourceUrl(src) {
|
|
let baseHref = null;
|
|
try {
|
|
const baseEl = globalThis.document?.querySelector('base[href]');
|
|
baseHref = baseEl ? baseEl.getAttribute('href') : null;
|
|
} catch(e) { baseHref = null; }
|
|
const docUrl = globalThis.location?.href || 'http://localhost/';
|
|
let baseUrl;
|
|
try { baseUrl = baseHref ? new URL(baseHref, docUrl).href : docUrl; }
|
|
catch(e) { baseUrl = docUrl; }
|
|
try {
|
|
return src.startsWith('http') || src.startsWith('data:')
|
|
? src
|
|
: new URL(src, baseUrl).href;
|
|
} catch(e) { return src; }
|
|
}
|
|
|
|
// A dynamically-inserted <link rel="stylesheet" href> must fetch and fire
|
|
// load/error so frameworks awaiting the link's onload (Promise.all of lazy
|
|
// CSS + JS, antd/bootstrap loaders, etc.) resolve instead of hanging forever.
|
|
// There is no layout engine to apply the CSS, but the load-event contract
|
|
// matches Chrome. Issue #409.
|
|
async function _loadLinkedStylesheet(c) {
|
|
// obscura does not yet reflect the `rel` IDL attribute back to the content
|
|
// attribute, so `link.rel = "stylesheet"` leaves getAttribute('rel') null.
|
|
// Read both so the property-assignment form (the common framework pattern)
|
|
// and the parsed-from-HTML form are both recognized.
|
|
const rel = (c.getAttribute('rel') || c.rel || '').toString().toLowerCase();
|
|
if (!rel.split(/\s+/).includes('stylesheet')) return;
|
|
const href = c.getAttribute('href');
|
|
if (!href) return;
|
|
const fullUrl = _resolveResourceUrl(href);
|
|
let pageOrigin = "";
|
|
try { pageOrigin = new URL(fullUrl).origin; } catch(e) {}
|
|
try {
|
|
await Deno.core.ops.op_fetch_url(fullUrl, "GET", "{}", "", pageOrigin, "no-cors");
|
|
try { c.dispatchEvent(new Event('load', { bubbles: true })); } catch(e) {}
|
|
} catch(e) {
|
|
try { c.dispatchEvent(new Event('error', { bubbles: true })); } catch(e) {}
|
|
}
|
|
}
|
|
|
|
function _fpRand(salt) {
|
|
let h = (_fpSeed ^ (salt || 0)) | 0;
|
|
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
|
|
h = Math.imul(h ^ (h >>> 13), 0x45d9f3b);
|
|
return ((h ^ (h >>> 16)) >>> 0) / 0xFFFFFFFF;
|
|
}
|
|
function _fpNoise(x, y, channel) {
|
|
return (_fpRand(x * 7919 + y * 6271 + channel * 8923) - 0.5) * 4;
|
|
}
|
|
|
|
var _fpCache = null;
|
|
function _getFp() {
|
|
if (_fpCache) return _fpCache;
|
|
const _uaPlat = globalThis.__obscura_ua_platform || 'Windows';
|
|
const isMac = _uaPlat === 'macOS';
|
|
const isLinux = _uaPlat === 'Linux';
|
|
const gpuPool = isMac ? [
|
|
'ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)',
|
|
'ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Pro, Unspecified Version)',
|
|
'ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)',
|
|
'ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version)',
|
|
'ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)',
|
|
'ANGLE (Intel Inc., ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)',
|
|
] : isLinux ? [
|
|
'ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)',
|
|
'ANGLE (Intel, Mesa Intel(R) Iris(R) Xe Graphics (TGL GT2), OpenGL 4.6)',
|
|
'ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (RPL-S), OpenGL 4.6)',
|
|
'ANGLE (AMD, AMD Radeon RX 580 (polaris10, LLVM 15.0.7, DRM 3.54, LLVM 15.0.7), OpenGL 4.6)',
|
|
'ANGLE (AMD, AMD Radeon RX 6700 XT (navi22, LLVM 16.0.6, DRM 3.54, LLVM 16.0.6), OpenGL 4.6)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 OpenGL 4.6)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 OpenGL 4.6)',
|
|
] : [
|
|
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (Intel, Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (AMD, AMD Radeon RX 5700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
];
|
|
const gpuVendorPool = isMac ? [
|
|
'Google Inc. (Apple)','Google Inc. (Apple)','Google Inc. (Apple)',
|
|
'Google Inc. (Apple)','Google Inc. (Apple)',
|
|
'Google Inc. (Intel Inc.)',
|
|
] : isLinux ? [
|
|
'Google Inc. (Intel)','Google Inc. (Intel)','Google Inc. (Intel)',
|
|
'Google Inc. (AMD)','Google Inc. (AMD)',
|
|
'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)',
|
|
] : [
|
|
'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)','Google Inc. (NVIDIA)',
|
|
'Google Inc. (Intel)','Google Inc. (Intel)',
|
|
'Google Inc. (AMD)','Google Inc. (AMD)',
|
|
'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)',
|
|
'Google Inc. (Intel)','Google Inc. (AMD)','Google Inc. (NVIDIA)',
|
|
];
|
|
const idx = Math.floor(_fpRand(42) * gpuPool.length);
|
|
const screenPool = [[1920,1080],[2560,1440],[1366,768],[1536,864],[1440,900],[1680,1050],[1280,720],[3840,2160]];
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
let cfp = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg';
|
|
for (let i = 0; i < 40; i++) cfp += chars[Math.floor(_fpRand(500 + i) * 64)];
|
|
cfp += '==';
|
|
_fpCache = {
|
|
gpu: gpuPool[idx], gpuVendor: gpuVendorPool[idx],
|
|
audioBaseLatency: 0.002 + _fpRand(100) * 0.008,
|
|
audioSampleRate: [44100, 48000][Math.floor(_fpRand(101) * 2)],
|
|
compThreshold: -24 + (_fpRand(102) - 0.5) * 4,
|
|
compKnee: 30 + (_fpRand(103) - 0.5) * 4,
|
|
compRatio: 12 + (_fpRand(104) - 0.5) * 4,
|
|
batteryLevel: 0.5 + _fpRand(200) * 0.5,
|
|
batteryCharging: _fpRand(201) > 0.3,
|
|
screen: screenPool[Math.floor(_fpRand(300) * screenPool.length)],
|
|
canvasFingerprint: cfp,
|
|
};
|
|
return _fpCache;
|
|
}
|
|
function _fp(key) { return _getFp()[key]; }
|
|
globalThis._eventRegistry = globalThis._eventRegistry || {};
|
|
globalThis._formValues = globalThis._formValues || {};
|
|
globalThis._formChecked = globalThis._formChecked || {};
|
|
const _eventRegistry = globalThis._eventRegistry;
|
|
const _formValues = globalThis._formValues;
|
|
const _formChecked = globalThis._formChecked;
|
|
const _domParse = (cmd, a1, a2) => { try { return JSON.parse(_dom(cmd, a1, a2)); } catch { return null; } };
|
|
|
|
// HTML "ASCII whitespace": U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE.
|
|
// Class token splitting (classList, getElementsByClassName) uses exactly this set.
|
|
// JS \s is wider (U+000B, U+00A0, U+2028, etc.), so it must not be used here.
|
|
const _ASCII_WS = /[ \t\n\f\r]+/;
|
|
function _splitAsciiWhitespace(s) {
|
|
// WebIDL DOMString coercion: null -> "null", undefined -> "undefined".
|
|
return String(s).split(_ASCII_WS).filter(Boolean);
|
|
}
|
|
// Shared getElementsByClassName: split the argument into an ordered set of
|
|
// tokens on ASCII whitespace, then return descendants (in tree order) whose
|
|
// class attribute contains every token, as an HTMLCollection (so namedItem and
|
|
// named access work on the result). `root` must expose querySelectorAll.
|
|
function _getElementsByClassName(root, classNames) {
|
|
const tokens = _splitAsciiWhitespace(classNames);
|
|
if (tokens.length === 0) return HTMLCollection._from([]);
|
|
// Fast path: a single CSS-identifier token goes straight to the native
|
|
// selector engine (the common case). Only multi-token sets or exotic class
|
|
// names (NBSP, leading digits, etc.) fall back to the O(n) JS scan below.
|
|
if (tokens.length === 1 && /^[A-Za-z_-][\w-]*$/.test(tokens[0])) {
|
|
return HTMLCollection._from(root.querySelectorAll("." + tokens[0]));
|
|
}
|
|
const all = root.querySelectorAll("*");
|
|
const matched = [];
|
|
for (let i = 0; i < all.length; i++) {
|
|
const el = all[i];
|
|
const elTokens = _splitAsciiWhitespace(el.getAttribute ? (el.getAttribute("class") || "") : "");
|
|
let ok = true;
|
|
for (let t = 0; t < tokens.length; t++) {
|
|
if (elTokens.indexOf(tokens[t]) < 0) { ok = false; break; }
|
|
}
|
|
if (ok) matched.push(el);
|
|
}
|
|
return HTMLCollection._from(matched);
|
|
}
|
|
const _consoleFn = (level, args) => {
|
|
try { Deno.core.ops.op_console_msg(level, args.map(a => {
|
|
if (a === null) return "null";
|
|
if (a === undefined) return "undefined";
|
|
if (a instanceof Error) {
|
|
const _pst = Error.prepareStackTrace;
|
|
if (_pst !== undefined) Error.prepareStackTrace = undefined;
|
|
const _s = a.stack || a.message || String(a);
|
|
if (_pst !== undefined) Error.prepareStackTrace = _pst;
|
|
return _s;
|
|
}
|
|
if (typeof a === "object") {
|
|
try {
|
|
const s = JSON.stringify(a);
|
|
return s === "{}" && a.message ? a.message : s;
|
|
} catch { return String(a); }
|
|
}
|
|
return String(a);
|
|
}).join(" ")); } catch {}
|
|
};
|
|
|
|
globalThis.console = {
|
|
log: (...a) => _consoleFn("log", a), warn: (...a) => _consoleFn("warn", a),
|
|
error: (...a) => _consoleFn("error", a), info: (...a) => _consoleFn("log", a),
|
|
debug: () => {}, dir: () => {}, trace: () => {}, table: () => {}, group: () => {},
|
|
groupEnd: () => {}, groupCollapsed: () => {}, time: () => {}, timeEnd: () => {},
|
|
timeLog: () => {}, count: () => {}, countReset: () => {}, clear: () => {},
|
|
assert: (c, ...a) => { if (!c) _consoleFn("error", ["Assertion failed:", ...a]); },
|
|
};
|
|
|
|
let _tid = 0;
|
|
const _clearedTimers = new Set();
|
|
const _intervals = new Set();
|
|
|
|
const _scheduleAfter = (delay, fn) => {
|
|
const d = Math.max(0, Number(delay) || 0);
|
|
if (d === 0) Promise.resolve().then(fn);
|
|
else Deno.core.ops.op_sleep(d).then(fn);
|
|
};
|
|
|
|
// Timers accept a string first arg per the HTML spec (e.g. the Aliyun WAF
|
|
// `acw_sc__v2` challenge drives `setTimeout('reload(arg2)', 2)`). A string is
|
|
// compiled and run in global scope, identical to a real browser; otherwise the
|
|
// call silently no-ops and JS-triggered navigations (cookie → reload) never fire.
|
|
const _coerceTimerFn = (fn) => {
|
|
if (typeof fn === "string") {
|
|
// Per HTML, a string handler is compiled and run as a classic script in
|
|
// global scope *at fire time*. Indirect eval ((0, eval)) runs in the true
|
|
// global scope, so top-level var/function declarations become globals (a
|
|
// `new Function(fn)` wrapper kept them local); deferring to fire time also
|
|
// surfaces a SyntaxError when the timer elapses, matching a real browser,
|
|
// instead of swallowing it eagerly at scheduling. The dynamic-script path
|
|
// uses the same indirect eval for the same reason.
|
|
const src = fn;
|
|
return () => { (0, eval)(src); };
|
|
}
|
|
return typeof fn === "function" ? fn : null;
|
|
};
|
|
|
|
globalThis.setTimeout = (fn, delay = 0, ...args) => {
|
|
const f = _coerceTimerFn(fn);
|
|
if (f === null) return ++_tid;
|
|
const id = ++_tid;
|
|
_scheduleAfter(delay, () => {
|
|
if (_clearedTimers.has(id)) return;
|
|
try { f(...args); } catch(e) { console.error("Timer error:", e); }
|
|
});
|
|
return id;
|
|
};
|
|
|
|
globalThis.clearTimeout = (id) => { _clearedTimers.add(id); };
|
|
|
|
globalThis.setInterval = (fn, delay = 0, ...args) => {
|
|
const f = _coerceTimerFn(fn);
|
|
if (f === null) return ++_tid;
|
|
const id = ++_tid;
|
|
_intervals.add(id);
|
|
const tick = () => {
|
|
if (!_intervals.has(id)) return;
|
|
try { f(...args); } catch(e) { console.error("Interval error:", e); }
|
|
if (!_intervals.has(id)) return;
|
|
_scheduleAfter(delay, tick);
|
|
};
|
|
_scheduleAfter(delay, tick);
|
|
return id;
|
|
};
|
|
|
|
globalThis.clearInterval = (id) => { _intervals.delete(id); _clearedTimers.add(id); };
|
|
globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0);
|
|
globalThis.cancelAnimationFrame = globalThis.clearTimeout;
|
|
globalThis.queueMicrotask = globalThis.queueMicrotask || ((fn) => Promise.resolve().then(fn));
|
|
|
|
class MessageChannel {
|
|
constructor() {
|
|
this.port1 = { onmessage: null, postMessage: () => {}, close() {}, addEventListener() {}, removeEventListener() {} };
|
|
this.port2 = { onmessage: null, postMessage: () => {}, close() {}, addEventListener() {}, removeEventListener() {} };
|
|
this.port1.postMessage = (data) => {
|
|
Promise.resolve().then(() => { if (this.port2.onmessage) this.port2.onmessage({ data }); });
|
|
};
|
|
this.port2.postMessage = (data) => {
|
|
Promise.resolve().then(() => { if (this.port1.onmessage) this.port1.onmessage({ data }); });
|
|
};
|
|
}
|
|
}
|
|
globalThis.MessageChannel = MessageChannel;
|
|
globalThis.MessagePort = class MessagePort { constructor(){} postMessage(){} close(){} addEventListener(){} removeEventListener(){} };
|
|
|
|
const _cssCamelToKebab = (s) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
const _cssKebabToCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
|
// Standard CSS property names (camelCase). Real CSSStyleDeclaration exposes every
|
|
// property as an enumerable accessor, so feature-detection code (`'gap' in
|
|
// el.style`) and enumeration (`Object.keys(el.style)`) see the whole set, not
|
|
// just the ones that happen to be assigned (issue #356).
|
|
const _CSS_PROPERTY_NAMES = [
|
|
"accentColor","alignContent","alignItems","alignSelf","all","animation","animationDelay",
|
|
"animationDirection","animationDuration","animationFillMode","animationIterationCount",
|
|
"animationName","animationPlayState","animationTimingFunction","appearance","aspectRatio",
|
|
"backdropFilter","backfaceVisibility","background","backgroundAttachment","backgroundBlendMode",
|
|
"backgroundClip","backgroundColor","backgroundImage","backgroundOrigin","backgroundPosition",
|
|
"backgroundPositionX","backgroundPositionY","backgroundRepeat","backgroundSize","blockSize",
|
|
"border","borderBlock","borderBlockColor","borderBlockEnd","borderBlockEndColor","borderBlockEndStyle",
|
|
"borderBlockEndWidth","borderBlockStart","borderBlockStartColor","borderBlockStartStyle",
|
|
"borderBlockStartWidth","borderBlockStyle","borderBlockWidth","borderBottom","borderBottomColor",
|
|
"borderBottomLeftRadius","borderBottomRightRadius","borderBottomStyle","borderBottomWidth",
|
|
"borderCollapse","borderColor","borderImage","borderImageOutset","borderImageRepeat",
|
|
"borderImageSlice","borderImageSource","borderImageWidth","borderInline","borderInlineColor",
|
|
"borderInlineEnd","borderInlineEndColor","borderInlineEndStyle","borderInlineEndWidth",
|
|
"borderInlineStart","borderInlineStartColor","borderInlineStartStyle","borderInlineStartWidth",
|
|
"borderInlineStyle","borderInlineWidth","borderLeft","borderLeftColor","borderLeftStyle",
|
|
"borderLeftWidth","borderRadius","borderRight","borderRightColor","borderRightStyle",
|
|
"borderRightWidth","borderSpacing","borderStyle","borderTop","borderTopColor","borderTopLeftRadius",
|
|
"borderTopRightRadius","borderTopStyle","borderTopWidth","borderWidth","bottom","boxShadow",
|
|
"boxSizing","breakAfter","breakBefore","breakInside","captionSide","caretColor","clear","clip",
|
|
"clipPath","color","colorScheme","columnCount","columnFill","columnGap","columnRule","columnRuleColor",
|
|
"columnRuleStyle","columnRuleWidth","columnSpan","columnWidth","columns","contain","container",
|
|
"containerName","containerType","content","counterIncrement","counterReset","counterSet","cssFloat",
|
|
"cursor","direction","display","emptyCells","filter","flex","flexBasis","flexDirection","flexFlow",
|
|
"flexGrow","flexShrink","flexWrap","float","font","fontFamily","fontFeatureSettings","fontKerning",
|
|
"fontOpticalSizing","fontSize","fontSizeAdjust","fontStretch","fontStyle","fontVariant",
|
|
"fontVariantCaps","fontVariantLigatures","fontVariantNumeric","fontWeight","gap","grid","gridArea",
|
|
"gridAutoColumns","gridAutoFlow","gridAutoRows","gridColumn","gridColumnEnd","gridColumnGap",
|
|
"gridColumnStart","gridGap","gridRow","gridRowEnd","gridRowGap","gridRowStart","gridTemplate",
|
|
"gridTemplateAreas","gridTemplateColumns","gridTemplateRows","height","hyphens","imageRendering",
|
|
"inlineSize","inset","insetBlock","insetBlockEnd","insetBlockStart","insetInline","insetInlineEnd",
|
|
"insetInlineStart","isolation","justifyContent","justifyItems","justifySelf","left","letterSpacing",
|
|
"lineBreak","lineHeight","listStyle","listStyleImage","listStylePosition","listStyleType","margin",
|
|
"marginBlock","marginBlockEnd","marginBlockStart","marginBottom","marginInline","marginInlineEnd",
|
|
"marginInlineStart","marginLeft","marginRight","marginTop","mask","maxBlockSize","maxHeight",
|
|
"maxInlineSize","maxWidth","minBlockSize","minHeight","minInlineSize","minWidth","mixBlendMode",
|
|
"objectFit","objectPosition","offset","opacity","order","outline","outlineColor","outlineOffset",
|
|
"outlineStyle","outlineWidth","overflow","overflowAnchor","overflowWrap","overflowX","overflowY",
|
|
"overscrollBehavior","overscrollBehaviorBlock","overscrollBehaviorInline","overscrollBehaviorX",
|
|
"overscrollBehaviorY","padding","paddingBlock","paddingBlockEnd","paddingBlockStart","paddingBottom",
|
|
"paddingInline","paddingInlineEnd","paddingInlineStart","paddingLeft","paddingRight","paddingTop",
|
|
"pageBreakAfter","pageBreakBefore","pageBreakInside","perspective","perspectiveOrigin","placeContent",
|
|
"placeItems","placeSelf","pointerEvents","position","quotes","resize","right","rotate","rowGap",
|
|
"scale","scrollBehavior","scrollMargin","scrollPadding","scrollSnapAlign","scrollSnapStop",
|
|
"scrollSnapType","tabSize","tableLayout","textAlign","textAlignLast","textCombineUpright",
|
|
"textDecoration","textDecorationColor","textDecorationLine","textDecorationSkipInk",
|
|
"textDecorationStyle","textDecorationThickness","textEmphasis","textIndent","textJustify",
|
|
"textOrientation","textOverflow","textRendering","textShadow","textTransform","textUnderlineOffset",
|
|
"textUnderlinePosition","top","touchAction","transform","transformBox","transformOrigin",
|
|
"transformStyle","transition","transitionDelay","transitionDuration","transitionProperty",
|
|
"transitionTimingFunction","translate","unicodeBidi","userSelect","verticalAlign","visibility",
|
|
"whiteSpace","width","willChange","wordBreak","wordSpacing","wordWrap","writingMode","zIndex","zoom",
|
|
];
|
|
const _CSS_PROP_SET = new Set(_CSS_PROPERTY_NAMES);
|
|
|
|
// Parse a `style` attribute string (`"color: red; margin: 5px"`) into the given
|
|
// dashed-key store, replacing its contents in place.
|
|
function _parseCssInto(props, text) {
|
|
for (const k in props) delete props[k];
|
|
if (text) String(text).split(";").forEach((p) => {
|
|
const i = p.indexOf(":");
|
|
if (i > 0) { const k = p.slice(0, i).trim(); const v = p.slice(i + 1).trim(); if (k && v) props[_cssCamelToKebab(k)] = v; }
|
|
});
|
|
}
|
|
function _serializeCss(props) {
|
|
const e = Object.entries(props);
|
|
return e.length ? e.map(([k, v]) => `${k}: ${v}`).join("; ") + ";" : "";
|
|
}
|
|
|
|
class CSSStyleDeclaration {
|
|
constructor(owner) {
|
|
// Non-enumerable so they never leak through the proxy's own-key traps.
|
|
Object.defineProperty(this, "_props", { value: {}, writable: true, enumerable: false, configurable: true });
|
|
// The owner Element, if any. A live declaration reflects that element's
|
|
// `style` content attribute in both directions; an owner-less declaration
|
|
// (getComputedStyle fallback, stylesheet rules) is purely in-memory.
|
|
Object.defineProperty(this, "_owner", { value: owner || null, writable: true, enumerable: false, configurable: true });
|
|
// Load the content attribute only when style is first observed. Keeping
|
|
// this as a primitive avoids allocating a separate sync object for every
|
|
// wrapped element.
|
|
Object.defineProperty(this, "_loaded", { value: !owner, writable: true, enumerable: false, configurable: true });
|
|
}
|
|
// Pull the initial `style` attribute once. Later attribute mutations update
|
|
// the declaration directly from Element.setAttribute/removeAttribute, so
|
|
// repeated style reads do not cross the JS/Rust op boundary.
|
|
_pull() {
|
|
if (this._loaded) return;
|
|
_parseCssInto(this._props, this._owner.getAttribute("style"));
|
|
this._loaded = true;
|
|
}
|
|
_replaceFromAttribute(text) {
|
|
_parseCssInto(this._props, text);
|
|
this._loaded = true;
|
|
}
|
|
// Serialize `_props` back onto the owner's `style` attribute after a mutation,
|
|
// so el.style.x = … and cssText reflect into getAttribute('style') and
|
|
// serialization. No-op when owner-less.
|
|
_push() {
|
|
const o = this._owner;
|
|
if (!o) return;
|
|
const text = _serializeCss(this._props);
|
|
if (text) o.setAttribute("style", text);
|
|
else o.removeAttribute("style");
|
|
}
|
|
// Storage is keyed by the dashed CSS name, matching CSSOM. The proxy maps the
|
|
// camelCase IDL access (el.style.fontSize) onto the dashed key (font-size), so
|
|
// getPropertyValue('font-size') and el.style.fontSize stay in sync.
|
|
setProperty(name, value) {
|
|
this._pull();
|
|
const k = _cssCamelToKebab(String(name));
|
|
if (value === "" || value == null) delete this._props[k];
|
|
else this._props[k] = String(value);
|
|
this._push();
|
|
}
|
|
removeProperty(name) { this._pull(); const k = _cssCamelToKebab(String(name)); const old = this._props[k]; delete this._props[k]; this._push(); return old || ""; }
|
|
getPropertyValue(name) { this._pull(); return this._props[_cssCamelToKebab(String(name))] || ""; }
|
|
getPropertyPriority() { return ""; }
|
|
get cssText() { this._pull(); return _serializeCss(this._props); }
|
|
set cssText(v) {
|
|
_parseCssInto(this._props, v);
|
|
this._push();
|
|
}
|
|
get length() { this._pull(); return Object.keys(this._props).length; }
|
|
item(i) { this._pull(); return Object.keys(this._props)[i] || ""; }
|
|
}
|
|
|
|
const _styleProxy = (decl) => new Proxy(decl, {
|
|
get(t, p) {
|
|
if (typeof p === "symbol" || p in t) return t[p];
|
|
if (/^\d+$/.test(p)) return t.item(+p);
|
|
return t.getPropertyValue(p);
|
|
},
|
|
set(t, p, v) {
|
|
if (typeof p === "symbol") { t[p] = v; return true; }
|
|
if (p === "_loaded") { t._loaded = v; return true; }
|
|
if (p === "cssText") { t.cssText = v; return true; }
|
|
if (/^\d+$/.test(p) || p in Object.getPrototypeOf(t)) return true;
|
|
t.setProperty(p, v);
|
|
return true;
|
|
},
|
|
has(t, p) {
|
|
if (typeof p !== "string") return Reflect.has(t, p);
|
|
if (p in Object.getPrototypeOf(t)) return true;
|
|
t._pull();
|
|
if (_cssCamelToKebab(p) in t._props) return true;
|
|
if (_CSS_PROP_SET.has(p) || _CSS_PROP_SET.has(_cssKebabToCamel(p))) return true;
|
|
return /^\d+$/.test(p) && +p < t.length;
|
|
},
|
|
ownKeys(t) {
|
|
t._pull();
|
|
const keys = [];
|
|
const n = t.length;
|
|
for (let i = 0; i < n; i++) keys.push(String(i));
|
|
const names = new Set(_CSS_PROPERTY_NAMES);
|
|
for (const k of Object.keys(t._props)) names.add(_cssKebabToCamel(k));
|
|
for (const name of names) keys.push(name);
|
|
return keys;
|
|
},
|
|
getOwnPropertyDescriptor(t, p) {
|
|
if (typeof p !== "string") return Reflect.getOwnPropertyDescriptor(t, p);
|
|
t._pull();
|
|
if (/^\d+$/.test(p) && +p < t.length) return { value: t.item(+p), writable: false, enumerable: true, configurable: true };
|
|
if (_cssCamelToKebab(p) in t._props || _CSS_PROP_SET.has(p) || _CSS_PROP_SET.has(_cssKebabToCamel(p))) {
|
|
return { value: t.getPropertyValue(p), writable: true, enumerable: true, configurable: true };
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
|
|
// Clone a single node (no children), used by Node.cloneNode. Elements are built
|
|
// with createElement/createElementNS and their content attributes copied, so no
|
|
// HTML parsing context is involved and every attribute (including style) is
|
|
// preserved. Text/Comment/DocumentFragment map to their factory; anything else
|
|
// yields null.
|
|
function _shallowCloneNode(node) {
|
|
const nt = node.nodeType;
|
|
if (nt === 3) return document.createTextNode(node.data != null ? node.data : (node.textContent || ""));
|
|
if (nt === 8) return document.createComment(node.data != null ? node.data : (node.nodeValue || ""));
|
|
if (nt === 11) return document.createDocumentFragment();
|
|
if (nt !== 1) return null;
|
|
const ns = node.namespaceURI;
|
|
const el = (ns && ns !== "http://www.w3.org/1999/xhtml")
|
|
? document.createElementNS(ns, node.nodeName)
|
|
: document.createElement(node.localName || node.nodeName.toLowerCase());
|
|
const names = node.getAttributeNames ? node.getAttributeNames() : [];
|
|
for (const name of names) {
|
|
const v = node.getAttribute(name);
|
|
if (v !== null) el.setAttribute(name, v);
|
|
}
|
|
// CSS declarations currently live on the JS wrapper independently of the
|
|
// DOM attribute. Copy that state as well so styles assigned through
|
|
// `node.style` survive cloning even before attribute reflection runs.
|
|
if (node.style && node.style.cssText) el.style.cssText = node.style.cssText;
|
|
return el;
|
|
}
|
|
|
|
class Node {
|
|
static ELEMENT_NODE = 1;
|
|
static ATTRIBUTE_NODE = 2;
|
|
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 DOCUMENT_NODE = 9;
|
|
static DOCUMENT_TYPE_NODE = 10;
|
|
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; }
|
|
get nodeType() { return +_dom("node_type", this._nid); }
|
|
get nodeName() { return _domParse("node_name", this._nid) || ""; }
|
|
get ownerDocument() { return globalThis.document; }
|
|
// https://dom.spec.whatwg.org/#dom-node-baseuri
|
|
get baseURI() {
|
|
try {
|
|
const doc = globalThis.document;
|
|
const docUrl = (doc && doc.URL) || "";
|
|
const baseEl = (doc && doc.querySelector) ? doc.querySelector("base[href]") : null;
|
|
if (baseEl) {
|
|
const href = baseEl.getAttribute("href");
|
|
if (href) {
|
|
return docUrl ? new URL(href, docUrl).href : href;
|
|
}
|
|
}
|
|
return docUrl;
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
get textContent() { return _domParse("text_content", this._nid) ?? ""; }
|
|
set textContent(v) {
|
|
const oldChildren = _domParse("child_nodes", this._nid) || [];
|
|
for (const c of oldChildren) _dom("remove_child", c);
|
|
let added = [];
|
|
if (v != null && v !== "") {
|
|
const tn = +_dom("create_text_node", String(v));
|
|
_dom("append_child", this._nid, tn);
|
|
added = [tn];
|
|
}
|
|
// Real MutationObserver fires childList for the children swap.
|
|
// Without this React 18+ hydration mismatch detection and many polling
|
|
// libs (intersection-driven lazy load, content sync) silently stall.
|
|
if (globalThis.__mutationObservers?.length) {
|
|
globalThis.__notifyMutation('childList', this._nid, added, oldChildren);
|
|
}
|
|
}
|
|
get nodeValue() {
|
|
const t = this.nodeType;
|
|
if (t === 3 || t === 8) return _domParse("text_content", this._nid) ?? "";
|
|
return null;
|
|
}
|
|
set nodeValue(v) {
|
|
const t = this.nodeType;
|
|
if (t === 3 || t === 8) _dom("set_text_content", this._nid, String(v ?? ""));
|
|
}
|
|
get parentNode() { return _wrap(+_dom("parent_node", this._nid)); }
|
|
get parentElement() { const p = this.parentNode; return p && p.nodeType === 1 ? p : null; }
|
|
get childNodes() {
|
|
const ids = _domParse("child_nodes", this._nid) || [];
|
|
return _nodeList(ids.map(_wrap).filter(Boolean));
|
|
}
|
|
get firstChild() { return _wrap(+_dom("first_child", this._nid)); }
|
|
get lastChild() { return _wrap(+_dom("last_child", this._nid)); }
|
|
get nextSibling() { return _wrap(+_dom("next_sibling", this._nid)); }
|
|
get previousSibling() { return _wrap(+_dom("prev_sibling", this._nid)); }
|
|
appendChild(c) {
|
|
if (!c) return c;
|
|
if (c instanceof DocumentFragment) {
|
|
const children = Array.from(c.childNodes);
|
|
for (const child of children) this.appendChild(child);
|
|
return c;
|
|
}
|
|
_dom("append_child", this._nid, c._nid);
|
|
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('childList', this._nid, [c._nid], []);
|
|
if (c instanceof Element && c.tagName === 'SCRIPT') {
|
|
const scriptType = c.getAttribute('type') || '';
|
|
const isModule = scriptType === 'module';
|
|
if (scriptType && !isModule && scriptType !== 'text/javascript' && scriptType !== 'application/javascript') {
|
|
return c;
|
|
}
|
|
const src = c.getAttribute('src');
|
|
const prevNid = globalThis.__currentScriptNid;
|
|
if (src) {
|
|
// Resolve against <base href> when present, else the document URL.
|
|
// The base href is resolved to an absolute URL first: a bare path like
|
|
// <base href="/"> (the common Angular form) is not a valid URL base on
|
|
// its own and would otherwise throw. Both the base and the final
|
|
// resolution are guarded so a bad value can never escape appendChild.
|
|
let baseHref;
|
|
try {
|
|
const baseEl = globalThis.document?.querySelector('base[href]');
|
|
baseHref = baseEl ? baseEl.getAttribute('href') : null;
|
|
} catch(e) { baseHref = null; }
|
|
const docUrl = globalThis.location?.href || 'http://localhost/';
|
|
let baseUrl;
|
|
try { baseUrl = baseHref ? new URL(baseHref, docUrl).href : docUrl; }
|
|
catch(e) { baseUrl = docUrl; }
|
|
let fullUrl;
|
|
try {
|
|
fullUrl = src.startsWith('http') || src.startsWith('data:')
|
|
? src
|
|
: new URL(src, baseUrl).href;
|
|
} catch(e) {
|
|
console.error('Dynamic script URL resolve failed (' + src + '):', e.message);
|
|
fullUrl = src;
|
|
}
|
|
const pageOrigin = (function() { try { return new URL(baseUrl).origin; } catch(e) { return ""; } })();
|
|
// Enqueue — serialized via __processDynScriptQueue to prevent
|
|
// concurrent import() calls from triggering deno_core RefCell panic.
|
|
__dynScriptQueue.push({
|
|
url: fullUrl,
|
|
isModule,
|
|
nid: c._nid,
|
|
prevNid,
|
|
pageOrigin,
|
|
dispatchEvent: (ev) => { try { c.dispatchEvent(ev); } catch(e) {} },
|
|
});
|
|
__processDynScriptQueue();
|
|
} else {
|
|
const code = c.textContent;
|
|
if (code) {
|
|
if (isModule) {
|
|
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
|
__dynScriptQueue.push({
|
|
url: dataUrl,
|
|
isModule: true,
|
|
nid: c._nid,
|
|
prevNid,
|
|
pageOrigin: "",
|
|
dispatchEvent: (ev) => { try { c.dispatchEvent(ev); } catch(e) {} },
|
|
});
|
|
__processDynScriptQueue();
|
|
} else {
|
|
globalThis.__currentScriptNid = c._nid;
|
|
try { (0, eval)(code); }
|
|
catch(e) { console.error('Dynamic inline script error:', e.message); }
|
|
finally { globalThis.__currentScriptNid = prevNid || 0; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (c instanceof Element && c.tagName === 'LINK') {
|
|
_loadLinkedStylesheet(c);
|
|
}
|
|
return c;
|
|
}
|
|
removeChild(c) {
|
|
if (!c) return c;
|
|
_dom("remove_child", c._nid);
|
|
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('childList', this._nid, [], [c._nid]);
|
|
return c;
|
|
}
|
|
replaceChild(newChild, oldChild) {
|
|
if (!oldChild || !newChild) return oldChild;
|
|
if (newChild instanceof DocumentFragment) {
|
|
const children = Array.from(newChild.childNodes);
|
|
for (const child of children) this.insertBefore(child, oldChild);
|
|
this.removeChild(oldChild);
|
|
return oldChild;
|
|
}
|
|
_dom("insert_before", newChild._nid, oldChild._nid);
|
|
_dom("remove_child", oldChild._nid);
|
|
return oldChild;
|
|
}
|
|
insertBefore(n, ref) {
|
|
if (!n) return n;
|
|
if (!ref) { this.appendChild(n); return n; }
|
|
if (n instanceof DocumentFragment) {
|
|
const children = Array.from(n.childNodes);
|
|
for (const child of children) this.insertBefore(child, ref);
|
|
return n;
|
|
}
|
|
_dom("insert_before", n._nid, ref._nid);
|
|
return n;
|
|
}
|
|
contains(o) { return o ? _dom("contains", this._nid, o._nid) === "true" : false; }
|
|
hasChildNodes() { return _dom("has_child_nodes", this._nid) === "true"; }
|
|
cloneNode(deep) {
|
|
// Clone structurally via real DOM nodes rather than round-tripping through a
|
|
// throwaway <div>.innerHTML: the fragment parser discards elements that are
|
|
// not valid children of <div> (<tr>, <td>, <option>, …), so the old path
|
|
// returned null for them and lost JS-set inline styles. Building each node
|
|
// directly with createElement(NS) + attribute copy avoids any parsing
|
|
// context, and an explicit stack keeps a deep subtree from overflowing the
|
|
// JS stack (issue #490).
|
|
const root = _shallowCloneNode(this);
|
|
if (!deep || !root) return root;
|
|
const stack = [[this, root]];
|
|
while (stack.length) {
|
|
const [src, dst] = stack.pop();
|
|
// A <template>'s children hang off its content fragment, not childNodes,
|
|
// so clone them into the clone's fragment. Gated on the tag name because
|
|
// .content means something else on other elements (e.g. <meta>).
|
|
if (src.localName === 'template' && dst.localName === 'template') {
|
|
const sc = src.content, dc = dst.content;
|
|
if (sc && dc && sc.childNodes) {
|
|
const tk = sc.childNodes;
|
|
for (let i = 0; i < tk.length; i++) {
|
|
const c = _shallowCloneNode(tk[i]);
|
|
if (c) { dc.appendChild(c); stack.push([tk[i], c]); }
|
|
}
|
|
}
|
|
}
|
|
const kids = src.childNodes;
|
|
for (let i = 0; i < kids.length; i++) {
|
|
const c = _shallowCloneNode(kids[i]);
|
|
if (c) { dst.appendChild(c); stack.push([kids[i], c]); }
|
|
}
|
|
}
|
|
return root;
|
|
}
|
|
compareDocumentPosition(other) {
|
|
if (!other) return 0;
|
|
if (this._nid === other._nid) return 0;
|
|
// Different roots: DISCONNECTED | IMPLEMENTATION_SPECIFIC plus a stable
|
|
// (consistent across calls) PRECEDING/FOLLOWING bit, chosen by node-id order.
|
|
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; }
|
|
normalize() {
|
|
// Merge adjacent exclusive Text nodes, drop empty ones, recurse. Detached
|
|
// removed nodes keep their own data (read from the backing node by nid).
|
|
let child = this.firstChild;
|
|
while (child) {
|
|
const next = child.nextSibling;
|
|
if (child.nodeType === 3) {
|
|
let data = child.data, sib = child.nextSibling;
|
|
while (sib && sib.nodeType === 3) { const after = sib.nextSibling; data += sib.data; this.removeChild(sib); sib = after; }
|
|
if (data.length === 0) { this.removeChild(child); child = sib; continue; }
|
|
if (data !== child.data) child.data = data;
|
|
child = sib; continue;
|
|
} else if (child.nodeType === 1 || child.nodeType === 11) {
|
|
child.normalize();
|
|
}
|
|
child = next;
|
|
}
|
|
}
|
|
isEqualNode(other) {
|
|
if (!other) return false;
|
|
if (this._nid === other._nid) return true;
|
|
if (this.nodeType !== other.nodeType) return false;
|
|
if (this.nodeName !== other.nodeName) return false;
|
|
if (this.nodeValue !== other.nodeValue) return false;
|
|
const a = this.attributes ? this.attributes : null;
|
|
const b = other.attributes ? other.attributes : null;
|
|
if ((a && a.length) || (b && b.length)) {
|
|
if (!a || !b || a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (other.getAttribute(a[i].name) !== a[i].value) return false;
|
|
}
|
|
}
|
|
const cA = this.childNodes || [];
|
|
const cB = other.childNodes || [];
|
|
if (cA.length !== cB.length) return false;
|
|
for (let i = 0; i < cA.length; i++) {
|
|
if (!cA[i].isEqualNode(cB[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
isSameNode(other) { return other && this._nid === other._nid; }
|
|
addEventListener() {} removeEventListener() {} dispatchEvent() { return true; }
|
|
}
|
|
class CharacterData extends Node {
|
|
get data() {
|
|
return _domParse("text_content", this._nid) ?? "";
|
|
}
|
|
set data(v) {
|
|
const oldValue = _domParse("text_content", this._nid) ?? "";
|
|
_dom("set_text_content", this._nid, String(v ?? ""));
|
|
if (globalThis.__mutationObservers?.length) {
|
|
globalThis.__notifyMutation('characterData', this._nid, [], [], null, oldValue);
|
|
}
|
|
}
|
|
get length() { return this.data.length; }
|
|
substringData(offset, count) {
|
|
return this.data.substring(offset, offset + count);
|
|
}
|
|
appendData(s) { this.data += s; }
|
|
insertData(offset, s) {
|
|
const d = this.data;
|
|
this.data = d.slice(0, offset) + s + d.slice(offset);
|
|
}
|
|
deleteData(offset, count) {
|
|
const d = this.data;
|
|
this.data = d.slice(0, offset) + d.slice(offset + count);
|
|
}
|
|
replaceData(offset, count, s) {
|
|
const d = this.data;
|
|
this.data = d.slice(0, offset) + s + d.slice(offset + count);
|
|
}
|
|
}
|
|
|
|
class Text extends CharacterData {
|
|
get nodeName() { return "#text"; }
|
|
get nodeType() { return 3; }
|
|
get wholeText() { return this.data; }
|
|
splitText(offset) {
|
|
const d = this.data;
|
|
const tail = d.substring(offset);
|
|
this.data = d.substring(0, offset);
|
|
const newNid = +_dom("create_text_node", tail);
|
|
const parent = this.parentNode;
|
|
if (parent) {
|
|
const ref = this.nextSibling;
|
|
parent.insertBefore(_wrap(newNid), ref);
|
|
}
|
|
return _wrap(newNid);
|
|
}
|
|
cloneNode() { return document.createTextNode(this.data); }
|
|
}
|
|
|
|
class Comment extends CharacterData {
|
|
get nodeName() { return "#comment"; }
|
|
get nodeType() { return 8; }
|
|
cloneNode() { return document.createComment(this.data); }
|
|
}
|
|
|
|
// DOMTokenList backs class/rel/sandbox/etc. attribute reflection. It parses the
|
|
// associated content attribute as an ordered set of tokens and writes changes
|
|
// straight back, so reads and writes stay live with the element. A Proxy is
|
|
// layered on top so numeric indexing (list[0]) hits item().
|
|
class DOMTokenList {
|
|
constructor(el, attr, supportedTokens) {
|
|
// Non-enumerable so the element <-> token-list cycle is not visible to
|
|
// enumeration/serialization (JSON.stringify(classList) would otherwise
|
|
// throw "circular structure").
|
|
Object.defineProperty(this, "_el", { value: el, writable: true, enumerable: false });
|
|
Object.defineProperty(this, "_attr", { value: attr, writable: true, enumerable: false });
|
|
Object.defineProperty(this, "_supported", { value: supportedTokens || null, writable: true, enumerable: false });
|
|
return new Proxy(this, {
|
|
get(t, k, r) {
|
|
if (typeof k === "string" && /^\d+$/.test(k)) return t.item(+k);
|
|
return Reflect.get(t, k, r);
|
|
},
|
|
has(t, k) {
|
|
if (typeof k === "string" && /^\d+$/.test(k)) return +k < t.length;
|
|
return Reflect.has(t, k);
|
|
},
|
|
});
|
|
}
|
|
get [Symbol.toStringTag]() { return "DOMTokenList"; }
|
|
_tokens() {
|
|
const v = this._el.getAttribute(this._attr);
|
|
if (!v) return [];
|
|
const seen = new Set();
|
|
const out = [];
|
|
for (const tok of v.split(/[ \t\n\f\r]+/)) {
|
|
if (tok && !seen.has(tok)) { seen.add(tok); out.push(tok); }
|
|
}
|
|
return out;
|
|
}
|
|
_write(tokens) {
|
|
this._el.setAttribute(this._attr, tokens.join(" "));
|
|
}
|
|
get length() { return this._tokens().length; }
|
|
get value() { return this._el.getAttribute(this._attr) || ""; }
|
|
set value(v) { this._el.setAttribute(this._attr, String(v)); }
|
|
item(i) { const t = this._tokens(); return (i >= 0 && i < t.length) ? t[i] : null; }
|
|
contains(token) { return this._tokens().includes(String(token)); }
|
|
add(...tokens) {
|
|
const t = this._tokens();
|
|
for (const raw of tokens) {
|
|
const tok = String(raw);
|
|
if (tok === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
|
|
if (/[ \t\n\f\r]/.test(tok)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
|
|
if (!t.includes(tok)) t.push(tok);
|
|
}
|
|
this._write(t);
|
|
}
|
|
remove(...tokens) {
|
|
let t = this._tokens();
|
|
for (const raw of tokens) {
|
|
const tok = String(raw);
|
|
if (tok === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
|
|
if (/[ \t\n\f\r]/.test(tok)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
|
|
t = t.filter((x) => x !== tok);
|
|
}
|
|
this._write(t);
|
|
}
|
|
toggle(token, force) {
|
|
const tok = String(token);
|
|
if (tok === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
|
|
if (/[ \t\n\f\r]/.test(tok)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
|
|
const t = this._tokens();
|
|
const has = t.includes(tok);
|
|
if (has) {
|
|
if (force === true) return true;
|
|
this._write(t.filter((x) => x !== tok));
|
|
return false;
|
|
}
|
|
if (force === false) return false;
|
|
t.push(tok);
|
|
this._write(t);
|
|
return true;
|
|
}
|
|
replace(token, newToken) {
|
|
const a = String(token), b = String(newToken);
|
|
if (a === "" || b === "") throw new DOMException("The token provided must not be empty.", "SyntaxError");
|
|
if (/[ \t\n\f\r]/.test(a) || /[ \t\n\f\r]/.test(b)) throw new DOMException("The token provided contains HTML space characters, which are not valid in tokens.", "InvalidCharacterError");
|
|
const t = this._tokens();
|
|
const i = t.indexOf(a);
|
|
if (i === -1) return false;
|
|
if (t.includes(b) && b !== a) { t.splice(i, 1); } else { t[i] = b; }
|
|
this._write(t);
|
|
return true;
|
|
}
|
|
supports(token) {
|
|
if (!this._supported) throw new TypeError("DOMTokenList has no supported tokens.");
|
|
return this._supported.includes(String(token).toLowerCase());
|
|
}
|
|
forEach(cb, thisArg) {
|
|
const t = this._tokens();
|
|
for (let i = 0; i < t.length; i++) cb.call(thisArg, t[i], i, this);
|
|
}
|
|
*values() { yield* this._tokens(); }
|
|
*keys() { const t = this._tokens(); for (let i = 0; i < t.length; i++) yield i; }
|
|
*entries() { const t = this._tokens(); for (let i = 0; i < t.length; i++) yield [i, t[i]]; }
|
|
[Symbol.iterator]() { return this._tokens()[Symbol.iterator](); }
|
|
toString() { return this.value; }
|
|
}
|
|
|
|
// CDATASection: a Text-derived node (nodeType 4) used only in XML documents.
|
|
// Extends Text so data/length/textContent/childNodes reuse the working text
|
|
// node machinery; only the type-identifying getters differ.
|
|
class CDATASection extends Text {
|
|
get nodeName() { return "#cdata-section"; }
|
|
get nodeType() { return 4; }
|
|
get nodeValue() { return this.data; }
|
|
set nodeValue(v) { this.data = v; }
|
|
cloneNode() { return new CDATASection(+_dom("create_text_node", this.data)); }
|
|
}
|
|
|
|
// ProcessingInstruction: nodeType 7, nodeName === target. Extends CharacterData
|
|
// and carries a separate target. Backed by a text node so data/nodeValue/
|
|
// textContent/length work without native PI support.
|
|
class ProcessingInstruction extends CharacterData {
|
|
constructor(nid, target) { super(nid); this._target = target; }
|
|
get target() { return this._target; }
|
|
get nodeName() { return this._target; }
|
|
get nodeType() { return 7; }
|
|
get nodeValue() { return this.data; }
|
|
set nodeValue(v) { this.data = v; }
|
|
cloneNode() { return new ProcessingInstruction(+_dom("create_text_node", this.data), this._target); }
|
|
}
|
|
|
|
// Document character encoding (WHATWG canonical name, e.g. "UTF-8", "EUC-JP").
|
|
// Cached per runtime: the encoding is fixed for a document's lifetime and this
|
|
// is read on every <a>/<area> URL-component access, so the UTF-8 common case
|
|
// must reduce to a single cached-boolean read with no op call and no allocation.
|
|
let __docEncoding;
|
|
let __docIsUtf8;
|
|
function _docEncoding() {
|
|
if (__docEncoding === undefined) {
|
|
const e = _domParse("document_encoding");
|
|
__docEncoding = (typeof e === 'string' && e) ? e : 'UTF-8';
|
|
__docIsUtf8 = __docEncoding.toLowerCase() === 'utf-8';
|
|
}
|
|
return __docEncoding;
|
|
}
|
|
function _docIsUtf8() { if (__docIsUtf8 === undefined) _docEncoding(); return __docIsUtf8; }
|
|
// WHATWG "special scheme" check (these get the special-query percent-encode set).
|
|
function _isSpecialScheme(protocol) {
|
|
const s = (protocol || '').replace(/:$/, '').toLowerCase();
|
|
return s === 'http' || s === 'https' || s === 'ws' || s === 'wss' || s === 'ftp' || s === 'file';
|
|
}
|
|
// Apply the WHATWG URL "encoding override": in a legacy (non-UTF-8) document
|
|
// the query of an <a>/<area> href is percent-encoded in the document charset,
|
|
// not UTF-8. The url op already produced a UTF-8-encoded query; recover the
|
|
// original characters (percent-decode + UTF-8) and re-encode them through the
|
|
// document charset. Pure-ASCII queries round-trip unchanged.
|
|
function _applyDocQueryEncoding(u) {
|
|
if (!u || !u.search || u.search.length < 2) return u;
|
|
let decoded;
|
|
try { decoded = decodeURIComponent(u.search.slice(1)); } catch (e) { return u; }
|
|
let reencoded;
|
|
try { reencoded = Deno.core.ops.op_url_encode_query(decoded, _docEncoding(), _isSpecialScheme(u.protocol)); }
|
|
catch (e) { return u; }
|
|
const newSearch = '?' + reencoded;
|
|
if (newSearch === u.search) return u;
|
|
const hashIdx = u.href.indexOf('#');
|
|
const frag = hashIdx >= 0 ? u.href.slice(hashIdx) : '';
|
|
const beforeHash = hashIdx >= 0 ? u.href.slice(0, hashIdx) : u.href;
|
|
const qIdx = beforeHash.indexOf('?');
|
|
u.href = (qIdx >= 0 ? beforeHash.slice(0, qIdx) : beforeHash) + newSearch + frag;
|
|
u.search = newSearch;
|
|
return u;
|
|
}
|
|
|
|
// HTMLHyperlinkElementUtils helpers (the <a>/<area> URL-decomposition members).
|
|
// The element's href attribute is parsed against the document base URL via the
|
|
// WHATWG url op; component getters read it, setters rewrite the href attribute.
|
|
function _anchorBase() { return _domParse("document_url") || "about:blank"; }
|
|
function _elemHrefURL(el) {
|
|
const raw = el.getAttribute('href');
|
|
if (raw === null || raw === undefined) return null;
|
|
const u = _urlParseOp(raw, _anchorBase());
|
|
if (u && !_docIsUtf8()) return _applyDocQueryEncoding(u);
|
|
return u;
|
|
}
|
|
function _setElemHrefPart(el, part, value) {
|
|
const u = _elemHrefURL(el);
|
|
if (!u) return;
|
|
const c = _urlSetOp(u.href, part, value);
|
|
if (c) el.setAttribute('href', c.href);
|
|
}
|
|
|
|
// --- <input> number/date conversion (valueAsNumber/valueAsDate/stepUp/Down) ---
|
|
// Applicable types and their step scale factor + default step (HTML spec).
|
|
const _INPUT_NUM_TYPES = { date: 1, month: 1, week: 1, time: 1, 'datetime-local': 1, number: 1, range: 1 };
|
|
const _INPUT_DATE_TYPES = { date: 1, month: 1, week: 1, time: 1, 'datetime-local': 1 };
|
|
const _INPUT_STEP_SCALE = { date: 86400000, 'datetime-local': 1000, month: 1, number: 1, range: 1, time: 1000, week: 604800000 };
|
|
const _INPUT_STEP_DEFAULT = { date: 1, 'datetime-local': 60, month: 1, number: 1, range: 1, time: 60, week: 1 };
|
|
function _pad(n, w) { n = String(Math.abs(n | 0)); while (n.length < w) n = '0' + n; return n; }
|
|
function _daysInMonth(y, m) { return [31, ((y % 4 === 0 && y % 100 !== 0) || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1]; }
|
|
function _isoWeek1Monday(y) { const jan4 = Date.UTC(y, 0, 4); const dow = (new Date(jan4).getUTCDay() + 6) % 7; return jan4 - dow * 86400000; }
|
|
// Parse an <input> value string to its numeric form per type; NaN if invalid.
|
|
function _inputParseNumber(type, v) {
|
|
v = String(v == null ? '' : v);
|
|
let m;
|
|
switch (type) {
|
|
case 'number': case 'range': { if (v === '') return NaN; const n = Number(v); return isFinite(n) ? n : NaN; }
|
|
case 'date': if ((m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(v))) { const y = +m[1], mo = +m[2], d = +m[3]; if (mo >= 1 && mo <= 12 && d >= 1 && d <= _daysInMonth(y, mo)) return Date.UTC(y, mo - 1, d); } return NaN;
|
|
case 'month': if ((m = /^(\d{4,})-(\d{2})$/.exec(v))) { const y = +m[1], mo = +m[2]; if (mo >= 1 && mo <= 12) return (y - 1970) * 12 + (mo - 1); } return NaN;
|
|
case 'week': if ((m = /^(\d{4,})-W(\d{2})$/.exec(v))) { const y = +m[1], w = +m[2]; if (w >= 1 && w <= 53) return _isoWeek1Monday(y) + (w - 1) * 604800000; } return NaN;
|
|
case 'time': if ((m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/.exec(v))) { const h = +m[1], mi = +m[2], s = m[3] ? +m[3] : 0, ms = m[4] ? +((m[4] + '00').slice(0, 3)) : 0; if (h <= 23 && mi <= 59 && s <= 59) return ((h * 60 + mi) * 60 + s) * 1000 + ms; } return NaN;
|
|
case 'datetime-local': if ((m = /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/.exec(v))) { const y = +m[1], mo = +m[2], d = +m[3], h = +m[4], mi = +m[5], s = m[6] ? +m[6] : 0, ms = m[7] ? +((m[7] + '00').slice(0, 3)) : 0; if (mo >= 1 && mo <= 12 && d >= 1 && d <= _daysInMonth(y, mo) && h <= 23 && mi <= 59 && s <= 59) return Date.UTC(y, mo - 1, d, h, mi, s, ms); } return NaN;
|
|
}
|
|
return NaN;
|
|
}
|
|
// Format a numeric value back to an <input> value string per type.
|
|
function _inputFormatNumber(type, n) {
|
|
switch (type) {
|
|
case 'number': case 'range': return String(n);
|
|
case 'date': { const dt = new Date(n); return _pad(dt.getUTCFullYear(), 4) + '-' + _pad(dt.getUTCMonth() + 1, 2) + '-' + _pad(dt.getUTCDate(), 2); }
|
|
case 'month': { const y = 1970 + Math.floor(n / 12); const mo = ((n % 12) + 12) % 12 + 1; return _pad(y, 4) + '-' + _pad(mo, 2); }
|
|
case 'week': { const d = new Date(n); const dow = (d.getUTCDay() + 6) % 7; const thu = n - dow * 86400000 + 3 * 86400000; const ty = new Date(thu).getUTCFullYear(); const w = Math.round((n - dow * 86400000 - _isoWeek1Monday(ty)) / 604800000) + 1; return _pad(ty, 4) + '-W' + _pad(w, 2); }
|
|
case 'time': { n = ((n % 86400000) + 86400000) % 86400000; const ms = n % 1000; n = Math.floor(n / 1000); const s = n % 60; n = Math.floor(n / 60); const mi = n % 60; const h = Math.floor(n / 60); let str = _pad(h, 2) + ':' + _pad(mi, 2); if (s || ms) { str += ':' + _pad(s, 2); if (ms) str += '.' + _pad(ms, 3); } return str; }
|
|
case 'datetime-local': { const dt = new Date(n); let str = _pad(dt.getUTCFullYear(), 4) + '-' + _pad(dt.getUTCMonth() + 1, 2) + '-' + _pad(dt.getUTCDate(), 2) + 'T' + _pad(dt.getUTCHours(), 2) + ':' + _pad(dt.getUTCMinutes(), 2); const s = dt.getUTCSeconds(), ms = dt.getUTCMilliseconds(); if (s || ms) { str += ':' + _pad(s, 2); if (ms) str += '.' + _pad(ms, 3); } return str; }
|
|
}
|
|
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;
|
|
}
|
|
|
|
// A submit button per the HTML spec: a <button> whose type is submit — the
|
|
// default, including when the type attribute is missing or invalid — or an
|
|
// <input> of type submit/image. Used to validate requestSubmit's submitter.
|
|
function _isSubmitButton(el) {
|
|
if (!el || typeof el.localName !== "string") return false;
|
|
const type = ((el.getAttribute && el.getAttribute("type")) || "").toLowerCase();
|
|
if (el.localName === "button") return type !== "reset" && type !== "button";
|
|
if (el.localName === "input") return type === "submit" || type === "image";
|
|
return false;
|
|
}
|
|
|
|
// Parse an HTML string into detached nodes using the actual insertion element
|
|
// as html5ever's fragment context. This preserves table/select parsing rules,
|
|
// comments, text-node order, and foreign-content namespaces without a wrap map.
|
|
function _parseHTMLFragment(html, context) {
|
|
html = String(html == null ? '' : html);
|
|
const ns = context && context.nodeType === 1 ? context.namespaceURI : null;
|
|
const tag = context && context.nodeType === 1 ? context.localName : 'body';
|
|
const tmp = ns && ns !== 'http://www.w3.org/1999/xhtml'
|
|
? document.createElementNS(ns, tag)
|
|
: document.createElement(tag);
|
|
tmp.innerHTML = html;
|
|
const out = [];
|
|
let child;
|
|
while ((child = tmp.firstChild)) out.push(tmp.removeChild(child));
|
|
return out;
|
|
}
|
|
|
|
class Element extends Node {
|
|
constructor(nid) {
|
|
super(nid);
|
|
this._style = _styleProxy(new CSSStyleDeclaration(this));
|
|
}
|
|
// Element wrappers always back a nodeType-1 node (_wrap/_wrapEl only build an
|
|
// Element for element nodes, and node ids are never freed-and-reused), so this
|
|
// is constant. Overrides Node's dynamic getter to drop one op per nodeType read.
|
|
get nodeType() { return 1; }
|
|
get tagName() { return _domParse("tag_name", this._nid) || ""; }
|
|
get localName() {
|
|
// tagName is an op call and the tag never changes, so cache the lowercased
|
|
// localName. This keeps the new <a>/<area> href getters (which read
|
|
// localName) and every other localName consumer off the op path.
|
|
if (this._lname !== undefined) return this._lname;
|
|
const ln = (this.tagName || "").toLowerCase();
|
|
if (ln) this._lname = ln;
|
|
return ln;
|
|
}
|
|
get id() { return this.getAttribute("id") || ""; }
|
|
set id(v) { this.setAttribute("id", v); }
|
|
get className() {
|
|
// SVG elements reflect class as an SVGAnimatedString (.baseVal/.animVal),
|
|
// not a plain string. Anti-fraud sensors read el.className.animVal.
|
|
if (this.namespaceURI === "http://www.w3.org/2000/svg") {
|
|
if (!this._svgClassName) this._svgClassName = new SVGAnimatedString(this, "class");
|
|
return this._svgClassName;
|
|
}
|
|
return this.getAttribute("class") || "";
|
|
}
|
|
set className(v) { this.setAttribute("class", v); }
|
|
get namespaceURI() {
|
|
// createElementNS records the requested namespace on _ns; an empty string
|
|
// maps to the null namespace per spec.
|
|
if (this._ns !== undefined) return this._ns === "" ? null : this._ns;
|
|
// Otherwise use the namespace the HTML tree builder assigned. Foreign
|
|
// content puts the WHOLE <svg>/<math> subtree in that namespace, not just
|
|
// the root, so deriving it from the tag name (the old `localName === "svg"`
|
|
// check) left every descendant looking like HTML and skipped the SVG-only
|
|
// reflections -- notably `get href()`, which then returned a plain string
|
|
// instead of an SVGAnimatedString. An element's namespace never changes,
|
|
// so cache it like _lname.
|
|
if (this._nsCache !== undefined) return this._nsCache;
|
|
let ns = _domParse("namespace_uri", this._nid) || "";
|
|
// Nodes with no element name recorded fall back to the previous heuristic.
|
|
if (!ns) ns = this.localName === "svg" ? "http://www.w3.org/2000/svg" : "http://www.w3.org/1999/xhtml";
|
|
this._nsCache = ns;
|
|
return ns;
|
|
}
|
|
// `inner_html` resolves a <template> to its contents document on the Rust
|
|
// side (issue #463), so this needs no template special case.
|
|
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
|
|
set innerHTML(v) {
|
|
if (this.localName === 'template') {
|
|
this.content.innerHTML = v;
|
|
return;
|
|
}
|
|
// Capture the children that are about to be replaced so we can deliver
|
|
// them as `removedNodes` in the MutationObserver record. Without this,
|
|
// libraries that mutate via `innerHTML =` (jQuery's `.html(s)`, React
|
|
// `dangerouslySetInnerHTML`, vue-style content swaps) silently bypass
|
|
// every MutationObserver subscriber and downstream hydration / polling
|
|
// logic stalls.
|
|
let oldChildren = [];
|
|
let newChildren = [];
|
|
if (globalThis.__mutationObservers?.length) {
|
|
oldChildren = _domParse("child_nodes", this._nid) || [];
|
|
}
|
|
_dom("set_inner_html", this._nid, String(v ?? ""));
|
|
if (globalThis.__mutationObservers?.length) {
|
|
newChildren = _domParse("child_nodes", this._nid) || [];
|
|
globalThis.__notifyMutation('childList', this._nid, newChildren, oldChildren);
|
|
}
|
|
}
|
|
get outerHTML() { return _domParse("outer_html", this._nid) ?? ""; }
|
|
get innerText() { return this.textContent; }
|
|
set innerText(v) { this.textContent = v; }
|
|
get children() {
|
|
const ids = _domParse("element_children", this._nid) || [];
|
|
return HTMLCollection._from(ids.map(_wrapEl).filter(Boolean));
|
|
}
|
|
get content() {
|
|
// <template>.content is a DocumentFragment; <meta>.content reflects
|
|
// the content attribute (read/write per spec). Next.js' next/head
|
|
// iterates <meta> tags and sets .content during hydration, which
|
|
// threw with the previous getter-only stub and put React into an
|
|
// infinite retry loop (issue #210).
|
|
const tag = this.localName;
|
|
if (tag === 'template') {
|
|
// Back the fragment with the node's real template contents (issue #463).
|
|
// The parser stores template children in a separate contents document
|
|
// instead of under the element, so without this the getter handed back a
|
|
// fabricated empty fragment and the parsed markup was unreachable.
|
|
// `template_contents` allocates one on demand for created templates.
|
|
const nid = +_dom("template_contents", this._nid);
|
|
if (nid >= 0) {
|
|
// Cache by node id so `.content` keeps a stable identity across reads —
|
|
// frameworks stash the fragment and compare it later.
|
|
if (!_cache.has(nid)) _cache.set(nid, new DocumentFragment(nid));
|
|
return _cache.get(nid);
|
|
}
|
|
if (!this._templateContent) this._templateContent = document.createDocumentFragment();
|
|
return this._templateContent;
|
|
}
|
|
if (tag === 'meta') return this.getAttribute('content') || '';
|
|
return undefined;
|
|
}
|
|
set content(v) {
|
|
if (this.localName === 'meta') {
|
|
this.setAttribute('content', v == null ? '' : String(v));
|
|
}
|
|
}
|
|
get childElementCount() { return this.children.length; }
|
|
get firstElementChild() { return this.children[0] || null; }
|
|
get lastElementChild() { const ch = this.children; return ch[ch.length-1] || null; }
|
|
get nextElementSibling() { let s = this.nextSibling; while(s && s.nodeType !== 1) s = s.nextSibling; return s; }
|
|
get previousElementSibling() { let s = this.previousSibling; while(s && s.nodeType !== 1) s = s.previousSibling; return s; }
|
|
get classList() {
|
|
if (!this._classList) this._classList = new DOMTokenList(this, "class");
|
|
return this._classList;
|
|
}
|
|
get relList() {
|
|
const ns = this.namespaceURI, ln = this.localName;
|
|
const ok = (ns === "http://www.w3.org/2000/svg" && ln === "a") ||
|
|
(ns === "http://www.w3.org/1999/xhtml" && (ln === "a" || ln === "area" || ln === "link"));
|
|
if (!ok) return undefined;
|
|
// relList has supported tokens, so relList.supports(x) returns a boolean
|
|
// rather than throwing. Vite's modulepreload polyfill runs
|
|
// link.relList.supports('modulepreload') at the top of every bundle; a
|
|
// throw there aborts the whole module and the SPA renders blank.
|
|
if (!this._relList) this._relList = new DOMTokenList(this, "rel", ["alternate","dns-prefetch","icon","manifest","modulepreload","next","pingback","preconnect","prefetch","preload","prev","search","stylesheet"]);
|
|
return this._relList;
|
|
}
|
|
get sandbox() {
|
|
if (this.namespaceURI !== "http://www.w3.org/1999/xhtml" || this.localName !== "iframe") return undefined;
|
|
if (!this._sandboxList) this._sandboxList = new DOMTokenList(this, "sandbox", ["allow-downloads","allow-forms","allow-modals","allow-orientation-lock","allow-pointer-lock","allow-popups","allow-popups-to-escape-sandbox","allow-presentation","allow-same-origin","allow-scripts","allow-top-navigation","allow-top-navigation-by-user-activation","allow-top-navigation-to-custom-protocols"]);
|
|
return this._sandboxList;
|
|
}
|
|
get sizes() {
|
|
if (this.namespaceURI !== "http://www.w3.org/1999/xhtml" || this.localName !== "link") return undefined;
|
|
if (!this._sizesList) this._sizesList = new DOMTokenList(this, "sizes");
|
|
return this._sizesList;
|
|
}
|
|
get htmlFor() {
|
|
if (this.namespaceURI !== "http://www.w3.org/1999/xhtml") return undefined;
|
|
const ln = this.localName;
|
|
if (ln === "output") {
|
|
if (!this._htmlForList) this._htmlForList = new DOMTokenList(this, "for");
|
|
return this._htmlForList;
|
|
}
|
|
if (ln === "label") return this.getAttribute("for") || "";
|
|
return undefined;
|
|
}
|
|
set htmlFor(v) {
|
|
if (this.namespaceURI === "http://www.w3.org/1999/xhtml" && this.localName === "label") {
|
|
this.setAttribute("for", String(v));
|
|
}
|
|
}
|
|
get style() { return this._style; }
|
|
set style(v) { if (typeof v === "string") this._style.cssText = v; }
|
|
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) {
|
|
n = _htmlAttrName(this, n);
|
|
const popoverPrev = (n === "popover") ? this.popover : undefined;
|
|
const value = String(v);
|
|
_dom("set_attribute", this._nid, n + "\0" + value);
|
|
if (n === "style") this._style._replaceFromAttribute(value);
|
|
if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev);
|
|
if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('attributes', this._nid, [], [], n);
|
|
}
|
|
setAttributeNS(ns, n, v) {
|
|
ns = ns == null || ns === '' ? '' : String(ns);
|
|
n = String(n);
|
|
const value = String(v);
|
|
_ns_validateQualifiedName(ns, n);
|
|
_dom("set_attribute_ns", this._nid, ns + "\0" + n + "\0" + value);
|
|
if (ns === "" && n === "style") this._style._replaceFromAttribute(value);
|
|
}
|
|
removeAttribute(n) { n = _htmlAttrName(this, n); const popoverPrev = (n === "popover") ? this.popover : undefined; _dom("remove_attribute", this._nid, n); if (n === "style") this._style._replaceFromAttribute(""); if (popoverPrev !== undefined) this._popoverTypeMaybeChanged(popoverPrev); }
|
|
removeAttributeNS(ns, n) {
|
|
ns = String(ns == null ? "" : ns);
|
|
n = String(n);
|
|
_dom("remove_attribute_ns", this._nid, ns + "\0" + n);
|
|
if (ns === "" && n === "style") this._style._replaceFromAttribute("");
|
|
}
|
|
hasAttribute(n) { return this.getAttribute(n) !== null; }
|
|
hasAttributes() { return true; } // Simplified
|
|
getAttributeNames() { return _domParse("attribute_names", this._nid) || []; }
|
|
get attributes() {
|
|
const el = this;
|
|
const names = _domParse("attribute_names", el._nid) || [];
|
|
const list = names.map((name) => {
|
|
const v = el.getAttribute(name) ?? "";
|
|
return {
|
|
name,
|
|
localName: name,
|
|
value: v,
|
|
namespaceURI: null,
|
|
prefix: null,
|
|
specified: true,
|
|
ownerElement: el,
|
|
nodeName: name,
|
|
nodeValue: v,
|
|
nodeType: 2,
|
|
};
|
|
});
|
|
list.length = names.length;
|
|
list.getNamedItem = (n) => names.includes(n) ? list[names.indexOf(n)] : null;
|
|
list.setNamedItem = (a) => { if (a && a.name) el.setAttribute(a.name, a.value); return a; };
|
|
list.removeNamedItem = (n) => { const a = list.getNamedItem(n); if (a) el.removeAttribute(n); return a; };
|
|
list.item = (i) => list[i] || null;
|
|
for (let i = 0; i < names.length; i++) {
|
|
Object.defineProperty(list, names[i], { value: list[i], configurable: true, enumerable: false });
|
|
}
|
|
return list;
|
|
}
|
|
getAttributeNS(ns, n) { return _domParse("get_attribute_ns", this._nid, String(ns == null ? "" : ns) + "\0" + String(n)); }
|
|
querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); }
|
|
querySelectorAll(s) {
|
|
const ids = _domParse("query_selector_all_scoped", this._nid, s) || [];
|
|
return _nodeList(ids.map(_wrapEl).filter(Boolean));
|
|
}
|
|
getElementsByTagName(t) { return HTMLCollection._from(this.querySelectorAll(t)); }
|
|
getElementsByClassName(c) { return _getElementsByClassName(this, c); }
|
|
matches(s) {
|
|
// :popover-open is a JS-observable popover state, not understood by the
|
|
// native selector engine. Handle it here (and strip it from compound
|
|
// selectors so the rest can still be matched natively).
|
|
if (typeof s === "string" && s.indexOf(":popover-open") !== -1) {
|
|
if (this._popoverState !== "showing") return false;
|
|
const rest = s.replace(/:popover-open/g, "").trim();
|
|
if (rest === "") return true;
|
|
return this.matches(rest);
|
|
}
|
|
// :modal is a JS-observable dialog state (a dialog opened via showModal()),
|
|
// not understood by the native selector engine; handle it like :popover-open.
|
|
if (typeof s === "string" && s.indexOf(":modal") !== -1) {
|
|
if (this._dialogModal !== true) return false;
|
|
const rest = s.replace(/:modal/g, "").trim();
|
|
if (rest === "") return true;
|
|
return this.matches(rest);
|
|
}
|
|
const parent = this.parentNode;
|
|
if (!parent || !parent.querySelectorAll) return false;
|
|
const matches = parent.querySelectorAll(s);
|
|
for (let i = 0; i < matches.length; i++) {
|
|
if (matches[i]._nid === this._nid) return true;
|
|
}
|
|
return false;
|
|
}
|
|
closest(s) {
|
|
let el = this;
|
|
while (el) {
|
|
if (el.nodeType === 1 && el.matches && el.matches(s)) return el;
|
|
el = el.parentNode;
|
|
}
|
|
return null;
|
|
}
|
|
insertAdjacentHTML(position, html) {
|
|
// Position is matched ASCII-case-insensitively; an unknown value throws
|
|
// SyntaxError (both were silent no-ops before). Sibling insertions parse
|
|
// against the parent's context, child insertions against this element, so
|
|
// table/select fragments keep the right parsing context (_parseHTMLFragment).
|
|
const pos = String(position).toLowerCase();
|
|
const parent = this.parentNode;
|
|
const context = (pos === 'beforebegin' || pos === 'afterend') ? parent : this;
|
|
switch (pos) {
|
|
case 'beforebegin':
|
|
if (parent) for (const n of _parseHTMLFragment(html, context)) parent.insertBefore(n, this);
|
|
break;
|
|
case 'afterbegin': {
|
|
const first = this.firstChild;
|
|
for (const n of _parseHTMLFragment(html, context)) this.insertBefore(n, first);
|
|
break;
|
|
}
|
|
case 'beforeend':
|
|
for (const n of _parseHTMLFragment(html, context)) this.appendChild(n);
|
|
break;
|
|
case 'afterend':
|
|
if (parent) { const next = this.nextSibling; for (const n of _parseHTMLFragment(html, context)) parent.insertBefore(n, next); }
|
|
break;
|
|
default:
|
|
throw new DOMException(
|
|
"Failed to execute 'insertAdjacentHTML' on 'Element': The value provided ('" + position + "') is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'.",
|
|
"SyntaxError"
|
|
);
|
|
}
|
|
}
|
|
// Like insertAdjacentHTML but inserts a Text node instead of parsing markup,
|
|
// so the content stays literal.
|
|
insertAdjacentText(position, text) {
|
|
const parent = this.parentNode;
|
|
const node = document.createTextNode(String(text));
|
|
switch (String(position).toLowerCase()) {
|
|
case 'beforebegin':
|
|
if (parent) parent.insertBefore(node, this);
|
|
break;
|
|
case 'afterbegin':
|
|
this.insertBefore(node, this.firstChild);
|
|
break;
|
|
case 'beforeend':
|
|
this.appendChild(node);
|
|
break;
|
|
case 'afterend':
|
|
if (parent) parent.insertBefore(node, this.nextSibling);
|
|
break;
|
|
}
|
|
}
|
|
// Returns the inserted element, or null for beforebegin/afterend when this
|
|
// element has no parent.
|
|
insertAdjacentElement(position, element) {
|
|
const parent = this.parentNode;
|
|
switch (String(position).toLowerCase()) {
|
|
case 'beforebegin':
|
|
if (!parent) return null;
|
|
parent.insertBefore(element, this);
|
|
return element;
|
|
case 'afterbegin':
|
|
this.insertBefore(element, this.firstChild);
|
|
return element;
|
|
case 'beforeend':
|
|
this.appendChild(element);
|
|
return element;
|
|
case 'afterend':
|
|
if (!parent) return null;
|
|
parent.insertBefore(element, this.nextSibling);
|
|
return element;
|
|
}
|
|
return null;
|
|
}
|
|
addEventListener(type, handler, opts) {
|
|
const key = this._nid;
|
|
if (!_eventRegistry[key]) _eventRegistry[key] = {};
|
|
if (!_eventRegistry[key][type]) _eventRegistry[key][type] = [];
|
|
_eventRegistry[key][type].push(handler);
|
|
}
|
|
removeEventListener(type, handler) {
|
|
const key = this._nid;
|
|
if (_eventRegistry[key] && _eventRegistry[key][type]) {
|
|
_eventRegistry[key][type] = _eventRegistry[key][type].filter(h => h !== handler);
|
|
}
|
|
}
|
|
dispatchEvent(event) {
|
|
if (!event) return true;
|
|
if (!event.target) event.target = this;
|
|
event.currentTarget = this;
|
|
// Spec: inline `onclick="..."` content attributes are event handlers
|
|
// for the matching event type. Fire them alongside any
|
|
// addEventListener handlers. Also honor the IDL property
|
|
// `el.onclick = fn` if set. Without this, b.click() never invokes
|
|
// the inline handler and forms with onsubmit / buttons with onclick
|
|
// are silently dead.
|
|
const handlerName = 'on' + event.type;
|
|
const inlineFn = this[handlerName] || this._resolveInlineHandler(handlerName);
|
|
if (typeof inlineFn === 'function') {
|
|
try {
|
|
const ret = inlineFn.call(this, event);
|
|
if (ret === false) event.preventDefault();
|
|
} catch(e) { console.error(e); }
|
|
}
|
|
const handlers = (_eventRegistry[this._nid] || {})[event.type] || [];
|
|
for (const h of handlers) {
|
|
try { h.call(this, event); } catch(e) { console.error(e); }
|
|
if (event._immediatePropagationStopped) break;
|
|
}
|
|
if (event.bubbles && !event._propagationStopped && this.parentNode) {
|
|
this.parentNode.dispatchEvent(event);
|
|
}
|
|
return !event.defaultPrevented;
|
|
}
|
|
_resolveInlineHandler(name) {
|
|
// name = 'onclick' / 'onsubmit' / etc. Compile the content attribute
|
|
// as a function body on first read and cache it on the instance.
|
|
const cache = this.__inlineHandlerCache || (this.__inlineHandlerCache = {});
|
|
if (Object.prototype.hasOwnProperty.call(cache, name)) return cache[name];
|
|
const src = this.getAttribute && this.getAttribute(name);
|
|
if (!src) { cache[name] = null; return null; }
|
|
try {
|
|
cache[name] = new Function('event', src);
|
|
} catch (e) {
|
|
cache[name] = null;
|
|
}
|
|
return cache[name];
|
|
}
|
|
click() {
|
|
const cancelled = !this.dispatchEvent(new MouseEvent("click", {bubbles: true, cancelable: true}));
|
|
if (!cancelled) {
|
|
const link = this.tagName === 'A' ? this : (this.closest ? this.closest('a[href]') : null);
|
|
if (link) {
|
|
const href = link.getAttribute('href');
|
|
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
location.assign(href);
|
|
return;
|
|
}
|
|
}
|
|
// Same predicate requestSubmit validates against, so an internal click
|
|
// can never hand it a submitter it would reject. Also matches the CDP
|
|
// click path in input.rs, which already treats <input type=image> as a
|
|
// submit button.
|
|
if (_isSubmitButton(this)) {
|
|
const form = this.closest ? this.closest('form') : null;
|
|
// A real submit-button click fires the cancelable submit event, so use
|
|
// requestSubmit() (not the plain submit() method, which now bypasses it).
|
|
if (form && typeof form.requestSubmit === 'function') {
|
|
form.requestSubmit(this);
|
|
} else if (form && typeof form.submit === 'function') {
|
|
form.submit(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
focus() { globalThis.__obscura_focused = this; globalThis.__obscura_click_target = this; }
|
|
blur() { if (globalThis.__obscura_focused === this) globalThis.__obscura_focused = null; }
|
|
|
|
// --- Popover API (HTML "popover") ---------------------------------------
|
|
// Read the popover content attribute case-insensitively. The HTML parser
|
|
// lowercases attribute names, but runtime setAttribute("PoPoVeR", ...)
|
|
// preserves case, and the IDL reflection matches the name ASCII-case-
|
|
// insensitively. Returns the raw stored string, or null if absent.
|
|
_popoverAttrValue() {
|
|
const v = this.getAttribute("popover");
|
|
if (v !== null) return v;
|
|
const names = _domParse("attribute_names", this._nid) || [];
|
|
for (let i = 0; i < names.length; i++) {
|
|
if (names[i].toLowerCase() === "popover") return this.getAttribute(names[i]);
|
|
}
|
|
return null;
|
|
}
|
|
// The reflected (effective) popover type: null (No Popover), "auto",
|
|
// "hint", or "manual". Empty string maps to "auto"; any non-keyword value
|
|
// (invalid) maps to "manual".
|
|
get popover() {
|
|
const raw = this._popoverAttrValue();
|
|
if (raw === null) return null;
|
|
const v = String(raw).toLowerCase();
|
|
if (v === "auto" || v === "hint" || v === "manual") return v;
|
|
if (v === "") return "auto";
|
|
return "manual";
|
|
}
|
|
set popover(value) {
|
|
if (value === null || value === undefined) { this._popoverRemoveAttr(); return; }
|
|
this.setAttribute("popover", String(value));
|
|
}
|
|
_popoverRemoveAttr() {
|
|
if (this.getAttribute("popover") !== null) { this.removeAttribute("popover"); return; }
|
|
const names = _domParse("attribute_names", this._nid) || [];
|
|
for (let i = 0; i < names.length; i++) {
|
|
if (names[i].toLowerCase() === "popover") { this.removeAttribute(names[i]); return; }
|
|
}
|
|
}
|
|
// "check popover validity". expectedToBeShowing is true for hide, false for
|
|
// show. Throws NotSupportedError when there is no valid popover type, and
|
|
// InvalidStateError when the element is not connected; returns false (no
|
|
// throw) when the current state does not match expectedToBeShowing.
|
|
_checkPopoverValidity(expectedToBeShowing) {
|
|
if (this.popover === null) throw new DOMException("Not supported on elements that don't have a valid value for the popover attribute", "NotSupportedError");
|
|
const showing = this._popoverState === "showing";
|
|
if ((expectedToBeShowing && !showing) || (!expectedToBeShowing && showing)) return false;
|
|
if (!this.isConnected) throw new DOMException("Invalid on popover elements which aren't connected", "InvalidStateError");
|
|
return true;
|
|
}
|
|
showPopover() {
|
|
if (!this._checkPopoverValidity(/*expectedToBeShowing*/false)) return;
|
|
const beforeEvent = new ToggleEvent("beforetoggle", { cancelable: true, oldState: "closed", newState: "open" });
|
|
if (!this.dispatchEvent(beforeEvent)) return;
|
|
// The beforetoggle handler may have changed our type or shown us; re-check.
|
|
if (!this._checkPopoverValidity(/*expectedToBeShowing*/false)) return;
|
|
this._popoverState = "showing";
|
|
const target = this;
|
|
setTimeout(() => { try { target.dispatchEvent(new ToggleEvent("toggle", { oldState: "closed", newState: "open" })); } catch (e) {} }, 0);
|
|
}
|
|
hidePopover() {
|
|
if (!this._checkPopoverValidity(/*expectedToBeShowing*/true)) return;
|
|
this.dispatchEvent(new ToggleEvent("beforetoggle", { oldState: "open", newState: "closed" }));
|
|
this._popoverState = "hidden";
|
|
const target = this;
|
|
setTimeout(() => { try { target.dispatchEvent(new ToggleEvent("toggle", { oldState: "open", newState: "closed" })); } catch (e) {} }, 0);
|
|
}
|
|
togglePopover(force) {
|
|
let options = force;
|
|
if (options && typeof options === "object") force = options.force;
|
|
const showing = this._popoverState === "showing";
|
|
if (showing && (force === undefined || force === null || force === false)) {
|
|
this.hidePopover();
|
|
} else if (force === undefined || force === null || force === true) {
|
|
this.showPopover();
|
|
}
|
|
return this._popoverState === "showing";
|
|
}
|
|
// Called from setAttribute/removeAttribute/IDL setter when the popover
|
|
// attribute may have changed. If the effective type changed while showing,
|
|
// hide the popover (firing the hide events) per the HTML spec.
|
|
_popoverTypeMaybeChanged(prevType) {
|
|
const newType = this.popover;
|
|
if (this._popoverState === "showing" && prevType !== newType) {
|
|
// Hide directly. Do not call hidePopover(): it re-validates against the
|
|
// popover attribute, which may now be removed (No Popover), and would
|
|
// throw NotSupportedError. This mirrors the spec hide with throw=false.
|
|
this.dispatchEvent(new ToggleEvent("beforetoggle", { oldState: "open", newState: "closed" }));
|
|
this._popoverState = "hidden";
|
|
const target = this;
|
|
setTimeout(() => { try { target.dispatchEvent(new ToggleEvent("toggle", { oldState: "open", newState: "closed" })); } catch (e) {} }, 0);
|
|
}
|
|
}
|
|
// HTMLDialogElement members (live on Element.prototype like popover/input;
|
|
// meaningful only when localName === 'dialog'). Modal top-layer/focus/render
|
|
// is layout (out of scope); the open state, returnValue, and beforetoggle/
|
|
// toggle/close/cancel events are JS-observable and implemented here.
|
|
get open() { return this.hasAttribute('open'); }
|
|
set open(v) { if (v) { if (!this.hasAttribute('open')) this.setAttribute('open', ''); } else if (this.hasAttribute('open')) { this.removeAttribute('open'); this._dialogModal = false; } }
|
|
get returnValue() { return this._returnValue != null ? this._returnValue : ''; }
|
|
set returnValue(v) { this._returnValue = String(v); }
|
|
get oncancel() { return this._oncancel || null; }
|
|
set oncancel(f) { this._oncancel = typeof f === 'function' ? f : null; }
|
|
get onclose() { return this._onclose || null; }
|
|
set onclose(f) { this._onclose = typeof f === 'function' ? f : null; }
|
|
get closedBy() { const v = (this.getAttribute('closedby') || '').toLowerCase(); return (v === 'any' || v === 'closerequest' || v === 'none') ? v : 'auto'; }
|
|
set closedBy(v) { this.setAttribute('closedby', String(v)); }
|
|
show() {
|
|
if (this.hasAttribute('open')) { if (this._dialogModal) throw new DOMException("The dialog is already open as a modal dialog.", "InvalidStateError"); return; }
|
|
const before = new ToggleEvent("beforetoggle", { cancelable: true, oldState: "closed", newState: "open" });
|
|
if (!this.dispatchEvent(before)) return;
|
|
if (this.hasAttribute('open')) return;
|
|
this.setAttribute('open', ''); this._dialogModal = false;
|
|
const self = this; setTimeout(() => { try { self.dispatchEvent(new ToggleEvent("toggle", { oldState: "closed", newState: "open" })); } catch (e) {} }, 0);
|
|
}
|
|
showModal() {
|
|
if (this.hasAttribute('open')) throw new DOMException("The dialog is already open.", "InvalidStateError");
|
|
if (!this.isConnected) throw new DOMException("The dialog is not connected to a document.", "InvalidStateError");
|
|
const before = new ToggleEvent("beforetoggle", { cancelable: true, oldState: "closed", newState: "open" });
|
|
if (!this.dispatchEvent(before)) return;
|
|
if (this.hasAttribute('open')) return;
|
|
this.setAttribute('open', ''); this._dialogModal = true;
|
|
const self = this; setTimeout(() => { try { self.dispatchEvent(new ToggleEvent("toggle", { oldState: "closed", newState: "open" })); } catch (e) {} }, 0);
|
|
}
|
|
_dialogClose(result, fireClose) {
|
|
if (!this.hasAttribute('open')) return;
|
|
this.dispatchEvent(new ToggleEvent("beforetoggle", { oldState: "open", newState: "closed" }));
|
|
this.removeAttribute('open'); this._dialogModal = false;
|
|
if (result !== undefined) this._returnValue = String(result);
|
|
const self = this;
|
|
setTimeout(() => { try { self.dispatchEvent(new ToggleEvent("toggle", { oldState: "open", newState: "closed" })); } catch (e) {} }, 0);
|
|
if (fireClose) setTimeout(() => { try { self.dispatchEvent(new Event('close', { bubbles: false, cancelable: false })); } catch (e) {} }, 0);
|
|
}
|
|
close(result) { this._dialogClose(result, true); }
|
|
requestClose(result) {
|
|
if (!this.hasAttribute('open')) return;
|
|
if (this._dialogCancelFiring) return; // no re-entrant cancel
|
|
this._dialogCancelFiring = true;
|
|
let canceled = false;
|
|
try { const ev = new Event('cancel', { bubbles: false, cancelable: true }); this.dispatchEvent(ev); canceled = ev.defaultPrevented; }
|
|
finally { this._dialogCancelFiring = false; }
|
|
if (canceled) return;
|
|
this._dialogClose(result, true);
|
|
}
|
|
attachInternals() {
|
|
const reg = (typeof customElements !== 'undefined' && customElements._registry) ? customElements._registry : null;
|
|
if (!reg || !reg.get(this.localName)) throw new DOMException("Failed to execute 'attachInternals' on 'HTMLElement': Unable to attach ElementInternals to non-custom elements.", "NotSupportedError");
|
|
if (this.getAttribute('is')) throw new DOMException("Failed to execute 'attachInternals' on 'HTMLElement': Unable to attach ElementInternals to a customized built-in element.", "NotSupportedError");
|
|
if (this._internalsAttached) throw new DOMException("Failed to execute 'attachInternals' on 'HTMLElement': ElementInternals for the specified element was already attached.", "NotSupportedError");
|
|
this._internalsAttached = true;
|
|
return new ElementInternals(this);
|
|
}
|
|
get value() {
|
|
const tag = this.localName;
|
|
if (tag === 'select') {
|
|
// Selected option wins; otherwise first option (HTML default).
|
|
const opts = this.querySelectorAll('option');
|
|
for (let i = 0; i < opts.length; i++) {
|
|
if (opts[i].selected) {
|
|
return opts[i].getAttribute('value') !== null ? opts[i].getAttribute('value') : opts[i].textContent;
|
|
}
|
|
}
|
|
if (opts.length) return opts[0].getAttribute('value') !== null ? opts[0].getAttribute('value') : opts[0].textContent;
|
|
return '';
|
|
}
|
|
if (_formValues[this._nid] !== undefined) return _formValues[this._nid];
|
|
if (tag === 'textarea') return this.textContent;
|
|
if (tag === 'option') {
|
|
const attr = this.getAttribute('value');
|
|
return attr !== null ? attr : this.textContent;
|
|
}
|
|
if (tag === 'input') {
|
|
const itype = (this.getAttribute('type') || '').toLowerCase();
|
|
if (itype === 'checkbox' || itype === 'radio') {
|
|
// A checkbox/radio with no value attribute defaults to "on" in a real
|
|
// browser, not the empty string.
|
|
const attr = this.getAttribute('value');
|
|
return attr !== null ? attr : 'on';
|
|
}
|
|
if (itype === 'file') {
|
|
// Chrome exposes a file input's value as C:\fakepath\<first filename>.
|
|
return (this._files && this._files.length) ? ('C:\\fakepath\\' + this._files[0].name) : '';
|
|
}
|
|
}
|
|
return this.getAttribute("value") || "";
|
|
}
|
|
// FileList for <input type=file>, populated by DOM.setFileInputFiles (Puppeteer
|
|
// uploadFile / Playwright setInputFiles). null for non-file inputs, matching
|
|
// the DOM. See __obscura_setInputFiles (issue #359).
|
|
get files() {
|
|
if (this.localName !== 'input') return undefined;
|
|
if ((this.getAttribute('type') || '').toLowerCase() !== 'file') return null;
|
|
return this._files || _emptyFileList();
|
|
}
|
|
set value(v) {
|
|
const tag = this.localName;
|
|
if (tag === 'select') {
|
|
// Set selected on matching option, clear on others. Puppeteer's
|
|
// page.select(selector, value) round-trips through this setter.
|
|
const wanted = String(v);
|
|
const opts = this.querySelectorAll('option');
|
|
let matched = false;
|
|
for (let i = 0; i < opts.length; i++) {
|
|
const attrV = opts[i].getAttribute('value');
|
|
const optVal = attrV !== null ? attrV : opts[i].textContent;
|
|
if (optVal === wanted) { opts[i].selected = true; matched = true; }
|
|
else { opts[i].selected = false; }
|
|
}
|
|
if (matched) try { this.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
|
|
return;
|
|
}
|
|
_formValues[this._nid] = String(v);
|
|
if (tag === 'textarea') {
|
|
this.textContent = String(v);
|
|
}
|
|
}
|
|
get min() { return this.getAttribute('min') || ''; }
|
|
set min(v) { this.setAttribute('min', v); }
|
|
get max() { return this.getAttribute('max') || ''; }
|
|
set max(v) { this.setAttribute('max', v); }
|
|
get step() { return this.getAttribute('step') || ''; }
|
|
set step(v) { this.setAttribute('step', v); }
|
|
_inputType() { return this.localName === 'input' ? (this.getAttribute('type') || 'text').toLowerCase() : ''; }
|
|
get valueAsNumber() {
|
|
const t = this._inputType();
|
|
if (!_INPUT_NUM_TYPES[t]) return NaN;
|
|
if (t === 'range') {
|
|
let minN = _inputParseNumber('range', this.getAttribute('min')); if (isNaN(minN)) minN = 0;
|
|
let maxN = _inputParseNumber('range', this.getAttribute('max')); if (isNaN(maxN)) maxN = 100;
|
|
if (maxN < minN) maxN = minN;
|
|
const v = _inputParseNumber('range', this.value);
|
|
let n = isNaN(v) ? (minN + (maxN - minN) / 2) : v;
|
|
if (n < minN) n = minN; if (n > maxN) n = maxN;
|
|
return n;
|
|
}
|
|
return _inputParseNumber(t, this.value);
|
|
}
|
|
set valueAsNumber(n) {
|
|
const t = this._inputType();
|
|
if (!_INPUT_NUM_TYPES[t]) throw new DOMException("Failed to set the 'valueAsNumber' property on 'HTMLInputElement': This input element does not support Number values.", 'InvalidStateError');
|
|
n = Number(n);
|
|
if (isNaN(n)) { this.value = ''; return; }
|
|
if (!isFinite(n)) throw new TypeError("Failed to set the 'valueAsNumber' property on 'HTMLInputElement': The value provided is infinite.");
|
|
this.value = _inputFormatNumber(t, n);
|
|
}
|
|
get valueAsDate() {
|
|
const t = this._inputType();
|
|
if (!_INPUT_DATE_TYPES[t]) return null;
|
|
const n = _inputParseNumber(t, this.value);
|
|
if (isNaN(n)) return null;
|
|
if (t === 'month') { const y = 1970 + Math.floor(n / 12); const mo = ((n % 12) + 12) % 12; return new Date(Date.UTC(y, mo, 1)); }
|
|
return new Date(n);
|
|
}
|
|
set valueAsDate(d) {
|
|
const t = this._inputType();
|
|
if (!_INPUT_DATE_TYPES[t]) throw new DOMException("Failed to set the 'valueAsDate' property on 'HTMLInputElement': This input element does not support Date values.", 'InvalidStateError');
|
|
if (d === null) { this.value = ''; return; }
|
|
if (!(d instanceof Date)) throw new TypeError("Failed to set the 'valueAsDate' property on 'HTMLInputElement': The provided value is not a Date.");
|
|
const ms = d.getTime();
|
|
if (isNaN(ms)) { this.value = ''; return; }
|
|
if (t === 'month') { this.value = _inputFormatNumber('month', (d.getUTCFullYear() - 1970) * 12 + d.getUTCMonth()); return; }
|
|
this.value = _inputFormatNumber(t, ms);
|
|
}
|
|
stepUp(n) { this._stepBy(n === undefined ? 1 : (n | 0)); }
|
|
stepDown(n) { this._stepBy(-(n === undefined ? 1 : (n | 0))); }
|
|
_stepBy(delta) {
|
|
const t = this._inputType();
|
|
const stepAttr = this.getAttribute('step');
|
|
if (!_INPUT_STEP_SCALE[t] || (stepAttr && stepAttr.trim().toLowerCase() === 'any')) {
|
|
throw new DOMException("Failed to execute 'stepUp' on 'HTMLInputElement': This form element does not have allowed value steps.", 'InvalidStateError');
|
|
}
|
|
const scale = _INPUT_STEP_SCALE[t];
|
|
let stepN = _INPUT_STEP_DEFAULT[t];
|
|
if (stepAttr) { const s = Number(stepAttr); if (isFinite(s) && s > 0) stepN = s; }
|
|
const allowed = stepN * scale;
|
|
const minN = _inputParseNumber(t, this.getAttribute('min'));
|
|
const maxN = _inputParseNumber(t, this.getAttribute('max'));
|
|
const stepBase = isNaN(minN) ? 0 : minN;
|
|
let value = this.valueAsNumber;
|
|
if (isNaN(value)) value = isNaN(minN) ? 0 : minN;
|
|
value += delta * allowed;
|
|
value = stepBase + Math.round((value - stepBase) / allowed) * allowed;
|
|
const effMin = (t === 'range' && isNaN(minN)) ? 0 : minN;
|
|
const effMax = (t === 'range' && isNaN(maxN)) ? 100 : maxN;
|
|
if (!isNaN(effMin) && value < effMin) value = effMin;
|
|
if (!isNaN(effMax) && value > effMax) value = effMax;
|
|
this.value = _inputFormatNumber(t, value);
|
|
}
|
|
get checked() {
|
|
if (_formChecked[this._nid] !== undefined) return _formChecked[this._nid];
|
|
return this.hasAttribute("checked");
|
|
}
|
|
set checked(v) { _formChecked[this._nid] = !!v; }
|
|
get selected() {
|
|
if (this._selected !== undefined) return this._selected;
|
|
return this.hasAttribute("selected");
|
|
}
|
|
set selected(v) { this._selected = !!v; }
|
|
get disabled() { return this.hasAttribute("disabled"); }
|
|
set disabled(v) { if (v) this.setAttribute("disabled", ""); else this.removeAttribute("disabled"); }
|
|
get type() { return this.getAttribute("type") || (this.localName === "input" ? "text" : ""); }
|
|
set type(v) { this.setAttribute("type", v); }
|
|
get name() { return this.getAttribute("name") || ""; }
|
|
set name(v) { this.setAttribute("name", v); }
|
|
get placeholder() { return this.getAttribute("placeholder") || ""; }
|
|
set placeholder(v) { this.setAttribute("placeholder", v); }
|
|
// For <a>/<area>, href returns the resolved absolute URL (the spec behavior,
|
|
// and what scrapers want). It uses op_url_resolve, which returns just the
|
|
// resolved string, rather than the full-component op the decomposition
|
|
// members use. Other elements reflect the raw attribute.
|
|
get href() {
|
|
const ln = this.localName;
|
|
// SVG href-bearing elements reflect href as an SVGAnimatedString (with the
|
|
// legacy xlink:href as a fallback), not a resolved URL string. Checked
|
|
// before the HTML <a> path because an SVG <a> also has localName 'a'.
|
|
if (this.namespaceURI === "http://www.w3.org/2000/svg" &&
|
|
(ln === 'a' || ln === 'image' || ln === 'use' || ln === 'script' ||
|
|
ln === 'pattern' || ln === 'filter' || ln === 'textPath' || ln === 'mpath' ||
|
|
ln === 'linearGradient' || ln === 'radialGradient' || ln === 'feImage' || ln === 'tref')) {
|
|
if (!this._svgHref) this._svgHref = new SVGAnimatedString(this, "href", "xlink:href");
|
|
return this._svgHref;
|
|
}
|
|
if (ln === 'a' || ln === 'area') {
|
|
const raw = this.getAttribute('href');
|
|
if (raw === null) return '';
|
|
// Legacy-charset document: href must reflect the encoding-override query.
|
|
if (!_docIsUtf8()) { const u = _elemHrefURL(this); return u ? u.href : raw; }
|
|
const r = _urlResolveOp(raw, _anchorBase());
|
|
return r !== null ? r : raw;
|
|
}
|
|
return this.getAttribute("href") || "";
|
|
}
|
|
set href(v) { this.setAttribute("href", v); }
|
|
// HTMLHyperlinkElementUtils URL-decomposition members, live on <a>/<area>.
|
|
get protocol() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.protocol : ''; }
|
|
set protocol(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'protocol', v); }
|
|
get username() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.username : ''; }
|
|
set username(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'username', v); }
|
|
get password() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.password : ''; }
|
|
set password(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'password', v); }
|
|
get host() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.host : ''; }
|
|
set host(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'host', v); }
|
|
get hostname() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.hostname : ''; }
|
|
set hostname(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'hostname', v); }
|
|
get port() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.port : ''; }
|
|
set port(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'port', v); }
|
|
get pathname() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.pathname : ''; }
|
|
set pathname(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'pathname', v); }
|
|
get search() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.search : ''; }
|
|
set search(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'search', v); }
|
|
get hash() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.hash : ''; }
|
|
set hash(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'hash', v); }
|
|
get origin() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.origin : ''; }
|
|
get src() {
|
|
// IDL reflection: HTMLScriptElement/HTMLImageElement/etc. `.src` returns the
|
|
// resolved absolute URL, not the literal attribute. Loaders that compute their
|
|
// base via `new URL(document.currentScript.src).origin` break on a relative
|
|
// value (issue #255). getAttribute("src") still returns the literal.
|
|
const v = this.getAttribute("src");
|
|
if (!v) return "";
|
|
try { return new URL(v, globalThis.location?.href || "about:blank").href; }
|
|
catch (e) { return v; }
|
|
}
|
|
set src(v) {
|
|
this.setAttribute("src", v);
|
|
if (this.localName === 'iframe' && v && v !== 'about:blank') {
|
|
this._loadIframeSrc(v);
|
|
}
|
|
}
|
|
_loadIframeSrc(url) {
|
|
let fullUrl = url;
|
|
if (!url.includes('://')) {
|
|
try { fullUrl = new URL(url, _domParse("document_url") || "about:blank").href; } catch(e) {}
|
|
}
|
|
const el = this;
|
|
fetch(fullUrl, {mode: 'no-cors'}).then(async resp => {
|
|
if (resp.ok || resp.type === 'opaque') {
|
|
const html = await resp.text();
|
|
el._iframeDoc = new _IframeDocument(html, fullUrl, el);
|
|
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
|
|
} else {
|
|
el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el);
|
|
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
|
|
}
|
|
|
|
// Dispatch through the element so the onload property/attribute and any
|
|
// addEventListener('load', ...) listeners all run. Calling el.onload()
|
|
// directly bypasses listeners registered via addEventListener.
|
|
el.dispatchEvent(new Event('load'));
|
|
}).catch(() => {
|
|
el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el);
|
|
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
|
|
|
|
el.dispatchEvent(new Event('load'));
|
|
});
|
|
}
|
|
get contentDocument() {
|
|
if (this.localName !== 'iframe') return undefined;
|
|
if (this._iframeDoc) {
|
|
const pageOrigin = (function(){ try { return new URL(_domParse("document_url")).origin; } catch(e) { return ''; } })();
|
|
const iframeOrigin = (function(url){ try { return new URL(url).origin; } catch(e) { return ''; } })(this.src);
|
|
if (pageOrigin === iframeOrigin || this.src === '' || this.src === 'about:blank' || !this.src.includes('://')) {
|
|
return this._iframeDoc;
|
|
}
|
|
return null; // Cross-origin: blocked
|
|
}
|
|
if (!this._iframeDoc) {
|
|
this._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', 'about:blank', this);
|
|
this._iframeWin = new _IframeWindow(this._iframeDoc, 'about:blank');
|
|
}
|
|
return this._iframeDoc;
|
|
}
|
|
get contentWindow() {
|
|
if (this.localName !== 'iframe') return undefined;
|
|
if (!this._iframeWin) {
|
|
if (this.parentNode === null) return null;
|
|
this.contentDocument;
|
|
}
|
|
return this._iframeWin;
|
|
}
|
|
get action() {
|
|
const action = this.getAttribute("action") || _domParse("document_url") || "";
|
|
try { return new URL(action, _domParse("document_url") || "about:blank").href; } catch(e) { return action; }
|
|
}
|
|
set action(v) { this.setAttribute("action", v); }
|
|
get method() { return this.getAttribute("method") || "get"; }
|
|
set method(v) { this.setAttribute("method", v); }
|
|
get form() {
|
|
let p = this.parentNode;
|
|
while (p && p.localName !== 'form') p = p.parentNode;
|
|
return p;
|
|
}
|
|
get options() {
|
|
if (this.localName !== 'select') return [];
|
|
return HTMLCollection._from(this.querySelectorAll('option'));
|
|
}
|
|
get selectedIndex() {
|
|
const opts = this.options;
|
|
for (let i = 0; i < opts.length; i++) {
|
|
if (opts[i].selected || opts[i].hasAttribute('selected')) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
set selectedIndex(v) {
|
|
const opts = this.options;
|
|
for (let i = 0; i < opts.length; i++) {
|
|
opts[i]._selected = (i === v);
|
|
}
|
|
}
|
|
// Per the HTML spec, the submit() METHOD submits the form WITHOUT firing a
|
|
// cancelable `submit` event — a page's submit listener cannot veto it. Only
|
|
// requestSubmit() and user-initiated submits fire the cancelable event.
|
|
// Conflating the two broke sites whose submit listener preventDefault()s the
|
|
// native submit and then calls form.submit() from a callback (e.g. an
|
|
// invisible-reCAPTCHA data-callback) to actually send the form.
|
|
submit(submitter) {
|
|
this._navigateSubmit(submitter);
|
|
}
|
|
requestSubmit(submitter) {
|
|
// Per spec, a given submitter must be a submit button owned by this form;
|
|
// both checks run before the submit event fires. A missing/null submitter
|
|
// means "submit from the form itself".
|
|
if (submitter !== undefined && submitter !== null) {
|
|
if (!_isSubmitButton(submitter)) {
|
|
throw new TypeError(
|
|
"Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not a submit button."
|
|
);
|
|
}
|
|
if (submitter.form !== this) {
|
|
throw new DOMException(
|
|
"Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not owned by this form element.",
|
|
'NotFoundError'
|
|
);
|
|
}
|
|
}
|
|
const cancelled = !this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
if (cancelled) return;
|
|
this._navigateSubmit(submitter);
|
|
}
|
|
_navigateSubmit(submitter) {
|
|
const pairs = [];
|
|
const fields = this.querySelectorAll('input, select, textarea');
|
|
for (let i = 0; i < fields.length; i++) {
|
|
const f = fields[i];
|
|
const name = f.getAttribute('name');
|
|
if (!name) continue;
|
|
if (f.getAttribute('disabled') !== null) continue;
|
|
const tag = f.localName;
|
|
const type = (f.getAttribute('type') || '').toLowerCase();
|
|
if ((type === 'checkbox' || type === 'radio') && !f.checked) continue;
|
|
if (type === 'file' || type === 'reset') continue;
|
|
if (type === 'button') continue;
|
|
if (type === 'submit' || tag === 'button') {
|
|
if (submitter && f !== submitter) continue;
|
|
if (!submitter) continue; // default submit: don't include submit button value
|
|
}
|
|
|
|
let val;
|
|
if (tag === 'select') {
|
|
const opt = f.querySelector('option[selected]') || f.querySelector('option');
|
|
val = opt ? (opt.getAttribute('value') !== null ? opt.getAttribute('value') : opt.textContent) : '';
|
|
} else if (tag === 'textarea') {
|
|
val = f.value || f.textContent || '';
|
|
} else {
|
|
val = f.value !== undefined ? f.value : (f.getAttribute('value') || '');
|
|
}
|
|
const enc = (s) => encodeURIComponent(s).replace(/%20/g, '+').replace(/!/g, '%21');
|
|
pairs.push(enc(name) + '=' + enc(val));
|
|
}
|
|
|
|
const action = this.getAttribute('action') || '';
|
|
const method = (this.getAttribute('method') || 'GET').toUpperCase();
|
|
const baseUrl = globalThis.location?.href || 'about:blank';
|
|
let targetUrl;
|
|
try { targetUrl = new URL(action, baseUrl).href; } catch(e) { targetUrl = action; }
|
|
|
|
const encoded = pairs.join('&');
|
|
if (method === 'POST') {
|
|
Deno.core.ops.op_navigate(targetUrl, 'POST', encoded);
|
|
} else {
|
|
const sep = targetUrl.includes('?') ? '&' : '?';
|
|
Deno.core.ops.op_navigate(targetUrl + (encoded ? sep + encoded : ''), 'GET', '');
|
|
}
|
|
}
|
|
reset() {
|
|
this.dispatchEvent(new Event('reset', { bubbles: true }));
|
|
}
|
|
get dataset() {
|
|
if (this._dataset) return this._dataset;
|
|
const el = this;
|
|
const attrFor = (k) => "data-" + _cssCamelToKebab(k);
|
|
// camelCase the part after the `data-` prefix, e.g. data-foo-bar -> fooBar.
|
|
const dataKeys = () => el.getAttributeNames()
|
|
.filter((n) => n.startsWith("data-"))
|
|
.map((n) => _cssKebabToCamel(n.slice(5)));
|
|
this._dataset = new Proxy({}, {
|
|
get(_, k) { if (typeof k !== "string") return undefined; return el.hasAttribute(attrFor(k)) ? el.getAttribute(attrFor(k)) : undefined; },
|
|
set(_, k, v) { el.setAttribute(attrFor(k), String(v)); return true; },
|
|
has(_, k) { return typeof k === "string" && el.hasAttribute(attrFor(k)); },
|
|
deleteProperty(_, k) { if (typeof k === "string") el.removeAttribute(attrFor(k)); return true; },
|
|
ownKeys() { return dataKeys(); },
|
|
getOwnPropertyDescriptor(_, k) {
|
|
if (typeof k === "string" && el.hasAttribute(attrFor(k))) {
|
|
return { value: el.getAttribute(attrFor(k)), writable: true, enumerable: true, configurable: true };
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
return this._dataset;
|
|
}
|
|
get offsetWidth() { return this._isViewportRoot() ? (globalThis.innerWidth || 1280) : 100; }
|
|
get offsetHeight() { return this._isViewportRoot() ? (globalThis.innerHeight || 720) : 20; }
|
|
get offsetTop() { return 0; } get offsetLeft() { return 0; }
|
|
// documentElement / body / window expose VIEWPORT geometry, not their own content box.
|
|
// Puppeteer's #clickableBox clips boxes to document.documentElement.clientWidth/Height;
|
|
// returning 100x20 there made every element appear off-screen and broke .click().
|
|
get clientWidth() { return this._isViewportRoot() ? (globalThis.innerWidth || 1280) : 100; }
|
|
get clientHeight() { return this._isViewportRoot() ? (globalThis.innerHeight || 720) : 20; }
|
|
get scrollWidth() { return this._isViewportRoot() ? (globalThis.innerWidth || 1280) : 100; }
|
|
get scrollHeight() { return this._isViewportRoot() ? (globalThis.innerHeight || 720) : 20; }
|
|
_isViewportRoot() {
|
|
const t = this.tagName;
|
|
return t === 'HTML' || t === 'BODY';
|
|
}
|
|
// No layout engine, so there is no real overflow to scroll and the offset is
|
|
// deliberately NOT clamped: without real geometry any synthetic max is a
|
|
// guess, and a max derived from a stub scroll box pins scrollTop at 0, which
|
|
// deadlocks scroll-driven lazy loaders (no scroll -> no content -> no scroll).
|
|
// We track the offset so scrollTop/scrollLeft round-trip, and fire a scroll
|
|
// event on direct assignment -- lazy loaders that set `el.scrollTop = N` rely
|
|
// on that event, and scrollTo/scrollBy below would otherwise be its only source.
|
|
get scrollTop() { return this._scrollTop || 0; }
|
|
set scrollTop(v) {
|
|
v = +v;
|
|
const nv = Number.isFinite(v) && v > 0 ? v : 0;
|
|
const changed = nv !== (this._scrollTop || 0);
|
|
this._scrollTop = nv;
|
|
if (changed && !this._scrollSuppress) this._fireScroll();
|
|
}
|
|
get scrollLeft() { return this._scrollLeft || 0; }
|
|
set scrollLeft(v) {
|
|
v = +v;
|
|
const nv = Number.isFinite(v) && v > 0 ? v : 0;
|
|
const changed = nv !== (this._scrollLeft || 0);
|
|
this._scrollLeft = nv;
|
|
if (changed && !this._scrollSuppress) this._fireScroll();
|
|
}
|
|
getBoundingClientRect() {
|
|
globalThis.__obscura_click_target = this;
|
|
// documentElement and body span the full viewport. Without this every
|
|
// hit test against them clips down to a 100x20 synthetic cell and
|
|
// Document.elementFromPoint can never recurse into their children.
|
|
if (this._isViewportRoot()) {
|
|
const vw = globalThis.innerWidth || 1280;
|
|
const vh = globalThis.innerHeight || 720;
|
|
return {
|
|
x: 0, y: 0, width: vw, height: vh,
|
|
top: 0, right: vw, bottom: vh, left: 0,
|
|
toJSON() { return this; },
|
|
};
|
|
}
|
|
// No layout engine, but Playwright's actionability polling needs each
|
|
// element to occupy a stable, distinct rect so hit-testing can pick the
|
|
// right one (issue #45). Synthesize a deterministic position from the
|
|
// node id: every nid maps to a unique cell in a 12-column grid, sized
|
|
// to fit a 1280x720 viewport. Stable across reads, different per node.
|
|
const VW = 1280, VH = 720, COLS = 12, CW = 100, CH = 20, GX = 110, GY = 30;
|
|
const rowsPerScreen = Math.max(1, Math.floor((VH - 10) / GY));
|
|
const cell = this._nid | 0;
|
|
const col = ((cell * 7) | 0) % COLS;
|
|
const row = (((cell * 13) | 0) >> 0) % rowsPerScreen;
|
|
const x = 10 + col * GX;
|
|
const y = 10 + row * GY;
|
|
return {
|
|
x, y, width: CW, height: CH,
|
|
top: y, right: x + CW, bottom: y + CH, left: x,
|
|
toJSON() { return this; },
|
|
};
|
|
}
|
|
getClientRects() { return new DOMRectList([this.getBoundingClientRect()]); }
|
|
// No layout engine: a stub that always returns true unblocks Playwright's
|
|
// actionability polling. With a real layout we'd check display, visibility,
|
|
// opacity and rect dimensions per spec.
|
|
checkVisibility(opts) { return true; }
|
|
// ARIA reflection properties. Without an accessibility tree we expose the
|
|
// raw aria-* attributes so Playwright's getByRole / getByLabel locators can
|
|
// at least find elements that author them explicitly.
|
|
get role() { return this.getAttribute('role'); }
|
|
set role(v) { if (v == null) this.removeAttribute('role'); else this.setAttribute('role', String(v)); }
|
|
get ariaLabel() { return this.getAttribute('aria-label'); }
|
|
set ariaLabel(v) { if (v == null) this.removeAttribute('aria-label'); else this.setAttribute('aria-label', String(v)); }
|
|
get ariaRoleDescription() { return this.getAttribute('aria-roledescription'); }
|
|
set ariaRoleDescription(v) { if (v == null) this.removeAttribute('aria-roledescription'); else this.setAttribute('aria-roledescription', String(v)); }
|
|
get ariaChecked() { return this.getAttribute('aria-checked'); }
|
|
set ariaChecked(v) { if (v == null) this.removeAttribute('aria-checked'); else this.setAttribute('aria-checked', String(v)); }
|
|
get ariaDisabled() { return this.getAttribute('aria-disabled'); }
|
|
set ariaDisabled(v) { if (v == null) this.removeAttribute('aria-disabled'); else this.setAttribute('aria-disabled', String(v)); }
|
|
get ariaExpanded() { return this.getAttribute('aria-expanded'); }
|
|
set ariaExpanded(v) { if (v == null) this.removeAttribute('aria-expanded'); else this.setAttribute('aria-expanded', String(v)); }
|
|
get ariaHidden() { return this.getAttribute('aria-hidden'); }
|
|
set ariaHidden(v) { if (v == null) this.removeAttribute('aria-hidden'); else this.setAttribute('aria-hidden', String(v)); }
|
|
get ariaSelected() { return this.getAttribute('aria-selected'); }
|
|
set ariaSelected(v) { if (v == null) this.removeAttribute('aria-selected'); else this.setAttribute('aria-selected', String(v)); }
|
|
scrollIntoView() { globalThis.__obscura_click_target = this; }
|
|
// scrollTo/scrollBy/scroll accept either (x, y) or a ScrollToOptions object.
|
|
// The setters fire a scroll event of their own, so suppress the per-axis ones
|
|
// here and emit a single event for the whole movement, the way a real browser
|
|
// coalesces one scroll per scroll operation rather than one per axis.
|
|
scrollTo(x, y) {
|
|
let left, top;
|
|
if (x !== null && typeof x === 'object') { left = x.left; top = x.top; }
|
|
else { left = x; top = y; }
|
|
this._scrollSuppress = true;
|
|
if (left !== undefined) this.scrollLeft = +left || 0;
|
|
if (top !== undefined) this.scrollTop = +top || 0;
|
|
this._scrollSuppress = false;
|
|
this._fireScroll();
|
|
}
|
|
scroll(x, y) { this.scrollTo(x, y); }
|
|
scrollBy(x, y) {
|
|
let dl, dt;
|
|
if (x !== null && typeof x === 'object') { dl = x.left; dt = x.top; }
|
|
else { dl = x; dt = y; }
|
|
this._scrollSuppress = true;
|
|
this.scrollLeft = (this.scrollLeft || 0) + (+dl || 0);
|
|
this.scrollTop = (this.scrollTop || 0) + (+dt || 0);
|
|
this._scrollSuppress = false;
|
|
this._fireScroll();
|
|
}
|
|
_fireScroll() {
|
|
const self = this;
|
|
setTimeout(() => { try { self.dispatchEvent(new Event('scroll', { bubbles: false })); } catch (e) {} }, 0);
|
|
}
|
|
animate(keyframes, options) {
|
|
const duration = typeof options === 'number' ? options : (options?.duration || 0);
|
|
return {
|
|
finished: Promise.resolve(), currentTime: 0, playState: 'finished',
|
|
effect: { getComputedTiming() { return { duration }; } },
|
|
cancel(){}, finish(){}, play(){}, pause(){}, reverse(){},
|
|
addEventListener(){}, removeEventListener(){},
|
|
onfinish: null, oncancel: null,
|
|
};
|
|
}
|
|
getAnimations() { return []; }
|
|
get isConnected() {
|
|
var node = this;
|
|
while (node) {
|
|
if (node.nodeType === 9) return true;
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
remove() { if (this.parentNode) this.parentNode.removeChild(this); }
|
|
append(...nodes) { for (const n of _convertNodes(nodes)) this.appendChild(n); }
|
|
prepend(...nodes) {
|
|
const ref = this.firstChild;
|
|
for (const n of _convertNodes(nodes)) {
|
|
if (ref) this.insertBefore(n, ref); else this.appendChild(n);
|
|
}
|
|
}
|
|
replaceChildren(...nodes) {
|
|
const converted = _convertNodes(nodes);
|
|
let c;
|
|
while ((c = this.firstChild)) this.removeChild(c);
|
|
for (const n of converted) this.appendChild(n);
|
|
}
|
|
}
|
|
|
|
// WHATWG "convert nodes into a node": a Node argument passes through, anything
|
|
// else is stringified into a Text node, so e.g. append(null) inserts the text
|
|
// "null" and append(undefined) inserts "undefined" per the (Node or DOMString)
|
|
// union, rather than throwing.
|
|
function _convertNodes(nodes) {
|
|
const out = [];
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const n = nodes[i];
|
|
if (n && typeof n._nid === "number") out.push(n);
|
|
else out.push(document.createTextNode(String(n)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---- Reflected IDL attributes (WHATWG) ---------------------------------------
|
|
// Installed ONCE on Element.prototype as shared getter/setter pairs. This is
|
|
// data-driven so there is no per-element defineProperty: element creation and
|
|
// the querySelector/mutation hot paths are unaffected (each access is a normal
|
|
// prototype getter that reads the backing attribute). Covers the global content
|
|
// attributes reflected on every element plus the ARIAMixin (aria-* + ariaXxx).
|
|
(function installElementReflectors() {
|
|
const P = Element.prototype;
|
|
const def = (name, get, set) => {
|
|
if (Object.prototype.hasOwnProperty.call(P, name)) return; // never clobber an existing member
|
|
Object.defineProperty(P, name, { get, set, enumerable: true, configurable: true });
|
|
};
|
|
// WHATWG "rules for parsing integers"; returns a JS number or null on failure.
|
|
const parseIntAttr = (s) => {
|
|
if (s === null || s === undefined) return null;
|
|
const m = /^[ \t\n\f\r]*([+-]?[0-9]+)/.exec(String(s));
|
|
if (!m) return null;
|
|
const n = parseInt(m[1], 10);
|
|
return Number.isFinite(n) ? n : null;
|
|
};
|
|
// IDL `long` conversion (ToInt32): finite, truncated, wrapped to 32-bit signed.
|
|
const toLong = (v) => {
|
|
let n = Number(v);
|
|
if (!Number.isFinite(n)) n = 0;
|
|
n = Math.trunc(n) % 4294967296;
|
|
if (n >= 2147483648) n -= 4294967296;
|
|
else if (n < -2147483648) n += 4294967296;
|
|
return n;
|
|
};
|
|
// DOMString reflect: get -> attribute or ""; set -> setAttribute(String(v)).
|
|
const reflectStr = (name, attr) => def(name,
|
|
function () { const v = this.getAttribute(attr); return v === null ? "" : v; },
|
|
function (v) { this.setAttribute(attr, String(v)); });
|
|
// boolean reflect: get -> hasAttribute; set -> truthy ? add("") : remove.
|
|
const reflectBool = (name, attr) => def(name,
|
|
function () { return this.hasAttribute(attr); },
|
|
function (v) { if (v) this.setAttribute(attr, ""); else this.removeAttribute(attr); });
|
|
// long reflect: get -> parse else default (static value or per-element fn);
|
|
// set -> setAttribute(String(ToInt32(v))).
|
|
const reflectLong = (name, attr, dflt) => def(name,
|
|
function () {
|
|
const r = parseIntAttr(this.getAttribute(attr));
|
|
if (r !== null && r >= -2147483648 && r <= 2147483647) return r;
|
|
return typeof dflt === "function" ? dflt.call(this) : dflt;
|
|
},
|
|
function (v) { this.setAttribute(attr, String(toLong(v))); });
|
|
// enumerated reflect: get -> canonical (lowercased) keyword, else missing/
|
|
// invalid default; set -> setAttribute(String(v)) (canonicalization on get).
|
|
const reflectEnum = (name, attr, keywords, missingDefault, invalidDefault) => def(name,
|
|
function () {
|
|
const v = this.getAttribute(attr);
|
|
if (v === null) return missingDefault;
|
|
const lc = String(v).toLowerCase();
|
|
return keywords.indexOf(lc) !== -1 ? lc : invalidDefault;
|
|
},
|
|
function (v) { this.setAttribute(attr, String(v)); });
|
|
// nullable DOMString reflect (ARIA): get -> attribute or null; set -> null/
|
|
// undefined removes, else setAttribute(String(v)).
|
|
const reflectNullable = (name, attr) => def(name,
|
|
function () { return this.getAttribute(attr); },
|
|
function (v) { if (v === null || v === undefined) this.removeAttribute(attr); else this.setAttribute(attr, String(v)); });
|
|
|
|
// Global content attributes reflected on every element (HTML "global attributes").
|
|
reflectStr("title", "title");
|
|
reflectStr("lang", "lang");
|
|
reflectStr("accessKey", "accesskey");
|
|
reflectStr("slot", "slot");
|
|
reflectEnum("dir", "dir", ["ltr", "rtl", "auto"], "", "");
|
|
reflectBool("autofocus", "autofocus");
|
|
reflectBool("hidden", "hidden");
|
|
// tabIndex default is element-dependent (0 for natively-focusable, else -1);
|
|
// reflection.js does not assert it, but match the common case anyway.
|
|
reflectLong("tabIndex", "tabindex", function () {
|
|
const ln = this.localName;
|
|
if (ln === "a" || ln === "area" || ln === "link") return this.hasAttribute("href") ? 0 : -1;
|
|
return (ln === "button" || ln === "input" || ln === "select" || ln === "textarea" || ln === "iframe") ? 0 : -1;
|
|
});
|
|
|
|
// ARIAMixin: aria-* content attributes reflected as nullable DOMString IDL
|
|
// properties (ariaAtomic <-> aria-atomic, ...).
|
|
const ARIA = {
|
|
ariaAtomic: "aria-atomic", ariaAutoComplete: "aria-autocomplete", ariaBrailleLabel: "aria-braillelabel",
|
|
ariaBrailleRoleDescription: "aria-brailleroledescription", ariaBusy: "aria-busy", ariaChecked: "aria-checked",
|
|
ariaColCount: "aria-colcount", ariaColIndex: "aria-colindex", ariaColIndexText: "aria-colindextext",
|
|
ariaColSpan: "aria-colspan", ariaCurrent: "aria-current", ariaDescription: "aria-description",
|
|
ariaDisabled: "aria-disabled", ariaExpanded: "aria-expanded", ariaHasPopup: "aria-haspopup",
|
|
ariaHidden: "aria-hidden", ariaInvalid: "aria-invalid", ariaKeyShortcuts: "aria-keyshortcuts",
|
|
ariaLabel: "aria-label", ariaLevel: "aria-level", ariaLive: "aria-live", ariaModal: "aria-modal",
|
|
ariaMultiLine: "aria-multiline", ariaMultiSelectable: "aria-multiselectable", ariaOrientation: "aria-orientation",
|
|
ariaPlaceholder: "aria-placeholder", ariaPosInSet: "aria-posinset", ariaPressed: "aria-pressed",
|
|
ariaReadOnly: "aria-readonly", ariaRelevant: "aria-relevant", ariaRequired: "aria-required",
|
|
ariaRoleDescription: "aria-roledescription", ariaRowCount: "aria-rowcount", ariaRowIndex: "aria-rowindex",
|
|
ariaRowIndexText: "aria-rowindextext", ariaRowSpan: "aria-rowspan", ariaSelected: "aria-selected",
|
|
ariaSetSize: "aria-setsize", ariaSort: "aria-sort", ariaValueMax: "aria-valuemax",
|
|
ariaValueMin: "aria-valuemin", ariaValueNow: "aria-valuenow", ariaValueText: "aria-valuetext",
|
|
};
|
|
for (const prop in ARIA) reflectNullable(prop, ARIA[prop]);
|
|
})();
|
|
|
|
function _parseXPathPredicate(part) {
|
|
part = String(part || "").trim();
|
|
let m = part.match(/^@([A-Za-z_][\w:.-]*)(?:\s*=\s*(["'])(.*?)\2)?$/);
|
|
if (m) return { kind: "attr", name: m[1], value: m[3] };
|
|
m = part.match(/^contains\(\s*@([A-Za-z_][\w:.-]*)\s*,\s*(["'])(.*?)\2\s*\)$/);
|
|
if (m) return { kind: "contains", name: m[1], value: m[3] };
|
|
m = part.match(/^starts-with\(\s*@([A-Za-z_][\w:.-]*)\s*,\s*(["'])(.*?)\2\s*\)$/);
|
|
if (m) return { kind: "startsWith", name: m[1], value: m[3] };
|
|
return null;
|
|
}
|
|
|
|
function _xpathPredicateParts(body) {
|
|
const out = [];
|
|
let quote = null, start = 0;
|
|
for (let i = 0; i < body.length; i++) {
|
|
const ch = body[i];
|
|
if (quote) {
|
|
if (ch === quote) quote = null;
|
|
continue;
|
|
}
|
|
if (ch === '"' || ch === "'") {
|
|
quote = ch;
|
|
continue;
|
|
}
|
|
if (body.slice(i, i + 5).toLowerCase() === " and " || body.slice(i, i + 4).toLowerCase() === "and ") {
|
|
const before = body.slice(start, i).trim();
|
|
if (before) out.push(before);
|
|
i += body[i] === " " ? 4 : 3;
|
|
start = i + 1;
|
|
}
|
|
}
|
|
const last = body.slice(start).trim();
|
|
if (last) out.push(last);
|
|
return out.length ? out : [body];
|
|
}
|
|
|
|
function _xpathFindNodes(expression, contextNode) {
|
|
expression = String(expression || "").trim();
|
|
contextNode = contextNode || document;
|
|
const m = expression.match(/^(?:\.?\/\/)([A-Za-z*][\w:.-]*|\*)?((?:\[[^\]]+\])*)$/);
|
|
if (!m) return [];
|
|
const tag = !m[1] || m[1] === "*" ? "*" : m[1];
|
|
const predicates = [];
|
|
const predText = m[2] || "";
|
|
for (const match of predText.matchAll(/\[([^\]]+)\]/g)) {
|
|
for (const part of _xpathPredicateParts(match[1])) {
|
|
const pred = _parseXPathPredicate(part);
|
|
if (pred) predicates.push(pred);
|
|
}
|
|
}
|
|
const source = typeof contextNode.querySelectorAll === "function"
|
|
? contextNode.querySelectorAll(tag)
|
|
: [];
|
|
return Array.prototype.filter.call(source, (node) => {
|
|
for (const pred of predicates) {
|
|
const value = node.getAttribute?.(pred.name);
|
|
if (pred.kind === "attr") {
|
|
if (value === null) return false;
|
|
if (pred.value !== undefined && value !== pred.value) return false;
|
|
} else if (pred.kind === "contains") {
|
|
if (value === null || !String(value).includes(pred.value)) return false;
|
|
} else if (pred.kind === "startsWith") {
|
|
if (value === null || !String(value).startsWith(pred.value)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function _makeXPathResult(type, nodes) {
|
|
nodes = Array.from(nodes || []);
|
|
const requested = type || XPathResult.ANY_TYPE;
|
|
const resultType = requested === XPathResult.ANY_TYPE
|
|
? XPathResult.UNORDERED_NODE_ITERATOR_TYPE
|
|
: requested;
|
|
let iter = 0;
|
|
return {
|
|
resultType,
|
|
singleNodeValue: nodes[0] || null,
|
|
snapshotLength: nodes.length,
|
|
snapshotItem(i) { return nodes[i] || null; },
|
|
iterateNext() { return nodes[iter++] || null; },
|
|
invalidIteratorState: false,
|
|
numberValue: nodes.length,
|
|
stringValue: nodes[0]?.textContent || "",
|
|
booleanValue: nodes.length > 0,
|
|
};
|
|
}
|
|
|
|
class Document extends Node {
|
|
get documentElement() { return _wrapEl(+_dom("document_element")); }
|
|
get head() { return this.querySelector("head"); }
|
|
get body() { return this.querySelector("body"); }
|
|
get doctype() {
|
|
if (this._doctype !== undefined) return this._doctype;
|
|
const info = _domParse("document_doctype");
|
|
if (info && info.name) {
|
|
this._doctype = new DocumentType(info.nodeId, info.name, info.publicId || "", info.systemId || "");
|
|
} else {
|
|
this._doctype = null;
|
|
}
|
|
return this._doctype;
|
|
}
|
|
get title() { return _domParse("document_title") ?? ""; }
|
|
set title(v) {}
|
|
get URL() { return _domParse("document_url") ?? ""; }
|
|
get documentURI() { return this.URL; }
|
|
get location() { return globalThis.location; }
|
|
set location(url) { Deno.core.ops.op_navigate(_resolveUrl(String(url)), 'GET', ''); }
|
|
get defaultView() { return globalThis; }
|
|
get nodeType() { return 9; }
|
|
get nodeName() { return "#document"; }
|
|
get ownerDocument() { return null; } // Document has no ownerDocument
|
|
get compatMode() { return "CSS1Compat"; }
|
|
// The document's character encoding, detected from the response charset
|
|
// (HTTP Content-Type -> <meta charset>). characterSet/charset/inputEncoding
|
|
// are WHATWG aliases. A node-less document (DOMParser/createDocument) has no
|
|
// backing encoding and reports UTF-8.
|
|
get characterSet() { return (this._nid === undefined || this._nid === null) ? "UTF-8" : _docEncoding(); }
|
|
get charset() { return this.characterSet; }
|
|
get inputEncoding() { return this.characterSet; }
|
|
get contentType() {
|
|
// An explicit type set by DOMParser/createDocument wins.
|
|
if (this._contentType) return this._contentType;
|
|
// `new Document()` (the WHATWG constructor, no backing node id) creates an
|
|
// XML document, so createCDATASection/etc. must not throw. Live documents
|
|
// wrapped from the tree carry a real nid and fall through to URL-derived.
|
|
if (this._nid === undefined || this._nid === null) return "application/xml";
|
|
const url = this.URL || "";
|
|
// data: URLs carry their MIME type explicitly.
|
|
const dm = /^data:([^,;]+)/i.exec(url);
|
|
if (dm) {
|
|
const mime = dm[1].toLowerCase();
|
|
if (mime === "application/xhtml+xml") return "application/xhtml+xml";
|
|
if (mime === "text/xml") return "text/xml";
|
|
if (mime === "application/xml" || mime.endsWith("+xml")) return "application/xml";
|
|
}
|
|
if (/\.xhtml(?:[?#]|$)/i.test(url)) return "application/xhtml+xml";
|
|
if (/\.(?:xml|svg)(?:[?#]|$)/i.test(url)) return "application/xml";
|
|
return "text/html";
|
|
}
|
|
get readyState() { return globalThis.__documentReadyState__ || 'complete'; }
|
|
get currentScript() {
|
|
// Next.js / Turbopack chunk loader reads document.currentScript.src to
|
|
// derive its base path. page.rs sets __currentScriptNid before each
|
|
// <script> body runs and clears it after, mirroring real Chrome.
|
|
const nid = globalThis.__currentScriptNid;
|
|
return nid ? _wrapEl(+nid) : null;
|
|
}
|
|
get hidden() { return false; }
|
|
get visibilityState() { return "visible"; }
|
|
getElementById(id) { return _wrapEl(+_dom("get_element_by_id", id)); }
|
|
querySelector(s) { return _wrapEl(+_dom("query_selector", s)); }
|
|
querySelectorAll(s) {
|
|
const ids = _domParse("query_selector_all", s) || [];
|
|
return _nodeList(ids.map(_wrapEl).filter(Boolean));
|
|
}
|
|
getElementsByTagName(t) { return HTMLCollection._from(this.querySelectorAll(t)); }
|
|
getElementsByClassName(c) { return _getElementsByClassName(this, c); }
|
|
getElementsByName(name) { return this.querySelectorAll('[name="' + String(name).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]'); }
|
|
evaluate(expression, contextNode, namespaceResolver, type, result) {
|
|
return _makeXPathResult(type, _xpathFindNodes(expression, contextNode || this));
|
|
}
|
|
createElement(t) {
|
|
const el = _wrapEl(+_dom("create_element", t.toLowerCase()));
|
|
if (el && t.toLowerCase() === 'template') {
|
|
el._templateContent = this.createDocumentFragment();
|
|
}
|
|
return el;
|
|
}
|
|
createElementNS(ns, t) {
|
|
const el = this.createElement(t);
|
|
if (el) el._ns = ns;
|
|
return el;
|
|
}
|
|
createTextNode(t) { return _wrap(+_dom("create_text_node", String(t))); }
|
|
createComment(t) {
|
|
const nid = +_dom("create_comment_node", String(t ?? ""));
|
|
const n = new Comment(nid);
|
|
_cache.set(nid, n);
|
|
return n;
|
|
}
|
|
createCDATASection(data) {
|
|
// Spec: throw NotSupportedError on an HTML document, reject data
|
|
// containing "]]>", then return a CDATASection node.
|
|
if (!_isXMLDocument(this)) {
|
|
throw new DOMException("createCDATASection is not supported in HTML documents", "NotSupportedError");
|
|
}
|
|
const str = String(data);
|
|
if (str.indexOf("]]>") !== -1) {
|
|
throw new DOMException("CDATA section data must not contain ']]>'", "InvalidCharacterError");
|
|
}
|
|
const nid = +_dom("create_text_node", str);
|
|
const n = new CDATASection(nid);
|
|
_cache.set(nid, n);
|
|
return n;
|
|
}
|
|
createProcessingInstruction(target, data) {
|
|
// Spec: not gated on document type. Reject targets that are not an XML
|
|
// Name, then reject data containing "?>", then return a PI node.
|
|
const tgt = String(target);
|
|
const str = String(data);
|
|
if (!_isValidPITarget(tgt)) {
|
|
throw new DOMException("Invalid processing instruction target", "InvalidCharacterError");
|
|
}
|
|
if (str.indexOf("?>") !== -1) {
|
|
throw new DOMException("Processing instruction data must not contain '?>'", "InvalidCharacterError");
|
|
}
|
|
const nid = +_dom("create_text_node", str);
|
|
const n = new ProcessingInstruction(nid, tgt);
|
|
_cache.set(nid, n);
|
|
return n;
|
|
}
|
|
createDocumentFragment() {
|
|
const nid = +_dom("create_document_fragment");
|
|
const frag = new DocumentFragment(nid);
|
|
_cache.set(nid, frag);
|
|
return frag;
|
|
}
|
|
// Legacy DOM Level 2 event factory. Spec returns an event of the requested
|
|
// class with an empty type until init*Event() is called. We previously
|
|
// returned a generic Event for every type, which broke libraries that call
|
|
// createEvent('CustomEvent').initCustomEvent(...) — see issue #41.
|
|
createEvent(type) {
|
|
const normalized = String(type || '').toLowerCase();
|
|
if (normalized === 'promiserejectionevent') {
|
|
throw new DOMException(
|
|
"The provided event type ('PromiseRejectionEvent') is invalid",
|
|
'NotSupportedError'
|
|
);
|
|
}
|
|
const map = {
|
|
'customevent': CustomEvent, 'customevents': CustomEvent,
|
|
'mouseevent': MouseEvent, 'mouseevents': MouseEvent,
|
|
'keyboardevent': KeyboardEvent, 'keyboardevents': KeyboardEvent,
|
|
'focusevent': FocusEvent,
|
|
'inputevent': InputEvent,
|
|
'uievent': UIEvent, 'uievents': UIEvent,
|
|
'compositionevent': CompositionEvent,
|
|
'wheelevent': WheelEvent,
|
|
'pointerevent': PointerEvent,
|
|
'errorevent': ErrorEvent,
|
|
'popstateevent': PopStateEvent,
|
|
'animationevent': AnimationEvent,
|
|
'transitionevent': TransitionEvent,
|
|
'storageevent': StorageEvent,
|
|
};
|
|
const Cls = map[normalized] || Event;
|
|
return new Cls('');
|
|
}
|
|
createRange() { return new Range(); }
|
|
addEventListener(type, fn, opts) {
|
|
if (typeof fn !== 'function') return;
|
|
if (!this._listeners) this._listeners = {};
|
|
if (!this._listeners[type]) this._listeners[type] = [];
|
|
if (!this._listeners[type].includes(fn)) this._listeners[type].push(fn);
|
|
}
|
|
removeEventListener(type, fn) {
|
|
if (this._listeners?.[type]) {
|
|
this._listeners[type] = this._listeners[type].filter(h => h !== fn);
|
|
}
|
|
}
|
|
dispatchEvent(event) {
|
|
if (!event) return true;
|
|
const handlers = (this._listeners?.[event.type] || []).slice();
|
|
for (const h of handlers) { try { h.call(this, event); } catch(e) { console.error('document event error:', e); } }
|
|
return !event.defaultPrevented;
|
|
}
|
|
createTreeWalker(root, whatToShow, filter) {
|
|
// 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 = {
|
|
root: root,
|
|
currentNode: root,
|
|
whatToShow: whatToShow,
|
|
filter: filter || null,
|
|
// Three-valued per NodeFilter: 1 ACCEPT, 2 REJECT, 3 SKIP. REJECT and
|
|
// SKIP both mean "don't return this node", but only REJECT prunes its
|
|
// descendants, so nextNode() needs to tell them apart (issue #461).
|
|
// A node filtered out by whatToShow is a SKIP: the spec never consults
|
|
// the filter for it, and its descendants stay eligible.
|
|
_filter(node) {
|
|
const nodeType = node.nodeType;
|
|
if (!((whatToShow >> (nodeType - 1)) & 1)) return 3;
|
|
if (this.filter) {
|
|
if (typeof this.filter === 'function') return this.filter(node);
|
|
if (this.filter.acceptNode) return this.filter.acceptNode(node);
|
|
}
|
|
return 1;
|
|
},
|
|
_accept(node) { return this._filter(node) === 1; },
|
|
nextNode() {
|
|
let node = _wrap(+_dom("next_in_subtree", this.root._nid, this.currentNode._nid));
|
|
while (node) {
|
|
const verdict = this._filter(node);
|
|
if (verdict === 1) { this.currentNode = node; return node; }
|
|
// FILTER_REJECT skips the node AND its subtree; FILTER_SKIP (and any
|
|
// other non-accept value) skips only the node.
|
|
const step = verdict === 2 ? "next_after_subtree" : "next_in_subtree";
|
|
node = _wrap(+_dom(step, this.root._nid, node._nid));
|
|
}
|
|
return null;
|
|
},
|
|
// DOM 6.1 "previousNode", implemented as specified (issue #462). The old
|
|
// version looked at exactly one candidate — the previous sibling's
|
|
// deepest last child — and returned null the moment it was filtered out,
|
|
// so a backward walk died mid-tree the way nextNode used to before #432.
|
|
//
|
|
// Unlike nextNode this stays in JS rather than using a DOM traversal op:
|
|
// the descent into last children has to stop on FILTER_REJECT, so the
|
|
// filter is consulted at every step anyway and there is no run of
|
|
// crossings for a native helper to collapse.
|
|
previousNode() {
|
|
let node = this.currentNode;
|
|
while (node !== this.root) {
|
|
let sibling = node.previousSibling;
|
|
while (sibling) {
|
|
node = sibling;
|
|
let verdict = this._filter(node);
|
|
// Descend to the deepest last descendant, but never into a rejected
|
|
// subtree — that is what makes REJECT prune backwards as well.
|
|
while (verdict !== 2 && node.lastChild) {
|
|
node = node.lastChild;
|
|
verdict = this._filter(node);
|
|
}
|
|
if (verdict === 1) { this.currentNode = node; return node; }
|
|
sibling = node.previousSibling;
|
|
}
|
|
const parent = node.parentNode;
|
|
// Reaching root (or a detached node) ends the walk: root is never
|
|
// returned by a backward traversal.
|
|
if (!parent || node === this.root) return null;
|
|
node = parent;
|
|
if (node === this.root) return null;
|
|
if (this._filter(node) === 1) { this.currentNode = node; return node; }
|
|
}
|
|
return null;
|
|
},
|
|
// DOM 6.1 "traverse children" (issue #469). The movers used to step
|
|
// straight to the next sibling when a node was not accepted, so a
|
|
// FILTER_SKIP node hid its children instead of exposing them. `edge` and
|
|
// `step` pick the direction: first/next for forward, last/previous for
|
|
// backward.
|
|
_traverseChildren(edge, step) {
|
|
let node = this.currentNode[edge];
|
|
while (node) {
|
|
const verdict = this._filter(node);
|
|
if (verdict === 1) { this.currentNode = node; return node; }
|
|
// Only SKIP leaves the children eligible; REJECT prunes the subtree.
|
|
if (verdict === 3) {
|
|
const child = node[edge];
|
|
if (child) { node = child; continue; }
|
|
}
|
|
// Subtree exhausted: step sideways, climbing out without passing
|
|
// root or the node the walk started from.
|
|
while (node) {
|
|
const sibling = node[step];
|
|
if (sibling) { node = sibling; break; }
|
|
const parent = node.parentNode;
|
|
if (!parent || parent === this.root || parent === this.currentNode) return null;
|
|
node = parent;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// DOM 6.1 "traverse siblings" (issue #469).
|
|
_traverseSiblings(edge, step) {
|
|
let node = this.currentNode;
|
|
if (node === this.root) return null;
|
|
for (;;) {
|
|
let sibling = node[step];
|
|
while (sibling) {
|
|
node = sibling;
|
|
const verdict = this._filter(node);
|
|
if (verdict === 1) { this.currentNode = node; return node; }
|
|
// Descend into a skipped sibling's subtree; a rejected one is
|
|
// off-limits, and a childless one has nothing to descend into.
|
|
sibling = node[edge];
|
|
if (verdict === 2 || !sibling) sibling = node[step];
|
|
}
|
|
node = node.parentNode;
|
|
if (!node || node === this.root) return null;
|
|
// An accepted parent is where the walk would go next, so there is no
|
|
// sibling to return.
|
|
if (this._filter(node) === 1) return null;
|
|
}
|
|
},
|
|
firstChild() { return this._traverseChildren('firstChild', 'nextSibling'); },
|
|
lastChild() { return this._traverseChildren('lastChild', 'previousSibling'); },
|
|
nextSibling() { return this._traverseSiblings('firstChild', 'nextSibling'); },
|
|
previousSibling() { return this._traverseSiblings('lastChild', 'previousSibling'); },
|
|
// DOM 6.1 "parentNode" (issue #475). The old version looked only at the
|
|
// immediate parent, so it couldn't climb past a skipped ancestor; it also
|
|
// excluded `root` as a result yet stepped to root's own parent when
|
|
// currentNode was root, returning a node OUTSIDE the walker's subtree.
|
|
// The loop's `node !== this.root` guard is what keeps the walk inside
|
|
// root while still allowing root itself to be returned.
|
|
parentNode() {
|
|
let node = this.currentNode;
|
|
while (node && node !== this.root) {
|
|
node = node.parentNode;
|
|
if (node && this._accept(node)) { this.currentNode = node; return node; }
|
|
}
|
|
return null;
|
|
},
|
|
};
|
|
return walker;
|
|
}
|
|
// A real NodeIterator (DOM 6.2), not a TreeWalker in disguise (issue #467).
|
|
// The two differ in more than naming: an iterator's pointer starts *before*
|
|
// its root, so the first nextNode() returns the root itself, and it exposes
|
|
// referenceNode/pointerBeforeReferenceNode/detach rather than a TreeWalker's
|
|
// currentNode and child/sibling movers.
|
|
createNodeIterator(root, whatToShow, filter) {
|
|
// 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);
|
|
return {
|
|
root: root,
|
|
referenceNode: root,
|
|
pointerBeforeReferenceNode: true,
|
|
whatToShow: whatToShow,
|
|
filter: filter || null,
|
|
// NodeIterator prunes nothing: FILTER_REJECT behaves as FILTER_SKIP, so
|
|
// unlike the TreeWalker only "accepted or not" matters here.
|
|
_accept(node) {
|
|
if (!((whatToShow >> (node.nodeType - 1)) & 1)) return false;
|
|
if (this.filter) {
|
|
if (typeof this.filter === 'function') return this.filter(node) === 1;
|
|
if (this.filter.acceptNode) return this.filter.acceptNode(node) === 1;
|
|
}
|
|
return true;
|
|
},
|
|
// DOM 6.2 "traverse". The pointer sits either before or after
|
|
// referenceNode, which is why reversing direction re-yields the current
|
|
// node instead of stepping over it.
|
|
_traverse(forward) {
|
|
let node = this.referenceNode;
|
|
let before = this.pointerBeforeReferenceNode;
|
|
for (;;) {
|
|
if (forward === before) {
|
|
// Consume the pointer's side without moving: it flips to the other
|
|
// side of the node it already references.
|
|
before = !before;
|
|
} else {
|
|
const step = forward ? "next_in_subtree" : "prev_in_subtree";
|
|
const next = _wrap(+_dom(step, this.root._nid, node._nid));
|
|
// A failed traversal leaves referenceNode and the pointer
|
|
// untouched, so the iterator can be resumed in either direction.
|
|
if (!next) return null;
|
|
node = next;
|
|
}
|
|
if (this._accept(node)) break;
|
|
}
|
|
this.referenceNode = node;
|
|
this.pointerBeforeReferenceNode = before;
|
|
return node;
|
|
},
|
|
nextNode() { return this._traverse(true); },
|
|
previousNode() { return this._traverse(false); },
|
|
// Legacy no-op since DOM4, but older library code still calls it and
|
|
// used to hit "detach is not a function".
|
|
detach() {},
|
|
};
|
|
}
|
|
getSelection() { return this.defaultView ? _selectionFor(this) : null; }
|
|
get activeElement() { return globalThis.__obscura_focused || this.body; }
|
|
// The element that scrolls the viewport, and where the page offset lives
|
|
// (issue #468). Standards mode, so documentElement — quirks mode would be
|
|
// body, but we never parse in quirks mode.
|
|
get scrollingElement() { return this.documentElement; }
|
|
get implementation() {
|
|
const ownerDoc = this;
|
|
return {
|
|
// Spec: createHTMLDocument returns a NEW detached Document. jQuery
|
|
// 3.x's selector feature-detect calls `body.innerHTML = '<form>'` on
|
|
// the result — when we returned `globalThis.document`, the real
|
|
// `<body>` was wiped, taking every page on the open web that ships
|
|
// jQuery 3.x with it. Reuse the DOMParser path to build a detached
|
|
// document, then optionally set the title.
|
|
createHTMLDocument(title) {
|
|
// Build head>title and body explicitly. Parsing a full skeleton string
|
|
// as innerHTML of <html> collapses through the fragment parser (it
|
|
// dropped head/body and kept only <title>), leaving doc.body null.
|
|
const doc = new DOMParser().parseFromString("", "text/html");
|
|
const root = doc.documentElement;
|
|
const head = document.createElement("head");
|
|
const titleEl = document.createElement("title");
|
|
if (title != null) titleEl.textContent = String(title);
|
|
head.appendChild(titleEl);
|
|
const body = document.createElement("body");
|
|
root.appendChild(head);
|
|
root.appendChild(body);
|
|
return doc;
|
|
},
|
|
// Real spec: createDocument(namespaceURI, qualifiedName, doctype) →
|
|
// an XML document with a root element of the given name. We don't
|
|
// have a separate XML stack, so return a minimal detached document
|
|
// with an element of the requested local name as documentElement.
|
|
createDocument(_ns, qualifiedName, _doctype) {
|
|
const name = (qualifiedName && String(qualifiedName)) || "root";
|
|
const safe = name.replace(/[^a-zA-Z0-9-]/g, "");
|
|
const html = qualifiedName ? `<${safe}></${safe}>` : "";
|
|
const doc = new DOMParser().parseFromString(html, "application/xml");
|
|
if (_doctype) doc._docType = _doctype;
|
|
return doc;
|
|
},
|
|
// createDocumentType(qualifiedName, publicId, systemId): build a detached
|
|
// DocumentType node. Browsers validate leniently here (only a name with
|
|
// ASCII whitespace or ">" is rejected, matching the WPT cases); the node's
|
|
// owner document is the document whose implementation was used.
|
|
createDocumentType(qualifiedName, publicId, systemId) {
|
|
const name = String(qualifiedName);
|
|
if (name === "" || /[\t\n\f\r >]/.test(name)) {
|
|
throw new DOMException("The qualified name '" + name + "' contains an invalid character", "InvalidCharacterError");
|
|
}
|
|
const dt = new DocumentType(
|
|
+_dom("create_comment_node", ""),
|
|
name,
|
|
publicId === undefined ? "" : String(publicId),
|
|
systemId === undefined ? "" : String(systemId)
|
|
);
|
|
dt._ownerDocument = ownerDoc;
|
|
return dt;
|
|
},
|
|
hasFeature() { return true; },
|
|
};
|
|
}
|
|
get styleSheets() { return []; }
|
|
get forms() { return this.querySelectorAll("form"); }
|
|
get images() { return this.querySelectorAll("img"); }
|
|
get links() { return this.querySelectorAll("a[href], area[href]"); }
|
|
get scripts() { return this.querySelectorAll("script"); }
|
|
get cookie() {
|
|
return Deno.core.ops.op_get_cookies();
|
|
}
|
|
set cookie(v) {
|
|
if (!v) return;
|
|
Deno.core.ops.op_set_cookie(v);
|
|
}
|
|
write(...args) {
|
|
var html = args.join('');
|
|
if (!html) return;
|
|
var body = this.body;
|
|
if (!body) return;
|
|
var temp = this.createElement('div');
|
|
temp.innerHTML = html;
|
|
var children = temp.childNodes;
|
|
for (var i = 0; i < children.length; i++) {
|
|
body.appendChild(children[i]);
|
|
}
|
|
}
|
|
writeln(...args) {
|
|
this.write(args.join('') + '\n');
|
|
}
|
|
open() {
|
|
var body = this.body;
|
|
if (body) body.innerHTML = '';
|
|
return this;
|
|
}
|
|
close() {
|
|
return;
|
|
}
|
|
hasFocus() { return true; }
|
|
execCommand() { return false; }
|
|
}
|
|
|
|
class DocumentFragment extends Node {
|
|
constructor(nid) {
|
|
super(nid !== undefined ? nid : +_dom("create_document_fragment"));
|
|
}
|
|
get nodeType() { return 11; }
|
|
get nodeName() { return "#document-fragment"; }
|
|
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
|
|
set innerHTML(v) { _dom("set_inner_html", this._nid, String(v ?? "")); }
|
|
querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); }
|
|
querySelectorAll(s) {
|
|
const ids = _domParse("query_selector_all_scoped", this._nid, s) || [];
|
|
return _nodeList(ids.map(_wrapEl).filter(Boolean));
|
|
}
|
|
get children() {
|
|
const ids = _domParse("element_children", this._nid) || [];
|
|
return HTMLCollection._from(ids.map(_wrapEl).filter(Boolean));
|
|
}
|
|
get firstElementChild() { return this.children[0] || null; }
|
|
get lastElementChild() { const ch = this.children; return ch[ch.length - 1] || null; }
|
|
getElementById(id) {
|
|
const needle = String(id);
|
|
const stack = Array.from(this.childNodes || []).reverse();
|
|
while (stack.length) {
|
|
const node = stack.pop();
|
|
if (!node) continue;
|
|
if (node.nodeType === 1 && node.id === needle) return node;
|
|
const children = node.childNodes || [];
|
|
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
}
|
|
return null;
|
|
}
|
|
cloneNode(deep) {
|
|
const frag = document.createDocumentFragment();
|
|
if (deep) frag.innerHTML = this.innerHTML;
|
|
return frag;
|
|
}
|
|
}
|
|
|
|
class DocumentType extends Node {
|
|
constructor(nid, name, publicId, systemId) {
|
|
super(nid);
|
|
this._name = name;
|
|
this._publicId = publicId;
|
|
this._systemId = systemId;
|
|
}
|
|
get nodeType() { return 10; }
|
|
get nodeName() { return this._name; }
|
|
get name() { return this._name; }
|
|
get publicId() { return this._publicId; }
|
|
get systemId() { return this._systemId; }
|
|
get nodeValue() { return null; }
|
|
set nodeValue(v) {}
|
|
get ownerDocument() { return this._ownerDocument || globalThis.document; }
|
|
}
|
|
|
|
const _cache = new Map();
|
|
|
|
// Media elements need canPlayType for codec detection fingerprinting.
|
|
// Values match Chrome 145 on Linux x86_64 without proprietary codecs.
|
|
class HTMLMediaElement extends Element {
|
|
canPlayType(type) {
|
|
if (!type || typeof type !== 'string') return '';
|
|
const mime = type.split(';')[0].trim().toLowerCase();
|
|
if (mime === 'video/mp4' || mime === 'video/webm' || mime === 'video/ogg') return 'probably';
|
|
if (mime === 'video/x-matroska') return 'maybe';
|
|
if (mime === 'audio/ogg' || mime === 'audio/webm' || mime === 'audio/wav' ||
|
|
mime === 'audio/mpeg') return 'probably';
|
|
if (mime === 'audio/mp4' || mime === 'audio/x-m4a' || mime === 'audio/aac') return 'maybe';
|
|
return '';
|
|
}
|
|
load() {}
|
|
play() { return Promise.resolve(); }
|
|
pause() {}
|
|
get paused() { return true; }
|
|
get ended() { return false; }
|
|
get readyState() { return 0; }
|
|
get currentTime() { return 0; }
|
|
set currentTime(v) {}
|
|
get duration() { return NaN; }
|
|
get volume() { return 1; }
|
|
set volume(v) {}
|
|
get muted() { return false; }
|
|
set muted(v) {}
|
|
get src() { return this.getAttribute('src') || ''; }
|
|
set src(v) { this.setAttribute('src', v); }
|
|
}
|
|
_markNative(HTMLMediaElement.prototype.canPlayType);
|
|
_markNative(HTMLMediaElement.prototype.play);
|
|
_markNative(HTMLMediaElement.prototype.load);
|
|
_markNative(HTMLMediaElement.prototype.pause);
|
|
class HTMLVideoElement extends HTMLMediaElement {}
|
|
class HTMLAudioElement extends HTMLMediaElement {}
|
|
globalThis.HTMLMediaElement = HTMLMediaElement;
|
|
globalThis.HTMLVideoElement = HTMLVideoElement;
|
|
globalThis.HTMLAudioElement = HTMLAudioElement;
|
|
|
|
function _elementClassFor(nid) {
|
|
const tag = _domParse("tag_name", nid);
|
|
if (tag === "FORM" && globalThis.HTMLFormElement) return globalThis.HTMLFormElement;
|
|
if (tag === "AUDIO") return HTMLAudioElement;
|
|
if (tag === "VIDEO") return HTMLVideoElement;
|
|
return Element;
|
|
}
|
|
function _wrap(nid) {
|
|
if (nid < 0 || nid === null || nid === undefined || isNaN(nid)) return null;
|
|
if (_cache.has(nid)) return _cache.get(nid);
|
|
const t = +_dom("node_type", nid);
|
|
let n;
|
|
if (t === 1) { const C = _elementClassFor(nid); n = new C(nid); }
|
|
else if (t === 3) n = new Text(nid);
|
|
else if (t === 8) n = new Comment(nid);
|
|
else if (t === 9) n = new Document(nid);
|
|
else n = new Node(nid);
|
|
_cache.set(nid, n);
|
|
return n;
|
|
}
|
|
function _wrapEl(nid) {
|
|
if (nid < 0 || nid === null || nid === undefined || isNaN(nid)) return null;
|
|
if (_cache.has(nid)) return _cache.get(nid);
|
|
const C = _elementClassFor(nid);
|
|
const n = new C(nid);
|
|
_cache.set(nid, n);
|
|
return n;
|
|
}
|
|
|
|
globalThis._wrap = _wrap;
|
|
globalThis.self = globalThis;
|
|
|
|
globalThis.document = null;
|
|
function _resolveUrl(url) {
|
|
if (!url) return url;
|
|
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('about:')) return url;
|
|
try { return new URL(url, _domParse("document_url") || "about:blank").href; } catch(e) { return url; }
|
|
}
|
|
// `__virtualUrl` is set by `history.pushState`/`replaceState` (and cleared by
|
|
// any real navigation). When set, `location.href` and friends read it instead
|
|
// of the underlying `document_url`. Without this, client-side routers
|
|
// (Next.js, React Router, vue-router) call `pushState` but the URL never
|
|
// changes, so their `useLocation` hooks return the wrong path and the UI
|
|
// freezes on the original route.
|
|
globalThis.__virtualUrl = null;
|
|
function __currentUrl() {
|
|
return globalThis.__virtualUrl || _domParse("document_url") || "about:blank";
|
|
}
|
|
globalThis.location = {
|
|
get href() { return __currentUrl(); },
|
|
set href(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
|
|
get origin() { try { return new URL(this.href).origin; } catch { return ""; } },
|
|
get protocol() { try { return new URL(this.href).protocol; } catch { return ""; } },
|
|
get host() { try { return new URL(this.href).host; } catch { return ""; } },
|
|
get hostname() { try { return new URL(this.href).hostname; } catch { return ""; } },
|
|
get pathname() { try { return new URL(this.href).pathname; } catch { return "/"; } },
|
|
get search() { try { return new URL(this.href).search; } catch { return ""; } },
|
|
get hash() { try { return new URL(this.href).hash; } catch { return ""; } },
|
|
get port() { try { return new URL(this.href).port; } catch { return ""; } },
|
|
toString() { return this.href; },
|
|
assign(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
|
|
reload() { var r = _resolveUrl(this.href); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
|
|
replace(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
|
|
};
|
|
const _locationObj = globalThis.location;
|
|
Object.defineProperty(globalThis, 'location', {
|
|
get() { return _locationObj; },
|
|
set(url) { var r = _resolveUrl(String(url)); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
|
|
configurable: false,
|
|
enumerable: true,
|
|
});
|
|
|
|
globalThis.window = globalThis;
|
|
globalThis.self = globalThis;
|
|
globalThis.top = globalThis;
|
|
globalThis.parent = globalThis;
|
|
globalThis.frames = globalThis;
|
|
globalThis.frameElement = null;
|
|
globalThis.length = 0;
|
|
|
|
// HTML spec exposes on* event handler IDL attributes via the GlobalEventHandlers
|
|
// mixin on Window, Document, and HTMLElement. Libraries feature-detect the modern
|
|
// event path through these: jQuery checks `("on" + ev) in window`, and React
|
|
// decides whether the `input` event is supported via `("oninput" in document)`.
|
|
// When that check fails React falls back to a legacy change-detection path that
|
|
// never fires onChange for controlled inputs (issue #324). Initialising these to
|
|
// null on all three targets makes the checks match real browsers. On Document and
|
|
// Element they are non-enumerable so they don't surface in `for..in` over nodes.
|
|
for (const _ev of [
|
|
"abort","beforeprint","beforeunload","blur","cancel","canplay","canplaythrough",
|
|
"change","click","close","contextmenu","cuechange","dblclick","drag","dragend",
|
|
"dragenter","dragleave","dragover","dragstart","drop","durationchange","emptied",
|
|
"ended","error","focus","focusin","focusout","formdata","gotpointercapture",
|
|
"hashchange","input","invalid","keydown","keypress","keyup","languagechange",
|
|
"load","loadeddata","loadedmetadata","loadstart","lostpointercapture","message",
|
|
"mousedown","mouseenter","mouseleave","mousemove","mouseout","mouseover","mouseup",
|
|
"offline","online","pagehide","pageshow","paste","pause","play","playing",
|
|
"pointercancel","pointerdown","pointerenter","pointerleave","pointermove",
|
|
"pointerout","pointerover","pointerup","popstate","progress","ratechange",
|
|
"rejectionhandled","reset","resize","scroll","seeked","seeking","select",
|
|
"stalled","storage","submit","suspend","timeupdate","toggle","unhandledrejection",
|
|
"unload","volumechange","waiting","wheel",
|
|
]) {
|
|
const _on = "on" + _ev;
|
|
if (!(_on in globalThis)) globalThis[_on] = null;
|
|
for (const _proto of [Document.prototype, Element.prototype]) {
|
|
if (!(_on in _proto)) {
|
|
Object.defineProperty(_proto, _on, { value: null, writable: true, configurable: true, enumerable: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
globalThis.Window = globalThis.Window || function Window() {};
|
|
Object.defineProperty(globalThis.Window, Symbol.hasInstance, {
|
|
value(obj) { return obj === globalThis || (obj && obj.window === obj); },
|
|
configurable: true,
|
|
});
|
|
|
|
|
|
// Remove the static _iframeRegistry and replace with dynamic getters.
|
|
Object.defineProperty(globalThis, 'length', {
|
|
get() {
|
|
return document.querySelectorAll('iframe').length;
|
|
},
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
|
|
// Since we cannot define a Proxy on globalThis easily, we'll define a reasonable number of indexed getters.
|
|
for (let i = 0; i < 50; i++) {
|
|
Object.defineProperty(globalThis, i, {
|
|
get() {
|
|
const iframes = document.querySelectorAll('iframe');
|
|
if (i < iframes.length) {
|
|
return iframes[i].contentWindow;
|
|
}
|
|
return undefined;
|
|
},
|
|
configurable: true,
|
|
enumerable: false
|
|
});
|
|
}
|
|
|
|
// Navigator constructor so that typeof Navigator !== 'undefined' and
|
|
// navigatorPrototype checks don't throw a ReferenceError.
|
|
function Navigator() {}
|
|
_markNative(Navigator);
|
|
|
|
// PluginArray must exist before navigator is built so the plugins getter can use it.
|
|
function PluginArray(items) {
|
|
for (var _pi = 0; _pi < items.length; _pi++) this[_pi] = items[_pi];
|
|
this.length = items.length;
|
|
}
|
|
PluginArray.prototype = Object.create(Array.prototype);
|
|
PluginArray.prototype.constructor = PluginArray;
|
|
PluginArray.prototype.item = function(i) { return this[i] || null; };
|
|
PluginArray.prototype.namedItem = function(name) {
|
|
for (var _pi = 0; _pi < this.length; _pi++) {
|
|
if (this[_pi].name === name) return this[_pi];
|
|
}
|
|
return null;
|
|
};
|
|
PluginArray.prototype.refresh = function() {};
|
|
PluginArray.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
|
Object.defineProperty(PluginArray.prototype, Symbol.toStringTag, {value: 'PluginArray', configurable: true});
|
|
_markNative(PluginArray);
|
|
_markNative(PluginArray.prototype.item);
|
|
_markNative(PluginArray.prototype.namedItem);
|
|
_markNative(PluginArray.prototype.refresh);
|
|
|
|
// Plugin / MimeType / MimeTypeArray global interfaces. Chrome exposes these as
|
|
// global constructors; their absence threw "ReferenceError: Plugin is not
|
|
// defined" in site bundles that reference them (issue #305). Plain function
|
|
// declarations (no globalThis assignment) so they survive the V8 snapshot, the
|
|
// same pattern PluginArray uses.
|
|
function Plugin(name, filename, description, mimeTypes) {
|
|
this.name = name;
|
|
this.filename = filename;
|
|
this.description = description;
|
|
var mt = mimeTypes || [];
|
|
for (var _i = 0; _i < mt.length; _i++) this[_i] = mt[_i];
|
|
this.length = mt.length;
|
|
}
|
|
Plugin.prototype.item = function(i) { return this[i] || null; };
|
|
Plugin.prototype.namedItem = function(name) {
|
|
for (var _i = 0; _i < this.length; _i++) if (this[_i] && this[_i].type === name) return this[_i];
|
|
return null;
|
|
};
|
|
Plugin.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
|
Object.defineProperty(Plugin.prototype, Symbol.toStringTag, {value: 'Plugin', configurable: true});
|
|
_markNative(Plugin);
|
|
_markNative(Plugin.prototype.item);
|
|
_markNative(Plugin.prototype.namedItem);
|
|
|
|
function MimeType(type, description, suffixes, plugin) {
|
|
this.type = type;
|
|
this.description = description;
|
|
this.suffixes = suffixes;
|
|
this.enabledPlugin = plugin || null;
|
|
}
|
|
Object.defineProperty(MimeType.prototype, Symbol.toStringTag, {value: 'MimeType', configurable: true});
|
|
_markNative(MimeType);
|
|
|
|
function MimeTypeArray(items) {
|
|
for (var _i = 0; _i < items.length; _i++) this[_i] = items[_i];
|
|
this.length = items.length;
|
|
}
|
|
MimeTypeArray.prototype.item = function(i) { return this[i] || null; };
|
|
MimeTypeArray.prototype.namedItem = function(name) {
|
|
for (var _i = 0; _i < this.length; _i++) if (this[_i] && this[_i].type === name) return this[_i];
|
|
return null;
|
|
};
|
|
MimeTypeArray.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
|
Object.defineProperty(MimeTypeArray.prototype, Symbol.toStringTag, {value: 'MimeTypeArray', configurable: true});
|
|
_markNative(MimeTypeArray);
|
|
_markNative(MimeTypeArray.prototype.item);
|
|
_markNative(MimeTypeArray.prototype.namedItem);
|
|
|
|
class NetworkInformation {
|
|
constructor() { _makeListenerBox(this); }
|
|
get downlink() { return 10; }
|
|
get downlinkMax() { return Infinity; }
|
|
get effectiveType() { return '4g'; }
|
|
get rtt() { return 50; }
|
|
get saveData() { return false; }
|
|
get type() { return 'wifi'; }
|
|
get onchange() { return null; }
|
|
set onchange(v) {}
|
|
get ontypechange() { return null; }
|
|
set ontypechange(v) {}
|
|
}
|
|
_markNative(NetworkInformation);
|
|
globalThis.NetworkInformation = NetworkInformation;
|
|
|
|
globalThis.ContentIndex = class ContentIndex {};
|
|
|
|
function _chromeMajor() {
|
|
var m = (globalThis.__obscura_ua || '').match(/Chrome\/(\d+)/);
|
|
return m ? (m[1] | 0) : 145;
|
|
}
|
|
// Chromium derives the sec-ch-ua GREASE brand, version, and brand order
|
|
// deterministically from the Chrome major version
|
|
// (components/embedder_support/user_agent_utils.cc). Replicating it keeps
|
|
// sec-ch-ua and userAgentData exact for every profile version rather than
|
|
// hardcoding one static token.
|
|
var _GREASE_CHARS = [' ', '(', ':', '-', '.', '/', ')', ';', '=', '?', '_'];
|
|
var _GREASE_VER = ['8', '99', '24'];
|
|
var _BRAND_PERMS = [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,0,1],[2,1,0]];
|
|
function _uaBrands() {
|
|
var seed = _chromeMajor();
|
|
var grease = {
|
|
brand: 'Not' + _GREASE_CHARS[seed % 11] + 'A' + _GREASE_CHARS[(seed + 1) % 11] + 'Brand',
|
|
version: _GREASE_VER[seed % 3],
|
|
};
|
|
var ordered = [
|
|
grease,
|
|
{brand: 'Chromium', version: String(seed)},
|
|
{brand: 'Google Chrome', version: String(seed)},
|
|
];
|
|
var p = _BRAND_PERMS[seed % 6];
|
|
return [ordered[p[0]], ordered[p[1]], ordered[p[2]]];
|
|
}
|
|
|
|
// Fingerprint surfaces (UA, plugins, webdriver, etc.) live on the prototype
|
|
// hop below, not as own props here: own accessors are a bot tell.
|
|
globalThis.navigator = {
|
|
onLine: true, cookieEnabled: true,
|
|
maxTouchPoints: 0,
|
|
vendor: "Google Inc.", product: "Gecko", productSub: "20030107",
|
|
doNotTrack: null,
|
|
connection: new NetworkInformation(),
|
|
pdfViewerEnabled: true,
|
|
userAgentData: {
|
|
mobile: false,
|
|
get brands() { return _uaBrands(); },
|
|
get platform() { return globalThis.__obscura_ua_platform || "Windows"; },
|
|
getHighEntropyValues(hints) {
|
|
var brands = _uaBrands();
|
|
return Promise.resolve({
|
|
architecture: "x86",
|
|
bitness: "64",
|
|
brands: brands,
|
|
fullVersionList: brands.map(function(b) { return {brand: b.brand, version: b.version + ".0.0.0"}; }),
|
|
mobile: false,
|
|
model: "",
|
|
platform: globalThis.__obscura_ua_platform || "Windows",
|
|
platformVersion: globalThis.__obscura_ua_platform_version || "15.0.0",
|
|
uaFullVersion: _chromeMajor() + ".0.0.0",
|
|
wow64: false,
|
|
});
|
|
},
|
|
toJSON() { return {brands:this.brands,mobile:this.mobile,platform:this.platform}; },
|
|
},
|
|
serviceWorker: { ready: Promise.resolve(), register(){return Promise.resolve();}, getRegistrations(){return Promise.resolve([]);}, controller: null, oncontrollerchange: null, onmessage: null, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} },
|
|
mediaDevices: {
|
|
enumerateDevices() {
|
|
return Promise.resolve([
|
|
{deviceId:"default",kind:"audioinput",label:"",groupId:"default"},
|
|
{deviceId:"comms",kind:"audioinput",label:"",groupId:"comms"},
|
|
{deviceId:"default",kind:"audiooutput",label:"",groupId:"default"},
|
|
{deviceId:"",kind:"videoinput",label:"",groupId:""},
|
|
]);
|
|
},
|
|
getUserMedia() { return Promise.reject(new DOMException("NotAllowedError")); },
|
|
getDisplayMedia() { return Promise.reject(new DOMException("NotAllowedError")); },
|
|
addEventListener(){}, removeEventListener(){},
|
|
},
|
|
clipboard: { writeText(){return Promise.resolve();}, readText(){return Promise.resolve("");} },
|
|
permissions: { query(params){
|
|
var n = params && params.name;
|
|
// Chrome defaults privacy-sensitive permissions to "prompt", not "granted";
|
|
// returning "granted" for camera/microphone is a bot tell.
|
|
if (n === 'notifications') return Promise.resolve({state: (globalThis.Notification && Notification.permission === 'granted') ? 'granted' : 'prompt', onchange: null});
|
|
if (n === 'geolocation' || n === 'camera' || n === 'microphone' || n === 'midi') return Promise.resolve({state: 'prompt', onchange: null});
|
|
return Promise.resolve({state: 'granted', onchange: null});
|
|
} },
|
|
getBattery() { return Promise.resolve({ charging: _fp('batteryCharging'), chargingTime: _fp('batteryCharging') ? 0 : Infinity, dischargingTime: _fp('batteryCharging') ? Infinity : Math.floor(3600 + _fpRand(250) * 7200), level: _fp('batteryLevel'), addEventListener(){} }); },
|
|
getGamepads() { return []; },
|
|
sendBeacon() { return true; },
|
|
javaEnabled() { return false; },
|
|
geolocation: {
|
|
getCurrentPosition(success, error) {
|
|
const coords = {
|
|
latitude: (globalThis.__obscura_geo_lat ?? 50.1109) + (_fpRand(500) - 0.5) * 0.1,
|
|
longitude: (globalThis.__obscura_geo_lon ?? 8.6821) + (_fpRand(501) - 0.5) * 0.1,
|
|
accuracy: 10 + _fpRand(502) * 40,
|
|
altitude: null,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
};
|
|
const pos = { coords, timestamp: Date.now() };
|
|
if (typeof success === 'function') success(pos);
|
|
},
|
|
watchPosition(success, error) {
|
|
if (typeof success === 'function') {
|
|
const coords = {
|
|
latitude: (globalThis.__obscura_geo_lat ?? 50.1109) + (_fpRand(503) - 0.5) * 0.1,
|
|
longitude: (globalThis.__obscura_geo_lon ?? 8.6821) + (_fpRand(504) - 0.5) * 0.1,
|
|
accuracy: 10 + _fpRand(505) * 40,
|
|
altitude: null,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
};
|
|
success({ coords, timestamp: Date.now() });
|
|
}
|
|
return 0;
|
|
},
|
|
clearWatch() {},
|
|
},
|
|
storage: {
|
|
estimate() { return Promise.resolve({ quota: 5000000000, usage: Math.floor(_fpRand(640) * 100000000) }); },
|
|
persist() { return Promise.resolve(false); },
|
|
persisted() { return Promise.resolve(false); },
|
|
},
|
|
};
|
|
|
|
// Put spoofed navigator props on a thin prototype above Navigator.prototype
|
|
// so hasOwnProperty/getOwnPropertyDescriptor on the instance match Chrome.
|
|
// Getters read __obscura_* lazily (snapshot vs per-page) and are _markNative'd.
|
|
(function() {
|
|
var _navProto = Object.create(Navigator.prototype);
|
|
|
|
function defGetter(key, fn) {
|
|
_markNative(fn);
|
|
Object.defineProperty(_navProto, key, {
|
|
get: fn, set: undefined, enumerable: true, configurable: true,
|
|
});
|
|
}
|
|
|
|
defGetter('webdriver', function() { return false; });
|
|
defGetter('userAgent', function() {
|
|
return globalThis.__obscura_ua ||
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
|
"(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
|
|
});
|
|
defGetter('appVersion', function() {
|
|
return (globalThis.__obscura_ua ||
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
|
"(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36").replace('Mozilla/', '');
|
|
});
|
|
defGetter('platform', function() {
|
|
return globalThis.__obscura_platform || "Win32";
|
|
});
|
|
defGetter('language', function() { return "en-US"; });
|
|
defGetter('languages', function() { return ["en-US", "en"]; });
|
|
|
|
// Cache plugins/mimeTypes so navigator.plugins === navigator.plugins.
|
|
var _plugins = new PluginArray([
|
|
new Plugin("PDF Viewer", "internal-pdf-viewer", "Portable Document Format", []),
|
|
new Plugin("Chrome PDF Viewer", "internal-pdf-viewer", "Portable Document Format", []),
|
|
new Plugin("Chromium PDF Viewer", "internal-pdf-viewer", "Portable Document Format", []),
|
|
new Plugin("Microsoft Edge PDF Viewer", "internal-pdf-viewer", "Portable Document Format", []),
|
|
new Plugin("WebKit built-in PDF", "internal-pdf-viewer", "Portable Document Format", []),
|
|
]);
|
|
var _mimeTypes = new MimeTypeArray([
|
|
new MimeType("application/pdf", "Portable Document Format", "pdf", null),
|
|
new MimeType("text/pdf", "Portable Document Format", "pdf", null),
|
|
]);
|
|
defGetter('plugins', function() { return _plugins; });
|
|
defGetter('mimeTypes', function() { return _mimeTypes; });
|
|
|
|
// Values set per-page by __obscura_init (avoids own data props on navigator).
|
|
defGetter('hardwareConcurrency', function() { return globalThis.__obscura_hw || 8; });
|
|
defGetter('deviceMemory', function() { return globalThis.__obscura_mem || 8; });
|
|
|
|
_navProto.share = _markNative(function share(data) {
|
|
return Promise.reject(new DOMException('Not allowed', 'NotAllowedError'));
|
|
});
|
|
_navProto.canShare = _markNative(function canShare() { return false; });
|
|
|
|
Object.setPrototypeOf(globalThis.navigator, _navProto);
|
|
})();
|
|
|
|
globalThis.chrome = {
|
|
app: { isInstalled: false, InstallState: { DISABLED: "disabled", INSTALLED: "installed", NOT_INSTALLED: "not_installed" }, RunningState: { CANNOT_RUN: "cannot_run", READY_TO_RUN: "ready_to_run", RUNNING: "running" } },
|
|
runtime: { OnInstalledReason: {}, OnRestartRequiredReason: {}, PlatformArch: {}, PlatformNaclArch: {}, PlatformOs: {}, RequestUpdateCheckStatus: {}, connect() { throw new Error("Could not establish connection. Receiving end does not exist."); }, sendMessage() { throw new Error("Could not establish connection. Receiving end does not exist."); } },
|
|
csi() {
|
|
const t = Date.now();
|
|
return { onloadT: t, startE: t - Math.floor(100 + _fpRand(610) * 200), pageT: 0, tran: 5, flashVersion: "" };
|
|
},
|
|
loadTimes() {
|
|
const t = Date.now() / 1000;
|
|
const request = t - 0.5 - _fpRand(611) * 0.5;
|
|
const startLoad = request + 0.05 + _fpRand(612) * 0.02;
|
|
const commit = request + 0.3 + _fpRand(613) * 0.4;
|
|
const finishDoc = commit + 0.1 + _fpRand(614) * 0.2;
|
|
const finish = finishDoc + 0.05 + _fpRand(615) * 0.1;
|
|
const firstPaint = commit + 0.03 + _fpRand(616) * 0.1;
|
|
const navTypes = ["BackForward","Reload","Link","Other"];
|
|
return {
|
|
requestTime: request, startLoadTime: startLoad * 1000, commitLoadTime: commit * 1000,
|
|
finishDocumentLoadTime: finishDoc * 1000, finishLoadTime: finish * 1000,
|
|
firstPaintTime: firstPaint * 1000, firstPaintAfterLoadTime: 0,
|
|
navigationType: navTypes[Math.floor(_fpRand(617) * 4)],
|
|
wasFetchedViaSpdy: false, wasNpnNegotiated: false,
|
|
npnNegotiatedProtocol: "http/1.1",
|
|
wasAlternateProtocolAvailable: false, connectionInfo: "http/1.1",
|
|
};
|
|
},
|
|
};
|
|
|
|
globalThis.Notification = class Notification {
|
|
static permission = "default";
|
|
static requestPermission() { return Promise.resolve(Notification.permission); }
|
|
constructor() {}
|
|
};
|
|
|
|
globalThis.WebGLRenderingContext = class WebGLRenderingContext {};
|
|
globalThis.WebGL2RenderingContext = class WebGL2RenderingContext {};
|
|
|
|
class Screen {
|
|
constructor(w, h) {
|
|
this._w = w; this._h = h;
|
|
this.colorDepth = 24; this.pixelDepth = 24; this.availTop = 0; this.availLeft = 0;
|
|
this.orientation = {type:'landscape-primary',angle:0,addEventListener(){},removeEventListener(){},dispatchEvent(){return true;}};
|
|
}
|
|
get width() { return this._w; }
|
|
get height() { return this._h; }
|
|
get availWidth() { return this._w; }
|
|
get availHeight() { return this._h - 40; }
|
|
}
|
|
['width','height','availWidth','availHeight'].forEach(function(k) {
|
|
var d = Object.getOwnPropertyDescriptor(Screen.prototype, k);
|
|
if (d && d.get) _markNative(d.get);
|
|
});
|
|
globalThis.Screen = Screen;
|
|
globalThis.screen = new Screen(1920, 1080);
|
|
globalThis.visualViewport = { width:1920, height:1000, offsetLeft:0, offsetTop:0, scale:1, addEventListener(){}, removeEventListener(){} };
|
|
globalThis.devicePixelRatio = 1;
|
|
globalThis.innerWidth = 1920; globalThis.innerHeight = 1000;
|
|
globalThis.outerWidth = 1920; globalThis.outerHeight = 1080;
|
|
globalThis.scrollX = 0; globalThis.scrollY = 0;
|
|
globalThis.pageXOffset = 0; globalThis.pageYOffset = 0;
|
|
|
|
globalThis.__fetchInterceptEnabled = false;
|
|
globalThis.__fetchInterceptCallback = null; // Set by CDP to handle paused requests
|
|
|
|
// charCode -> 6-bit value reverse table for base64 decode. -1 for any byte not
|
|
// in the standard alphabet, which mirrors String.indexOf's miss exactly, so the
|
|
// bitmath below stays byte-identical to the old indexOf path including on
|
|
// malformed input. Built once at module load.
|
|
const _B64_DECODE_TABLE = (function () {
|
|
const t = new Int16Array(128).fill(-1);
|
|
const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
for (let i = 0; i < 64; i++) t[a.charCodeAt(i)] = i;
|
|
return t;
|
|
})();
|
|
|
|
function _base64ToUint8Array(b64) {
|
|
const clean = String(b64 || '').replace(/[\r\n\s]/g, '');
|
|
if (!clean) return new Uint8Array();
|
|
const T = _B64_DECODE_TABLE;
|
|
const padding = clean.endsWith('==') ? 2 : (clean.endsWith('=') ? 1 : 0);
|
|
const bytes = new Uint8Array((clean.length * 3 >> 2) - padding);
|
|
let out = 0;
|
|
for (let i = 0; i < clean.length; i += 4) {
|
|
// charCodeAt avoids the per-char substring alloc; T[code] replaces the
|
|
// O(64) indexOf scan. Out-of-range (NaN or code >= 128) folds to -1, and
|
|
// `=== 61` is `=== '='`, so results match the old code exactly.
|
|
const ca = clean.charCodeAt(i); const a = ca < 128 ? T[ca] : -1;
|
|
const cb = clean.charCodeAt(i + 1); const b = cb < 128 ? T[cb] : -1;
|
|
const cc = clean.charCodeAt(i + 2); const c = cc === 61 ? 0 : (cc < 128 ? T[cc] : -1);
|
|
const cd = clean.charCodeAt(i + 3); const d = cd === 61 ? 0 : (cd < 128 ? T[cd] : -1);
|
|
const n = (a << 18) | (b << 12) | (c << 6) | d;
|
|
if (out < bytes.length) bytes[out++] = (n >> 16) & 0xff;
|
|
if (out < bytes.length) bytes[out++] = (n >> 8) & 0xff;
|
|
if (out < bytes.length) bytes[out++] = n & 0xff;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function _bodyToUint8Array(body) {
|
|
if (body == null) return new Uint8Array();
|
|
if (body instanceof Uint8Array) return body;
|
|
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
// obscura's Blob materializes its data into _bytes in the constructor.
|
|
if (body._bytes instanceof Uint8Array) return body._bytes;
|
|
return new TextEncoder().encode(String(body));
|
|
}
|
|
|
|
function _arrayBufferFromBytes(bytes) {
|
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
}
|
|
|
|
function _installWasmStreamingFallback() {
|
|
if (typeof WebAssembly === 'undefined') return;
|
|
if (WebAssembly.instantiateStreaming && WebAssembly.instantiateStreaming.__obscuraFallback) return;
|
|
const nativeInstantiateStreaming = WebAssembly.instantiateStreaming;
|
|
const fallback = async function instantiateStreaming(source, imports) {
|
|
const response = await source;
|
|
if (response && typeof response.arrayBuffer === 'function') {
|
|
return WebAssembly.instantiate(await response.arrayBuffer(), imports);
|
|
}
|
|
if (typeof nativeInstantiateStreaming === 'function') {
|
|
return nativeInstantiateStreaming.call(WebAssembly, response, imports);
|
|
}
|
|
return WebAssembly.instantiate(response, imports);
|
|
};
|
|
fallback.__obscuraFallback = true;
|
|
WebAssembly.instantiateStreaming = fallback;
|
|
}
|
|
_installWasmStreamingFallback();
|
|
|
|
// Serialize a FormData into a multipart/form-data body the way a browser does
|
|
// when it is passed as fetch()/XHR body. The previous shim did String(body),
|
|
// so a FormData became the literal "[object Object]" and the multipart payload
|
|
// (with its boundary) was lost; servers replied "Invalid boundary for
|
|
// multipart/form-data" (e.g. the AWS WAF challenge /mp_verify POST).
|
|
function _formDataToMultipart(fd) {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let bnd = '----WebKitFormBoundary';
|
|
for (let i = 0; i < 16; i++) bnd += chars[Math.floor(Math.random() * chars.length)];
|
|
let out = '';
|
|
const entries = fd._d || [];
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const k = entries[i][0], v = entries[i][1];
|
|
out += '--' + bnd + '\r\n';
|
|
if (v != null && typeof v === 'object' && v._bytes != null) {
|
|
out += 'Content-Disposition: form-data; name="' + k + '"; filename="' + (v.name || 'blob') + '"\r\n';
|
|
out += 'Content-Type: ' + (v.type || 'application/octet-stream') + '\r\n\r\n';
|
|
try { out += new TextDecoder().decode(v._bytes); } catch (e) {}
|
|
out += '\r\n';
|
|
} else {
|
|
out += 'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + String(v) + '\r\n';
|
|
}
|
|
}
|
|
out += '--' + bnd + '--\r\n';
|
|
return { boundary: bnd, body: out };
|
|
}
|
|
|
|
// Coerce a fetch()/XHR body into the string op_fetch_url expects, attaching a
|
|
// Content-Type header for body types that need one (FormData, URLSearchParams).
|
|
function _serializeBody(initBody, headers) {
|
|
if (initBody == null || initBody === '') return '';
|
|
if (initBody instanceof FormData) {
|
|
const mp = _formDataToMultipart(initBody);
|
|
headers['Content-Type'] = 'multipart/form-data; boundary=' + mp.boundary;
|
|
return mp.body;
|
|
}
|
|
if (initBody instanceof URLSearchParams) {
|
|
if (!Object.keys(headers).some(k => k.toLowerCase() === 'content-type')) {
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
|
}
|
|
return initBody.toString();
|
|
}
|
|
if (typeof Blob !== 'undefined' && initBody instanceof Blob) {
|
|
if (initBody.type && !Object.keys(headers).some(k => k.toLowerCase() === 'content-type')) {
|
|
headers['Content-Type'] = initBody.type;
|
|
}
|
|
return _bytesToBinaryString(_bodyToUint8Array(initBody));
|
|
}
|
|
if (typeof ArrayBuffer !== 'undefined' && initBody instanceof ArrayBuffer) {
|
|
const bytes = new Uint8Array(initBody);
|
|
let s = ''; for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
return s;
|
|
}
|
|
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(initBody) && initBody.buffer instanceof ArrayBuffer) {
|
|
const bytes = new Uint8Array(initBody.buffer, initBody.byteOffset, initBody.byteLength);
|
|
let s = ''; for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
return s;
|
|
}
|
|
return typeof initBody === 'string' ? initBody : String(initBody);
|
|
}
|
|
|
|
globalThis.fetch = async (input, init = {}) => {
|
|
let url = typeof input === "string"
|
|
? input
|
|
: (input instanceof Request
|
|
? input.url
|
|
: ((typeof URL === 'function' && input instanceof URL) ? input.href : (input?.url || input?.href || String(input || ""))));
|
|
if (url && !url.includes('://')) {
|
|
try {
|
|
const base = _domParse("document_url") || "about:blank";
|
|
url = new URL(url, base).href;
|
|
} catch(e) { /* keep as-is if URL resolution fails */ }
|
|
}
|
|
const method = init.method || (input instanceof Request ? input.method : "GET");
|
|
let _h = init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : (init.headers || {});
|
|
const body = _serializeBody(init.body, _h);
|
|
const hdrs = JSON.stringify(_h);
|
|
const fetchMode = init.mode || (input instanceof Request ? input.mode : "cors");
|
|
const pageOrigin = (function() { try { const u = new URL(_domParse("document_url") || "about:blank"); return u.origin; } catch(e) { return ""; } })();
|
|
const raw = await Deno.core.ops.op_fetch_url(url, method, hdrs, body, pageOrigin, fetchMode);
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed.blocked) {
|
|
const err = new TypeError('net::ERR_FAILED');
|
|
err.name = 'AbortError';
|
|
err.__aborted = true;
|
|
throw err;
|
|
}
|
|
if (parsed.corsBlocked) {
|
|
throw new TypeError('Failed to fetch: ' + (parsed.corsError || 'CORS error'));
|
|
}
|
|
const respType = parsed.status === 0 ? "opaque" : (fetchMode === "no-cors" ? "opaque" : "basic");
|
|
const responseBody = parsed.bodyBase64 ? _base64ToUint8Array(parsed.bodyBase64) : (parsed.body || "");
|
|
const response = new Response(responseBody, {
|
|
status: parsed.status,
|
|
statusText: "",
|
|
headers: parsed.headers || {},
|
|
type: respType,
|
|
url: parsed.url || url,
|
|
redirected: false,
|
|
});
|
|
if (parsed.requestId) {
|
|
Object.defineProperty(response, "__obscuraRequestId", {
|
|
value: parsed.requestId,
|
|
configurable: true,
|
|
});
|
|
}
|
|
return response;
|
|
};
|
|
|
|
if (typeof Headers === "undefined") {
|
|
globalThis.Headers = class Headers {
|
|
constructor(init={}) { this._h={}; if(init) { if(init instanceof Headers) { init.forEach((v,k)=>{this._h[k]=v;}); } else if(typeof init==="object") { for(const[k,v]of Object.entries(init)) this._h[k.toLowerCase()]=String(v); } } }
|
|
get(n) { return this._h[n.toLowerCase()]??null; } set(n,v) { this._h[n.toLowerCase()]=String(v); }
|
|
has(n) { return n.toLowerCase() in this._h; } delete(n) { delete this._h[n.toLowerCase()]; }
|
|
append(n,v) { this._h[n.toLowerCase()]=String(v); }
|
|
forEach(cb) { for(const[k,v] of Object.entries(this._h)) cb(v,k,this); }
|
|
entries() { return Object.entries(this._h)[Symbol.iterator](); }
|
|
keys() { return Object.keys(this._h)[Symbol.iterator](); }
|
|
values() { return Object.values(this._h)[Symbol.iterator](); }
|
|
[Symbol.iterator]() { return this.entries(); }
|
|
};
|
|
}
|
|
|
|
// XMLHttpRequestEventTarget — spec-required ancestor for XHR EventTarget methods.
|
|
// zone.js prefers to walk XMLHttpRequestEventTarget.prototype for addEventListener/
|
|
// removeEventListener/dispatchEvent descriptors before falling back to XHR.prototype.
|
|
class XMLHttpRequestEventTarget {
|
|
addEventListener(type, handler) {
|
|
if (!this._listeners) this._listeners = {};
|
|
if (!this._listeners[type]) this._listeners[type] = [];
|
|
this._listeners[type].push(handler);
|
|
}
|
|
removeEventListener(type, handler) {
|
|
if (this._listeners && this._listeners[type]) {
|
|
this._listeners[type] = this._listeners[type].filter(h => h !== handler);
|
|
}
|
|
}
|
|
dispatchEvent(event) {
|
|
if (!event || !event.type) return false;
|
|
const ev = (typeof event === 'object') ? event : { type: event };
|
|
ev.target = ev.target || this;
|
|
ev.currentTarget = ev.currentTarget || this;
|
|
const type = ev.type;
|
|
const handlers = (this._listeners && this._listeners[type]) || [];
|
|
for (const h of handlers) { try { h.call(this, ev); } catch (e) {} }
|
|
const prop = 'on' + type;
|
|
if (typeof this[prop] === 'function') {
|
|
try { this[prop](ev); } catch (e) {}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
globalThis.XMLHttpRequestEventTarget = XMLHttpRequestEventTarget;
|
|
_markNative(XMLHttpRequestEventTarget);
|
|
_markNative(XMLHttpRequestEventTarget.prototype.addEventListener);
|
|
_markNative(XMLHttpRequestEventTarget.prototype.removeEventListener);
|
|
_markNative(XMLHttpRequestEventTarget.prototype.dispatchEvent);
|
|
|
|
globalThis.XMLHttpRequest = class XMLHttpRequest extends XMLHttpRequestEventTarget {
|
|
static UNSENT = 0;
|
|
static OPENED = 1;
|
|
static HEADERS_RECEIVED = 2;
|
|
static LOADING = 3;
|
|
static DONE = 4;
|
|
UNSENT = 0; OPENED = 1; HEADERS_RECEIVED = 2; LOADING = 3; DONE = 4;
|
|
|
|
constructor() {
|
|
super();
|
|
this.readyState = 0;
|
|
this.status = 0;
|
|
this.statusText = "";
|
|
this.responseText = "";
|
|
this.responseXML = null;
|
|
this.responseURL = "";
|
|
this.responseType = "";
|
|
this.response = null;
|
|
this.timeout = 0;
|
|
this.withCredentials = false;
|
|
this.upload = { addEventListener(){}, removeEventListener(){} };
|
|
this._method = "GET";
|
|
this._url = "";
|
|
this._headers = {};
|
|
this._responseHeaders = {};
|
|
this._aborted = false;
|
|
this._listeners = {};
|
|
this.onreadystatechange = null;
|
|
this.onload = null;
|
|
this.onerror = null;
|
|
this.onabort = null;
|
|
this.onprogress = null;
|
|
this.ontimeout = null;
|
|
this.onloadstart = null;
|
|
this.onloadend = null;
|
|
}
|
|
|
|
open(method, url, async_) {
|
|
this._method = method;
|
|
this._url = url;
|
|
this._headers = {};
|
|
this._responseHeaders = {};
|
|
this._aborted = false;
|
|
this.status = 0;
|
|
this.statusText = "";
|
|
this.responseText = "";
|
|
this.response = null;
|
|
this._setReadyState(1);
|
|
}
|
|
|
|
setRequestHeader(name, value) {
|
|
this._headers[name] = value;
|
|
}
|
|
|
|
getResponseHeader(name) {
|
|
const lower = name.toLowerCase();
|
|
for (const [k, v] of Object.entries(this._responseHeaders)) {
|
|
if (k.toLowerCase() === lower) return v;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getAllResponseHeaders() {
|
|
return Object.entries(this._responseHeaders)
|
|
.map(([k, v]) => k + ': ' + v)
|
|
.join('\r\n');
|
|
}
|
|
|
|
overrideMimeType(mime) { this._overrideMime = mime; }
|
|
|
|
send(body) {
|
|
if (this.readyState !== 1) return;
|
|
if (this._aborted) return;
|
|
|
|
const xhr = this;
|
|
this._fireEvent('loadstart');
|
|
|
|
let url = this._url;
|
|
if (url && !url.includes('://')) {
|
|
try {
|
|
const base = _domParse("document_url") || "about:blank";
|
|
url = new URL(url, base).href;
|
|
} catch(e) {}
|
|
}
|
|
|
|
fetch(url, {
|
|
method: this._method,
|
|
headers: this._headers,
|
|
body: body || undefined,
|
|
mode: 'cors',
|
|
}).then(async (resp) => {
|
|
if (xhr._aborted) return;
|
|
|
|
xhr.status = resp.status;
|
|
xhr.statusText = resp.statusText || '';
|
|
xhr.responseURL = resp.url || url;
|
|
|
|
if (resp.headers) {
|
|
resp.headers.forEach((v, k) => { xhr._responseHeaders[k] = v; });
|
|
}
|
|
|
|
xhr._setReadyState(2); // HEADERS_RECEIVED
|
|
|
|
const text = await resp.text();
|
|
if (xhr._aborted) return;
|
|
|
|
xhr.responseText = text;
|
|
xhr._setReadyState(3); // LOADING
|
|
|
|
switch (xhr.responseType) {
|
|
case 'json':
|
|
try { xhr.response = JSON.parse(text); } catch(e) { xhr.response = null; }
|
|
break;
|
|
case 'text':
|
|
case '':
|
|
xhr.response = text;
|
|
break;
|
|
case 'arraybuffer':
|
|
xhr.response = new TextEncoder().encode(text).buffer;
|
|
break;
|
|
case 'blob':
|
|
xhr.response = new Blob([text]);
|
|
break;
|
|
case 'document':
|
|
xhr.response = text; // simplified
|
|
break;
|
|
default:
|
|
xhr.response = text;
|
|
}
|
|
|
|
xhr._setReadyState(4); // DONE
|
|
xhr._fireEvent('load');
|
|
xhr._fireEvent('loadend');
|
|
}).catch((err) => {
|
|
if (xhr._aborted) return;
|
|
xhr.status = 0;
|
|
xhr.readyState = 4;
|
|
xhr._fireEvent('readystatechange');
|
|
if (err && err.__aborted) {
|
|
xhr._aborted = true;
|
|
xhr._fireEvent('abort');
|
|
xhr._fireEvent('loadend');
|
|
if (xhr.onabort) xhr.onabort(err);
|
|
} else {
|
|
xhr._fireEvent('error');
|
|
xhr._fireEvent('loadend');
|
|
if (xhr.onerror) xhr.onerror(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
abort() {
|
|
this._aborted = true;
|
|
if (this.readyState > 0 && this.readyState < 4) {
|
|
this._setReadyState(4);
|
|
this._fireEvent('abort');
|
|
this._fireEvent('loadend');
|
|
}
|
|
this.readyState = 0;
|
|
}
|
|
|
|
addEventListener(type, handler) {
|
|
if (!this._listeners[type]) this._listeners[type] = [];
|
|
this._listeners[type].push(handler);
|
|
}
|
|
|
|
removeEventListener(type, handler) {
|
|
if (this._listeners[type]) {
|
|
this._listeners[type] = this._listeners[type].filter(h => h !== handler);
|
|
}
|
|
}
|
|
|
|
// Per WHATWG DOM spec — required by zone.js which patches XHR via
|
|
// Object.getOwnPropertyDescriptor on XMLHttpRequestEventTarget.prototype.
|
|
dispatchEvent(event) {
|
|
if (!event || !event.type) return false;
|
|
const ev = (typeof event === 'object') ? event : { type: event };
|
|
ev.target = ev.target || this;
|
|
ev.currentTarget = ev.currentTarget || this;
|
|
const type = ev.type;
|
|
const handlers = (this._listeners && this._listeners[type]) || [];
|
|
for (const h of handlers) { try { h.call(this, ev); } catch (e) {} }
|
|
const prop = 'on' + type;
|
|
if (typeof this[prop] === 'function') {
|
|
try { this[prop](ev); } catch (e) {}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_setReadyState(state) {
|
|
this.readyState = state;
|
|
this._fireEvent('readystatechange');
|
|
if (this.onreadystatechange) {
|
|
try { this.onreadystatechange(); } catch(e) {}
|
|
}
|
|
}
|
|
|
|
_fireEvent(type) {
|
|
const event = { type, target: this, currentTarget: this, bubbles: false };
|
|
const handlers = this._listeners[type] || [];
|
|
for (const h of handlers) { try { h.call(this, event); } catch(e) {} }
|
|
const prop = 'on' + type;
|
|
if (type !== 'readystatechange' && typeof this[prop] === 'function') {
|
|
try { this[prop](event); } catch(e) {}
|
|
}
|
|
}
|
|
};
|
|
_markNative(XMLHttpRequest);
|
|
_markNative(XMLHttpRequest.prototype.open);
|
|
_markNative(XMLHttpRequest.prototype.send);
|
|
_markNative(XMLHttpRequest.prototype.abort);
|
|
_markNative(XMLHttpRequest.prototype.setRequestHeader);
|
|
_markNative(XMLHttpRequest.prototype.addEventListener);
|
|
_markNative(XMLHttpRequest.prototype.removeEventListener);
|
|
_markNative(XMLHttpRequest.prototype.dispatchEvent);
|
|
_markNative(XMLHttpRequest.prototype.getResponseHeader);
|
|
_markNative(XMLHttpRequest.prototype.getAllResponseHeaders);
|
|
|
|
// WHATWG URL parsing/serialization is delegated to the Rust `url` crate via
|
|
// op_url_parse / op_url_set. The op returns the full component set as JSON; the
|
|
// constructor caches it so getters are plain field reads (no per-access op) and
|
|
// the hot paths (navigation, fetch, _resolveUrl) stay cheap. Returns null when
|
|
// the input is not a valid URL.
|
|
function _urlParseOp(url, base) {
|
|
try {
|
|
const s = Deno.core.ops.op_url_parse(String(url), (base === undefined || base === null) ? "" : String(base));
|
|
const c = JSON.parse(s);
|
|
return (c && c.ok) ? c : null;
|
|
} catch (e) { return null; }
|
|
}
|
|
function _urlSetOp(href, part, value) {
|
|
try {
|
|
const s = Deno.core.ops.op_url_set(String(href), part, String(value));
|
|
const c = JSON.parse(s);
|
|
return (c && c.ok) ? c : null;
|
|
} catch (e) { return null; }
|
|
}
|
|
// Returns just the resolved absolute URL string (no component JSON), or null on
|
|
// failure. Cheaper than _urlParseOp for callers that only need the href.
|
|
function _urlResolveOp(href, base) {
|
|
try {
|
|
const r = Deno.core.ops.op_url_resolve(String(href), (base === undefined || base === null) ? "" : String(base));
|
|
return r ? r : null;
|
|
} catch (e) { return null; }
|
|
}
|
|
if (typeof URL === 'undefined' || !URL.prototype || !URL.__obscura) {
|
|
const _URL = class URL {
|
|
constructor(url, base) {
|
|
const c = _urlParseOp(url, base);
|
|
if (!c) throw new TypeError("Failed to construct 'URL': Invalid URL");
|
|
this._c = c;
|
|
this._sp = null;
|
|
}
|
|
get href() { return this._c.href; }
|
|
set href(v) { const c = _urlParseOp(v, undefined); if (!c) throw new TypeError("Failed to set the 'href' property on 'URL': Invalid URL"); this._c = c; this._refreshSP(); }
|
|
get protocol() { return this._c.protocol; }
|
|
set protocol(v) { this._set('protocol', v); }
|
|
get username() { return this._c.username; }
|
|
set username(v) { this._set('username', v); }
|
|
get password() { return this._c.password; }
|
|
set password(v) { this._set('password', v); }
|
|
get host() { return this._c.host; }
|
|
set host(v) { this._set('host', v); }
|
|
get hostname() { return this._c.hostname; }
|
|
set hostname(v) { this._set('hostname', v); }
|
|
get port() { return this._c.port; }
|
|
set port(v) { this._set('port', v); }
|
|
get pathname() { return this._c.pathname; }
|
|
set pathname(v) { this._set('pathname', v); }
|
|
get search() { return this._c.search; }
|
|
set search(v) { this._set('search', v); this._refreshSP(); }
|
|
get hash() { return this._c.hash; }
|
|
set hash(v) { this._set('hash', v); }
|
|
get origin() { return this._c.origin; }
|
|
get searchParams() {
|
|
if (!this._sp) { this._sp = new URLSearchParams(this._c.search); this._sp._url = this; }
|
|
return this._sp;
|
|
}
|
|
_set(part, value) { const c = _urlSetOp(this._c.href, part, value); if (c) this._c = c; }
|
|
// search changed on the URL side: refresh the bound searchParams contents.
|
|
_refreshSP() { if (this._sp && this._sp._setFromString) this._sp._setFromString(this._c.search); }
|
|
// searchParams mutated: write the serialized query back without re-refreshing.
|
|
_updateSearch(qs) { this._set('search', qs ? ('?' + qs) : ''); }
|
|
toString() { return this._c.href; }
|
|
toJSON() { return this._c.href; }
|
|
static createObjectURL() { return 'blob:null/fake-' + Math.random().toString(36).slice(2); }
|
|
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;
|
|
globalThis.URL = _URL;
|
|
}
|
|
|
|
globalThis.requestIdleCallback = globalThis.requestIdleCallback || function requestIdleCallback(cb, opts) {
|
|
const start = Date.now();
|
|
return setTimeout(() => {
|
|
cb({
|
|
didTimeout: false,
|
|
timeRemaining() { return Math.max(0, 50 - (Date.now() - start)); },
|
|
});
|
|
}, 1);
|
|
};
|
|
globalThis.cancelIdleCallback = globalThis.cancelIdleCallback || function cancelIdleCallback(id) { clearTimeout(id); };
|
|
_markNative(globalThis.requestIdleCallback);
|
|
_markNative(globalThis.cancelIdleCallback);
|
|
|
|
if (typeof Request === 'undefined') {
|
|
globalThis.Request = class Request {
|
|
constructor(input, init = {}) {
|
|
if (typeof input === 'string') { this.url = input; }
|
|
else if (input instanceof Request) { this.url = input.url; init = { ...input, ...init }; }
|
|
else if (typeof URL === 'function' && input instanceof URL) { this.url = input.href; }
|
|
else { this.url = input?.url || input?.href || String(input); }
|
|
this.method = (init.method || 'GET').toUpperCase();
|
|
this.headers = new Headers(init.headers);
|
|
this.body = init.body || null;
|
|
this.mode = init.mode || 'cors';
|
|
this.credentials = init.credentials || 'same-origin';
|
|
this.redirect = init.redirect || 'follow';
|
|
this.referrer = init.referrer || '';
|
|
this.signal = init.signal || { aborted: false, addEventListener(){}, removeEventListener(){} };
|
|
this.cache = init.cache || 'default';
|
|
}
|
|
clone() { return new Request(this.url, { method: this.method, headers: this.headers, body: this.body }); }
|
|
async text() { return this.body ? String(this.body) : ''; }
|
|
async json() { return JSON.parse(await this.text()); }
|
|
async arrayBuffer() { return new TextEncoder().encode(await this.text()).buffer; }
|
|
async blob() {
|
|
const ct = this.headers && this.headers.get ? (this.headers.get('content-type') || '') : '';
|
|
return new Blob(this.body != null ? [this.body] : [], { type: ct });
|
|
}
|
|
};
|
|
}
|
|
|
|
// Decode a response body honoring the Content-Type charset, so fetch()/XHR
|
|
// over non-UTF-8 resources (GBK, Shift_JIS, ISO-8859-x, ...) return correctly
|
|
// decoded text instead of mojibake. The UTF-8 case (the overwhelming majority)
|
|
// takes the plain TextDecoder fast path; only an explicit non-UTF-8 charset
|
|
// routes through TextDecoder(label), which falls back to UTF-8 on a bad label.
|
|
function _decodeBodyWithCharset(bytes, headers) {
|
|
let label = '';
|
|
try {
|
|
const ct = headers && typeof headers.get === 'function' ? (headers.get('content-type') || '') : '';
|
|
const m = /charset\s*=\s*"?([^";]+)"?/i.exec(ct);
|
|
if (m) label = m[1].trim();
|
|
} catch (e) {}
|
|
if (!label || /^utf-?8$/i.test(label)) return new TextDecoder().decode(bytes);
|
|
try { return new TextDecoder(label).decode(bytes); }
|
|
catch (e) { return new TextDecoder().decode(bytes); }
|
|
}
|
|
|
|
if (typeof Response === 'undefined') {
|
|
globalThis.Response = class Response {
|
|
constructor(body, init = {}) {
|
|
this._bodyBytes = _bodyToUint8Array(body); this.status = init.status || 200; this.statusText = init.statusText || '';
|
|
this.ok = this.status >= 200 && this.status < 300;
|
|
this.headers = new Headers(init.headers);
|
|
this.type = init.type || 'basic'; this.url = init.url || ''; this.redirected = !!init.redirected;
|
|
}
|
|
async text() { return _decodeBodyWithCharset(this._bodyBytes, this.headers); }
|
|
async json() { return JSON.parse(await this.text()); }
|
|
async arrayBuffer() { return _arrayBufferFromBytes(this._bodyBytes); }
|
|
async blob() { return new Blob([this._bodyBytes]); }
|
|
clone() { return new Response(this._bodyBytes, { status: this.status, statusText: this.statusText, headers: this.headers, type: this.type, url: this.url, redirected: this.redirected }); }
|
|
static error() { return new Response(null, { status: 0 }); }
|
|
static redirect(url, status) { return new Response(null, { status: status || 302, headers: { Location: url } }); }
|
|
static json(data, init) { return new Response(JSON.stringify(data), { ...init, headers: { 'content-type': 'application/json', ...(init?.headers || {}) } }); }
|
|
};
|
|
}
|
|
|
|
if (!Element.prototype.replaceWith) {
|
|
// _convertNodes turns any non-node argument (numbers, booleans, null, …) into
|
|
// a Text node via String(n), matching the spec and append()/prepend(); the
|
|
// old `typeof n === 'string'` check corrupted insert_before for other types.
|
|
Element.prototype.replaceWith = function(...nodes) {
|
|
const parent = this.parentNode;
|
|
if (!parent) return;
|
|
for (const n of _convertNodes(nodes)) parent.insertBefore(n, this);
|
|
parent.removeChild(this);
|
|
};
|
|
_markNative(Element.prototype.replaceWith);
|
|
}
|
|
if (!Element.prototype.before) {
|
|
Element.prototype.before = function(...nodes) {
|
|
const parent = this.parentNode;
|
|
if (!parent) return;
|
|
for (const n of _convertNodes(nodes)) parent.insertBefore(n, this);
|
|
};
|
|
_markNative(Element.prototype.before);
|
|
}
|
|
if (!Element.prototype.after) {
|
|
Element.prototype.after = function(...nodes) {
|
|
const parent = this.parentNode;
|
|
if (!parent) return;
|
|
const ref = this.nextSibling;
|
|
for (const n of _convertNodes(nodes)) parent.insertBefore(n, ref);
|
|
};
|
|
_markNative(Element.prototype.after);
|
|
}
|
|
|
|
// ChildNode mixin: also mix before/after/replaceWith/remove into
|
|
// CharacterData.prototype (covers Text, Comment, ProcessingInstruction).
|
|
// These are the same implementations as Element.prototype — frameworks
|
|
// (Svelte 5, Vue, Lit) anchor on Comment/Text nodes and call these methods.
|
|
if (!CharacterData.prototype.before) CharacterData.prototype.before = Element.prototype.before;
|
|
if (!CharacterData.prototype.after) CharacterData.prototype.after = Element.prototype.after;
|
|
if (!CharacterData.prototype.replaceWith) CharacterData.prototype.replaceWith = Element.prototype.replaceWith;
|
|
if (!CharacterData.prototype.remove) CharacterData.prototype.remove = Element.prototype.remove;
|
|
|
|
if (!('isConnected' in Node.prototype)) {
|
|
Object.defineProperty(Node.prototype, 'isConnected', {
|
|
get() {
|
|
let node = this;
|
|
while (node) {
|
|
if (node.nodeType === 9) return true; // Document node
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
globalThis.ResizeObserver = class ResizeObserver {
|
|
constructor(callback) {
|
|
this._callback = callback;
|
|
this._targets = new Set();
|
|
this._connected = true;
|
|
this._fireCount = 0;
|
|
}
|
|
_fireFor(targets) {
|
|
if (!this._connected || !targets.length) return;
|
|
const records = targets.map(target => {
|
|
const r = target.getBoundingClientRect ? target.getBoundingClientRect() : { x: 0, y: 0, width: 100, height: 20 };
|
|
return {
|
|
target,
|
|
contentRect: { x: r.x || 0, y: r.y || 0, width: r.width || 100, height: r.height || 20, top: r.top || 0, left: r.left || 0, bottom: r.bottom || 20, right: r.right || 100 },
|
|
borderBoxSize: [{ blockSize: r.height || 20, inlineSize: r.width || 100 }],
|
|
contentBoxSize: [{ blockSize: r.height || 20, inlineSize: r.width || 100 }],
|
|
devicePixelContentBoxSize: [{ blockSize: r.height || 20, inlineSize: r.width || 100 }],
|
|
};
|
|
});
|
|
try { this._callback(records, this); } catch (e) { /* RO callbacks must not propagate */ }
|
|
}
|
|
observe(el) {
|
|
if (!el || !this._connected) return;
|
|
if (this._targets.has(el)) return;
|
|
this._targets.add(el);
|
|
Promise.resolve().then(() => this._fireFor([el]));
|
|
[200, 800].forEach(delay => {
|
|
setTimeout(() => {
|
|
if (this._connected && this._targets.has(el) && this._fireCount < 16) {
|
|
this._fireCount++;
|
|
this._fireFor([el]);
|
|
}
|
|
}, delay);
|
|
});
|
|
}
|
|
unobserve(el) { this._targets.delete(el); }
|
|
disconnect() { this._connected = false; this._targets.clear(); }
|
|
};
|
|
|
|
if (typeof TextEncoder === 'undefined') {
|
|
globalThis.TextEncoder = class TextEncoder {
|
|
get encoding() { return 'utf-8'; }
|
|
encode(str) {
|
|
str = String(str);
|
|
const buf = [];
|
|
for (let i = 0; i < str.length; i++) {
|
|
let c = str.charCodeAt(i);
|
|
if (c < 0x80) buf.push(c);
|
|
else if (c < 0x800) { buf.push(0xC0|(c>>6), 0x80|(c&0x3F)); }
|
|
else if (c < 0xD800 || c >= 0xE000) { buf.push(0xE0|(c>>12), 0x80|((c>>6)&0x3F), 0x80|(c&0x3F)); }
|
|
else { c = 0x10000 + (((c & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF)); buf.push(0xF0|(c>>18), 0x80|((c>>12)&0x3F), 0x80|((c>>6)&0x3F), 0x80|(c&0x3F)); }
|
|
}
|
|
return new Uint8Array(buf);
|
|
}
|
|
encodeInto(str, dest) { const enc = this.encode(str); dest.set(enc.slice(0, dest.length)); return { read: str.length, written: Math.min(enc.length, dest.length) }; }
|
|
};
|
|
}
|
|
// Fast pure-JS UTF-8 decode (the common case: Response/Blob .text(), most
|
|
// pages). Avoids the op + JSON round trip for plain UTF-8.
|
|
function _utf8DecodeBytes(bytes, start) {
|
|
let str = '', i = start | 0;
|
|
const n = bytes.length;
|
|
while (i < n) {
|
|
let c = bytes[i++];
|
|
if (c < 0x80) str += String.fromCharCode(c);
|
|
else if (c < 0xE0) str += String.fromCharCode(((c & 0x1F) << 6) | (bytes[i++] & 0x3F));
|
|
else if (c < 0xF0) { const b1 = bytes[i++], b2 = bytes[i++]; str += String.fromCharCode(((c & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F)); }
|
|
else { const b1 = bytes[i++], b2 = bytes[i++], b3 = bytes[i++]; const cp = ((c & 0x07) << 18) | ((b1 & 0x3F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F); if (cp > 0xFFFF) { const s = cp - 0x10000; str += String.fromCharCode(0xD800 + (s >> 10), 0xDC00 + (s & 0x3FF)); } else str += String.fromCharCode(cp); }
|
|
}
|
|
return str;
|
|
}
|
|
if (typeof TextDecoder === 'undefined') {
|
|
globalThis.TextDecoder = class TextDecoder {
|
|
constructor(label, options) {
|
|
// No-arg construction (Response.text()/Blob.text() and most pages) is
|
|
// UTF-8; skip the label-validation op on that hot path.
|
|
let name;
|
|
if (label === undefined) {
|
|
name = 'utf-8';
|
|
} else {
|
|
name = Deno.core.ops.op_encoding_for_label(String(label));
|
|
if (!name) throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('" + label + "') is invalid.");
|
|
}
|
|
const o = options || {};
|
|
Object.defineProperty(this, 'encoding', { value: name, enumerable: true });
|
|
Object.defineProperty(this, 'fatal', { value: !!o.fatal, enumerable: true });
|
|
Object.defineProperty(this, 'ignoreBOM', { value: !!o.ignoreBOM, enumerable: true });
|
|
}
|
|
decode(input, options) {
|
|
if (input === undefined) return '';
|
|
const bytes = ArrayBuffer.isView(input)
|
|
? new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
|
|
: new Uint8Array(input);
|
|
// Fast path: plain UTF-8, non-fatal (Response/Blob text, most pages).
|
|
if (this.encoding === 'utf-8' && !this.fatal) {
|
|
let off = 0;
|
|
if (!this.ignoreBOM && bytes.length >= 3 && bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) off = 3;
|
|
return _utf8DecodeBytes(bytes, off);
|
|
}
|
|
// Legacy encodings / fatal mode: encoding_rs via the op.
|
|
const r = JSON.parse(Deno.core.ops.op_text_decode(this.encoding, bytes, this.fatal, this.ignoreBOM));
|
|
if (!r.ok) throw new TypeError("Failed to execute 'decode' on 'TextDecoder': The encoded data was not valid.");
|
|
return r.v;
|
|
}
|
|
};
|
|
}
|
|
|
|
globalThis.matchMedia = _markNative(function matchMedia(q) {
|
|
var s = (q || '').toLowerCase().replace(/\s+/g, '');
|
|
var matches = false;
|
|
if (s.includes('prefers-color-scheme:light')) matches = false;
|
|
else if (s.includes('prefers-color-scheme:dark')) matches = true;
|
|
else if (s.includes('prefers-reduced-motion:no-preference')) matches = true;
|
|
else if (s.includes('prefers-reduced-motion:reduce')) matches = false;
|
|
else if (s.includes('any-pointer:fine')) matches = true;
|
|
else if (s.includes('any-pointer:coarse')) matches = false;
|
|
else if (s.includes('pointer:fine')) matches = true;
|
|
else if (s.includes('hover:hover')) matches = true;
|
|
else if (s.includes('any-hover:hover')) matches = true;
|
|
else if (s.includes('color)') || s === '(color)') matches = true;
|
|
else if (s.includes('min-width')) {
|
|
var m = s.match(/min-width:\s*(\d+)px/);
|
|
matches = m ? (globalThis.innerWidth || 1440) >= parseInt(m[1]) : false;
|
|
}
|
|
else if (s.includes('max-width')) {
|
|
var m2 = s.match(/max-width:\s*(\d+)px/);
|
|
matches = m2 ? (globalThis.innerWidth || 1440) <= parseInt(m2[1]) : false;
|
|
}
|
|
return { matches: matches, media: q, onchange: null, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} };
|
|
});
|
|
globalThis.getComputedStyle = (el) => {
|
|
if (!el) el = document.body || {};
|
|
const style = el?.style || el?._style || new CSSStyleDeclaration();
|
|
// React virtualization libraries (react-window, tanstack-virtual,
|
|
// react-virtuoso) all compute container dimensions via getComputedStyle.
|
|
// The defaults table previously returned `auto` for width/height and
|
|
// `'static'` for position, which made every list render 0 items. Pulling
|
|
// width/height from the synthesized bounding rect makes those libraries
|
|
// actually render content.
|
|
const dimensionFor = (name) => {
|
|
try {
|
|
const r = el.getBoundingClientRect && el.getBoundingClientRect();
|
|
if (!r) return null;
|
|
switch (name) {
|
|
case 'width': case 'inline-size':
|
|
return r.width != null ? `${r.width}px` : null;
|
|
case 'height': case 'block-size':
|
|
return r.height != null ? `${r.height}px` : null;
|
|
case 'left': return r.left != null ? `${r.left}px` : null;
|
|
case 'top': return r.top != null ? `${r.top}px` : null;
|
|
case 'right': return r.right != null ? `${r.right}px` : null;
|
|
case 'bottom': return r.bottom != null ? `${r.bottom}px` : null;
|
|
case 'client-width': case 'offset-width':
|
|
return r.width != null ? `${r.width}px` : null;
|
|
case 'client-height': case 'offset-height':
|
|
return r.height != null ? `${r.height}px` : null;
|
|
}
|
|
} catch (e) {}
|
|
return null;
|
|
};
|
|
|
|
const defaultsKebab = {
|
|
display: 'block', visibility: 'visible', opacity: '1',
|
|
position: 'static', overflow: 'visible',
|
|
transform: 'none', 'transform-origin': '0px 0px',
|
|
transition: 'none', animation: 'none',
|
|
float: 'none', clear: 'none',
|
|
margin: '0px', padding: '0px',
|
|
'margin-top': '0px', 'margin-right': '0px', 'margin-bottom': '0px', 'margin-left': '0px',
|
|
'padding-top': '0px', 'padding-right': '0px', 'padding-bottom': '0px', 'padding-left': '0px',
|
|
'font-size': '16px', 'line-height': 'normal', 'font-weight': '400',
|
|
'font-family': 'Times',
|
|
color: 'rgb(0, 0, 0)', 'background-color': 'rgba(0, 0, 0, 0)',
|
|
'border-width': '0px', 'border-style': 'none', 'border-color': 'rgb(0, 0, 0)',
|
|
'border-top-width': '0px', 'border-right-width': '0px',
|
|
'border-bottom-width': '0px', 'border-left-width': '0px',
|
|
'border-radius': '0px',
|
|
'z-index': 'auto', 'pointer-events': 'auto',
|
|
'box-sizing': 'content-box', cursor: 'auto',
|
|
'white-space': 'normal', 'text-align': 'start',
|
|
'flex-direction': 'row', 'flex-wrap': 'nowrap', 'align-items': 'normal',
|
|
'justify-content': 'normal', gap: 'normal',
|
|
'grid-template-columns': 'none', 'grid-template-rows': 'none',
|
|
'will-change': 'auto', 'backface-visibility': 'visible',
|
|
};
|
|
|
|
const lookup = (rawProp) => {
|
|
if (typeof rawProp !== 'string') return '';
|
|
// Inline value first.
|
|
const inlineVal = target.getPropertyValue ? target.getPropertyValue(rawProp) : '';
|
|
if (inlineVal) return inlineVal;
|
|
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
const dim = dimensionFor(kebab);
|
|
if (dim != null) return dim;
|
|
if (defaultsKebab[rawProp]) return defaultsKebab[rawProp];
|
|
if (defaultsKebab[kebab]) return defaultsKebab[kebab];
|
|
return '';
|
|
};
|
|
|
|
const target = style;
|
|
return new Proxy(style, {
|
|
get(_, prop) {
|
|
if (prop === Symbol.toPrimitive || prop === Symbol.toStringTag) return undefined;
|
|
if (prop in target) return target[prop];
|
|
if (prop === 'getPropertyValue') return (name) => lookup(name);
|
|
if (prop === 'getPropertyPriority') return () => '';
|
|
if (prop === 'item') return (i) => '';
|
|
if (prop === 'length') return 0;
|
|
if (prop === 'cssText') return '';
|
|
if (prop === 'parentRule') return null;
|
|
if (typeof prop === 'string') return lookup(prop);
|
|
return undefined;
|
|
},
|
|
});
|
|
};
|
|
// Returns the one Selection instance for a document (cached on the document),
|
|
// so window.getSelection() === document.getSelection(). The real Selection
|
|
// class is defined below, after Range. _selectionFor is hoisted.
|
|
function _selectionFor(doc) {
|
|
if (!doc) return null;
|
|
if (!doc._selection) doc._selection = new Selection(doc);
|
|
return doc._selection;
|
|
}
|
|
globalThis.getSelection = _markNative(function getSelection() {
|
|
return _selectionFor(globalThis.document);
|
|
});
|
|
|
|
globalThis.CSSStyleSheet = class CSSStyleSheet {
|
|
constructor(options) {
|
|
this.cssRules = [];
|
|
this.ownerRule = null;
|
|
this.disabled = false;
|
|
this._rules = [];
|
|
}
|
|
insertRule(rule, index) {
|
|
const idx = index ?? this._rules.length;
|
|
this._rules.splice(idx, 0, { cssText: rule, type: 1 });
|
|
this.cssRules = this._rules;
|
|
return idx;
|
|
}
|
|
deleteRule(index) {
|
|
this._rules.splice(index, 1);
|
|
this.cssRules = this._rules;
|
|
}
|
|
addRule(selector, style, index) {
|
|
return this.insertRule(selector + '{' + style + '}', index);
|
|
}
|
|
removeRule(index) { this.deleteRule(index); }
|
|
replace(text) {
|
|
this._rules = [{ cssText: text, type: 1 }];
|
|
this.cssRules = this._rules;
|
|
return Promise.resolve(this);
|
|
}
|
|
replaceSync(text) {
|
|
this._rules = [{ cssText: text, type: 1 }];
|
|
this.cssRules = this._rules;
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(Document.prototype, 'adoptedStyleSheets', {
|
|
get() { return this._adoptedStyleSheets || []; },
|
|
set(sheets) { this._adoptedStyleSheets = sheets; },
|
|
});
|
|
|
|
globalThis.__mutationObservers = [];
|
|
globalThis.MutationObserver = class MutationObserver {
|
|
constructor(callback) {
|
|
this._callback = callback;
|
|
this._targets = [];
|
|
this._records = [];
|
|
}
|
|
observe(target, options) {
|
|
this._targets.push({ target, options: options || {} });
|
|
globalThis.__mutationObservers.push(this);
|
|
}
|
|
disconnect() {
|
|
this._targets = [];
|
|
const idx = globalThis.__mutationObservers.indexOf(this);
|
|
if (idx >= 0) globalThis.__mutationObservers.splice(idx, 1);
|
|
}
|
|
takeRecords() {
|
|
const r = this._records.slice();
|
|
this._records = [];
|
|
return r;
|
|
}
|
|
_notify(records) {
|
|
this._records.push(...records);
|
|
Promise.resolve().then(() => {
|
|
if (this._records.length > 0) {
|
|
const batch = this._records.splice(0);
|
|
try { this._callback(batch, this); } catch(e) { /* observer errors shouldn't propagate */ }
|
|
}
|
|
});
|
|
}
|
|
};
|
|
globalThis.__notifyMutation = function(type, target_nid, addedNodes, removedNodes, attributeName, oldValue) {
|
|
if (!globalThis.__mutationObservers.length) return;
|
|
// Use `_wrap` (the canonical node-id → wrapper resolver) instead of a
|
|
// direct cache poke. The previous code referenced `globalThis._cache`,
|
|
// but `_cache` is a module-local Map — the lookup always returned
|
|
// undefined, so the function silently bailed every time. Result: no
|
|
// MutationObserver fired in obscura, ever, despite the call sites being
|
|
// wired up at appendChild / setAttribute. _wrap also lazily creates a
|
|
// wrapper for nodes that didn't have one yet (e.g. children parsed from
|
|
// `set innerHTML`), which we need for record.target/added/removed.
|
|
const target = _wrap(target_nid);
|
|
if (!target) return;
|
|
const record = {
|
|
type: type, // 'childList', 'attributes', 'characterData'
|
|
target: target,
|
|
addedNodes: (addedNodes || []).map(nid => _wrap(nid)).filter(Boolean),
|
|
removedNodes: (removedNodes || []).map(nid => _wrap(nid)).filter(Boolean),
|
|
attributeName: attributeName || null,
|
|
oldValue: oldValue ?? null,
|
|
previousSibling: null,
|
|
nextSibling: null,
|
|
};
|
|
// Walk target → ancestors so a subtree-mode observer rooted at any
|
|
// ancestor matches. The previous implementation just checked that
|
|
// `target.contains` and `target.closest` were defined (always true on
|
|
// any Element), so subtree=true silently behaved like subtree=false and
|
|
// every nested mutation missed its subscriber.
|
|
for (const obs of globalThis.__mutationObservers) {
|
|
let matched = false;
|
|
for (const t of obs._targets) {
|
|
const root = t.target;
|
|
if (!root) continue;
|
|
// Filter by type per the observer options. Default behaviour matches
|
|
// real MutationObserver: attribute mutations need options.attributes,
|
|
// characterData mutations need options.characterData, childList
|
|
// needs options.childList.
|
|
const wantsType =
|
|
(type === 'attributes' && t.options.attributes) ||
|
|
(type === 'characterData' && t.options.characterData) ||
|
|
(type === 'childList' && t.options.childList);
|
|
if (!wantsType) continue;
|
|
if (root._nid === target_nid) { matched = true; break; }
|
|
if (t.options.subtree) {
|
|
// Walk parents until we hit the observed root or run off the tree.
|
|
let cur = target.parentNode;
|
|
while (cur) {
|
|
if (cur._nid === root._nid) { matched = true; break; }
|
|
cur = cur.parentNode;
|
|
}
|
|
if (matched) break;
|
|
}
|
|
}
|
|
if (matched) obs._notify([record]);
|
|
}
|
|
};
|
|
|
|
globalThis.ShadowRoot = class ShadowRoot extends DocumentFragment {};
|
|
// Constructible-stylesheet adoption, mirroring Document.adoptedStyleSheets.
|
|
Object.defineProperty(globalThis.ShadowRoot.prototype, 'adoptedStyleSheets', {
|
|
get() { return this._adoptedStyleSheets || []; },
|
|
set(sheets) { this._adoptedStyleSheets = sheets; },
|
|
configurable: true,
|
|
});
|
|
globalThis.__obscura_shadowHostNames = new Set(['article','aside','blockquote','body','div','footer','h1','h2','h3','h4','h5','h6','header','main','nav','p','section','span']);
|
|
function _isConstructorCE(v) {
|
|
if (typeof v !== 'function') return false;
|
|
try { Reflect.construct(function () {}, [], v); return true; } catch (e) { return false; }
|
|
}
|
|
const _CE_RESERVED = new Set(['annotation-xml', 'color-profile', 'font-face', 'font-face-src', 'font-face-uri', 'font-face-format', 'font-face-name', 'missing-glyph']);
|
|
function _isValidCustomElementName(name) {
|
|
if (typeof name !== 'string' || _CE_RESERVED.has(name)) return false;
|
|
// PotentialCustomElementName (approx): lowercase start, a hyphen, no uppercase.
|
|
return /^[a-z][a-z0-9._·À--]*-[a-z0-9._·À--]*$/.test(name);
|
|
}
|
|
class CustomElementRegistry {
|
|
constructor() { this._registry = new Map(); this._byCtor = new Map(); this._whenDefinedResolvers = new Map(); this._defining = false; }
|
|
define(name, cls, opts) {
|
|
if (!_isConstructorCE(cls)) throw new TypeError("Failed to execute 'define' on 'CustomElementRegistry': parameter 2 is not a constructor.");
|
|
if (!_isValidCustomElementName(name)) throw new DOMException("Failed to execute 'define' on 'CustomElementRegistry': \"" + name + "\" is not a valid custom element name", "SyntaxError");
|
|
if (this._defining) throw new DOMException("Failed to execute 'define' on 'CustomElementRegistry': operation is not supported while a definition is in progress", "NotSupportedError");
|
|
if (this._registry.has(name)) throw new DOMException("Failed to execute 'define' on 'CustomElementRegistry': the name \"" + name + "\" has already been used with this registry", "NotSupportedError");
|
|
if (this._byCtor.has(cls)) throw new DOMException("Failed to execute 'define' on 'CustomElementRegistry': the constructor has already been used with this registry", "NotSupportedError");
|
|
this._defining = true;
|
|
try { this._byCtor.set(cls, name); this._defineInner(name, cls, opts); } finally { this._defining = false; }
|
|
}
|
|
_defineInner(name, cls, opts) {
|
|
this._registry.set(name, cls);
|
|
// Upgrade existing matching elements: instantiate the class on each,
|
|
// fire connectedCallback if the element is in the document. Without
|
|
// this, lit / MusicKit / Polymer components never wire up their
|
|
// shadow DOM or render, leaving heavy chunks of YouTube,
|
|
// music.apple.com, and any web-component site as empty shells.
|
|
try {
|
|
const matches = globalThis.document?.querySelectorAll(name) || [];
|
|
for (const el of matches) this._upgradeElement(el, cls);
|
|
} catch (e) {}
|
|
const resolvers = this._whenDefinedResolvers.get(name);
|
|
if (resolvers) {
|
|
for (const r of resolvers) r(cls);
|
|
this._whenDefinedResolvers.delete(name);
|
|
}
|
|
}
|
|
_upgradeElement(el, cls) {
|
|
if (el.__customUpgraded) return;
|
|
el.__customUpgraded = true;
|
|
try {
|
|
// Web Components spec: copy own props from the prototype onto the
|
|
// element. JS-side classes define behavior via methods on the
|
|
// prototype; we don't truly swap prototypes (Element is shared),
|
|
// so attach the prototype methods directly to the instance.
|
|
const proto = cls.prototype;
|
|
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
if (key === 'constructor') continue;
|
|
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
if (desc) Object.defineProperty(el, key, desc);
|
|
}
|
|
// Run constructor-side init on the element. Real custom elements
|
|
// run the class constructor, but Element instances aren't a `cls`
|
|
// subclass here; calling `.call(el)` runs whatever init logic the
|
|
// class defines without needing a new allocation.
|
|
try { cls.call(el); } catch (e) {}
|
|
if (typeof el.connectedCallback === 'function' && globalThis.document?.contains?.(el)) {
|
|
try { el.connectedCallback(); } catch (e) {}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
get(name) { return this._registry.get(name); }
|
|
getName(cls) {
|
|
if (!_isConstructorCE(cls)) throw new TypeError("Failed to execute 'getName' on 'CustomElementRegistry': parameter 1 is not a constructor.");
|
|
return this._byCtor.has(cls) ? this._byCtor.get(cls) : null;
|
|
}
|
|
whenDefined(name) {
|
|
if (!_isValidCustomElementName(name)) return Promise.reject(new DOMException("Failed to execute 'whenDefined' on 'CustomElementRegistry': \"" + name + "\" is not a valid custom element name", "SyntaxError"));
|
|
const cls = this._registry.get(name);
|
|
if (cls) return Promise.resolve(cls);
|
|
return new Promise((resolve) => {
|
|
const list = this._whenDefinedResolvers.get(name) || [];
|
|
list.push(resolve);
|
|
this._whenDefinedResolvers.set(name, list);
|
|
});
|
|
}
|
|
upgrade(root) {
|
|
if (!root || !root.querySelectorAll) return;
|
|
for (const [name, cls] of this._registry.entries()) {
|
|
const matches = root.querySelectorAll(name);
|
|
for (const el of matches) this._upgradeElement(el, cls);
|
|
}
|
|
}
|
|
}
|
|
globalThis.CustomElementRegistry = CustomElementRegistry;
|
|
globalThis.customElements = new CustomElementRegistry();
|
|
globalThis.HTMLUnknownElement = Element;
|
|
// ElementInternals: form-associated custom element internals. Validity/state
|
|
// are JS-observable; ARIA reflection that needs the accessibility tree is not.
|
|
globalThis.ElementInternals = class ElementInternals {
|
|
constructor(el) { this._el = el; this._valid = true; this._flags = {}; this._message = ''; this._value = null; this._states = new Set(); }
|
|
setFormValue(value, state) { this._value = value; }
|
|
setValidity(flags, message, anchor) {
|
|
flags = flags || {};
|
|
const bad = Object.keys(flags).some((k) => k !== 'valid' && flags[k]);
|
|
if (bad && (message == null || message === '')) throw new TypeError("Failed to execute 'setValidity' on 'ElementInternals': The second argument should not be empty if one or more flags in the first argument are true.");
|
|
this._flags = flags; this._valid = !bad; this._message = bad ? String(message) : '';
|
|
}
|
|
checkValidity() { return this._valid; }
|
|
reportValidity() { return this._valid; }
|
|
get validity() {
|
|
const f = this._flags || {};
|
|
return { valid: this._valid, valueMissing: !!f.valueMissing, typeMismatch: !!f.typeMismatch, patternMismatch: !!f.patternMismatch, tooLong: !!f.tooLong, tooShort: !!f.tooShort, rangeUnderflow: !!f.rangeUnderflow, rangeOverflow: !!f.rangeOverflow, stepMismatch: !!f.stepMismatch, badInput: !!f.badInput, customError: !!f.customError };
|
|
}
|
|
get validationMessage() { return this._message || ''; }
|
|
get willValidate() { return true; }
|
|
get form() { return this._el && this._el.closest ? this._el.closest('form') : null; }
|
|
get labels() { return _nodeList([]); }
|
|
get shadowRoot() { return (this._el && this._el._shadowRoot) || null; }
|
|
get states() { return this._states; }
|
|
};
|
|
// Full standard constant set (issue #439). The partial version here lacked
|
|
// FILTER_ACCEPT/REJECT/SKIP and most SHOW_* values, so the canonical
|
|
// `acceptNode() { return NodeFilter.FILTER_ACCEPT; }` filter idiom returned
|
|
// undefined and TreeWalker/NodeIterator rejected every node.
|
|
globalThis.NodeFilter = {
|
|
SHOW_ALL: 0xFFFFFFFF,
|
|
SHOW_ELEMENT: 0x1,
|
|
SHOW_ATTRIBUTE: 0x2,
|
|
SHOW_TEXT: 0x4,
|
|
SHOW_CDATA_SECTION: 0x8,
|
|
SHOW_ENTITY_REFERENCE: 0x10,
|
|
SHOW_ENTITY: 0x20,
|
|
SHOW_PROCESSING_INSTRUCTION: 0x40,
|
|
SHOW_COMMENT: 0x80,
|
|
SHOW_DOCUMENT: 0x100,
|
|
SHOW_DOCUMENT_TYPE: 0x200,
|
|
SHOW_DOCUMENT_FRAGMENT: 0x400,
|
|
SHOW_NOTATION: 0x800,
|
|
FILTER_ACCEPT: 1,
|
|
FILTER_REJECT: 2,
|
|
FILTER_SKIP: 3,
|
|
};
|
|
// ResizeObserver is defined earlier with real per-target firing; the stub
|
|
// that previously lived here was a no-op that clobbered the real class.
|
|
//
|
|
// IntersectionObserver: without a layout engine we can't compute real
|
|
// intersection geometry, so every observed target is treated as fully
|
|
// in-viewport (`isIntersecting: true`, `intersectionRatio: 1`). Real
|
|
// libraries lean on this in three patterns we must support:
|
|
//
|
|
// 1. Lazy load: observe(img) -> first intersection -> load src -> unobserve.
|
|
// One fire is enough — covered by the initial microtask fire.
|
|
// 2. Infinite scroll: observe(sentinel) -> on intersection load more ->
|
|
// new sentinel mounts -> fire again. Needs re-fires as DOM grows.
|
|
// 3. Reveal-on-scroll animations: observe(card) -> isIntersecting flips
|
|
// true once and an animation runs. One fire is enough.
|
|
//
|
|
// To cover (2) without spinning forever, we burst-fire at an exponential
|
|
// backoff schedule and ALSO re-fire whenever the DOM mutates (a strong
|
|
// signal that the page just rendered something new). Per-observer total
|
|
// fire cap stops us from looping on a never-disconnected observer.
|
|
globalThis.__intersectionObservers = [];
|
|
globalThis.IntersectionObserver = class IntersectionObserver {
|
|
constructor(callback, options) {
|
|
this._callback = callback;
|
|
this._options = options || {};
|
|
this._targets = new Set();
|
|
this._connected = true;
|
|
this._fireCount = 0;
|
|
globalThis.__intersectionObservers.push(this);
|
|
}
|
|
_fireFor(targets) {
|
|
if (!this._connected || !targets.length || this._fireCount >= 256) return;
|
|
this._fireCount++;
|
|
const records = targets.map(target => ({
|
|
target,
|
|
isIntersecting: true,
|
|
intersectionRatio: 1,
|
|
boundingClientRect: target.getBoundingClientRect
|
|
? target.getBoundingClientRect()
|
|
: { x: 0, y: 0, width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20 },
|
|
intersectionRect: target.getBoundingClientRect
|
|
? target.getBoundingClientRect()
|
|
: { x: 0, y: 0, width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20 },
|
|
rootBounds: { x: 0, y: 0, width: 1280, height: 720, top: 0, left: 0, right: 1280, bottom: 720 },
|
|
time: Date.now(),
|
|
}));
|
|
try { this._callback(records, this); } catch (e) { /* IO callbacks must not propagate */ }
|
|
}
|
|
observe(el) {
|
|
if (!el || !this._connected) return;
|
|
if (this._targets.has(el)) return;
|
|
this._targets.add(el);
|
|
Promise.resolve().then(() => this._fireFor([el]));
|
|
// Exponential burst to cover infinite-scroll sentinels that "re-arm"
|
|
// after content lands. Without a real scroll/layout signal, we fake the
|
|
// re-fire schedule. Beyond ~10s the page has usually settled.
|
|
[120, 500, 1500, 3500, 7000].forEach(delay => {
|
|
setTimeout(() => {
|
|
if (this._connected && this._targets.has(el)) this._fireFor([el]);
|
|
}, delay);
|
|
});
|
|
}
|
|
unobserve(el) { this._targets.delete(el); }
|
|
disconnect() {
|
|
this._connected = false;
|
|
this._targets.clear();
|
|
const idx = globalThis.__intersectionObservers.indexOf(this);
|
|
if (idx >= 0) globalThis.__intersectionObservers.splice(idx, 1);
|
|
}
|
|
takeRecords() { return []; }
|
|
get root() { return this._options.root || null; }
|
|
get rootMargin() { return this._options.rootMargin || "0px 0px 0px 0px"; }
|
|
get thresholds() {
|
|
const t = this._options.threshold;
|
|
if (t == null) return [0];
|
|
return Array.isArray(t) ? t.slice() : [t];
|
|
}
|
|
};
|
|
// When the DOM mutates (e.g. infinite scroll loads a batch of items), re-fire
|
|
// every active IntersectionObserver so libraries observing dynamic content
|
|
// see a fresh isIntersecting=true event. Uses the same per-observer fire cap
|
|
// to prevent runaway loops if the page is mutating in a tight cycle.
|
|
(function() {
|
|
const reFire = () => {
|
|
for (const obs of globalThis.__intersectionObservers) {
|
|
if (!obs._connected) continue;
|
|
const ts = [...obs._targets];
|
|
if (ts.length) obs._fireFor(ts);
|
|
}
|
|
};
|
|
// Lazy-attach a single MutationObserver on document.body once the page is
|
|
// ready, debounced via a microtask so a flurry of mutations only triggers
|
|
// one IO sweep.
|
|
let pending = false;
|
|
const wireUp = () => {
|
|
if (!globalThis.document?.body) return;
|
|
const mo = new MutationObserver(() => {
|
|
if (pending) return;
|
|
pending = true;
|
|
Promise.resolve().then(() => { pending = false; reFire(); });
|
|
});
|
|
try { mo.observe(globalThis.document.body, {childList: true, subtree: true}); } catch {}
|
|
};
|
|
if (globalThis.document?.body) wireUp();
|
|
else Promise.resolve().then(wireUp);
|
|
})();
|
|
globalThis.IntersectionObserverEntry = class IntersectionObserverEntry {};
|
|
globalThis.PerformanceObserver = class { constructor(){} observe(){} disconnect(){} };
|
|
|
|
globalThis.DOMException = (function () {
|
|
const NAME_TO_CODE = {
|
|
IndexSizeError: 1, HierarchyRequestError: 3, WrongDocumentError: 4,
|
|
InvalidCharacterError: 5, NoModificationAllowedError: 7, NotFoundError: 8,
|
|
NotSupportedError: 9, InUseAttributeError: 10, InvalidStateError: 11,
|
|
SyntaxError: 12, InvalidModificationError: 13, NamespaceError: 14,
|
|
InvalidAccessError: 15, TypeMismatchError: 17, SecurityError: 18,
|
|
NetworkError: 19, AbortError: 20, URLMismatchError: 21,
|
|
QuotaExceededError: 22, TimeoutError: 23, InvalidNodeTypeError: 24,
|
|
DataCloneError: 25,
|
|
};
|
|
class DOMException extends Error {
|
|
constructor(message = "", name = "Error") {
|
|
super(message);
|
|
this.name = name;
|
|
this.message = String(message);
|
|
}
|
|
get code() { return NAME_TO_CODE[this.name] || 0; }
|
|
}
|
|
const CONSTS = {
|
|
INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3,
|
|
WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6,
|
|
NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9,
|
|
INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12,
|
|
INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15,
|
|
VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18,
|
|
NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21,
|
|
QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24,
|
|
DATA_CLONE_ERR: 25,
|
|
};
|
|
for (const k in CONSTS) {
|
|
Object.defineProperty(DOMException, k, { value: CONSTS[k], enumerable: true });
|
|
Object.defineProperty(DOMException.prototype, k, { value: CONSTS[k], enumerable: true });
|
|
}
|
|
return DOMException;
|
|
})();
|
|
// Per the UI Events spec, only events the user agent dispatches (real or
|
|
// automation-synthesized input) are trusted; events page script builds with
|
|
// `new Event(...)` must report isTrusted === false (issue #303). Returning true
|
|
// for everything is a trivial bot-detection tell. Trusted events are tracked in
|
|
// a closure-private WeakSet so page JS can neither read nor forge the flag.
|
|
// obscura's CDP input pipeline marks its synthetic events via the
|
|
// non-enumerable __obscura_markTrusted helper.
|
|
const _trustedEvents = new WeakSet();
|
|
globalThis.__obscura_markTrusted = function(ev) { try { if (ev) _trustedEvents.add(ev); } catch (_e) {} return ev; };
|
|
|
|
// Write value/checked through the element's *prototype* accessor, skipping any
|
|
// per-instance property a framework layered on top. React (and Preact/Vue)
|
|
// install a value tracker by redefining `value`/`checked` on the element to
|
|
// record the last value they wrote; a plain `el.value = x` runs that wrapper,
|
|
// so their tracker updates in lockstep and the next input/change event looks
|
|
// unchanged, so onChange never fires (issue #324). Writing through the
|
|
// prototype setter leaves the tracker stale, so the edit is seen as a real
|
|
// user change. When no framework wrapper is present this is identical to a
|
|
// direct assignment.
|
|
globalThis.__obscura_setFieldValue = function(el, field, value) {
|
|
try {
|
|
let proto = Object.getPrototypeOf(el);
|
|
let desc;
|
|
while (proto && !((desc = Object.getOwnPropertyDescriptor(proto, field)) && desc.set)) {
|
|
proto = Object.getPrototypeOf(proto);
|
|
}
|
|
if (desc && desc.set) { desc.set.call(el, value); return; }
|
|
} catch (_e) {}
|
|
el[field] = value;
|
|
};
|
|
|
|
// Build a FileList-like object: an array with the DOM's `item(i)` accessor.
|
|
function _makeFileList(files) {
|
|
const list = files.slice();
|
|
Object.defineProperty(list, "item", { value: (i) => list[i] || null, enumerable: false });
|
|
return list;
|
|
}
|
|
function _emptyFileList() { return _makeFileList([]); }
|
|
|
|
// Populate an <input type=file>'s FileList from the CDP DOM.setFileInputFiles
|
|
// call (Puppeteer uploadFile / Playwright setInputFiles). `specs` is an array of
|
|
// { name, type, b64 } where b64 is the base64-encoded file bytes read on the
|
|
// Rust side. Real File objects (backed by the bytes) are created so page code can
|
|
// read them via FileReader or upload them via fetch/FormData, then input+change
|
|
// fire as a genuine selection would (issue #359).
|
|
globalThis.__obscura_setInputFiles = function(el, specs) {
|
|
const files = (specs || []).map((s) => {
|
|
let bytes;
|
|
try {
|
|
const bin = atob(s.b64 || "");
|
|
bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
} catch (_e) { bytes = new Uint8Array(0); }
|
|
return new File([bytes], s.name || "", { type: s.type || "" });
|
|
});
|
|
el._files = _makeFileList(files);
|
|
// Mark the events trusted (isTrusted === true), like the Input domain does
|
|
// for synthesized clicks/keys. A real <input type=file> selection fires
|
|
// trusted events; upload flows that gate their change handler on
|
|
// event.isTrusted (common in frameworks and anti-bot code) ignore untrusted
|
|
// ones, which would silently break the exact case this feature targets.
|
|
try { el.dispatchEvent(globalThis.__obscura_markTrusted(new Event("input", { bubbles: true }))); } catch (_e) {}
|
|
try { el.dispatchEvent(globalThis.__obscura_markTrusted(new Event("change", { bubbles: true }))); } catch (_e) {}
|
|
};
|
|
globalThis.Event = class Event {
|
|
constructor(t,o={}) { if (arguments.length < 1) throw new TypeError("Failed to construct 'Event': 1 argument required, but only 0 present."); this.type=String(t);this.bubbles=!!o.bubbles;this.cancelable=!!o.cancelable;this.composed=!!o.composed;this.defaultPrevented=false;this.target=null;this.currentTarget=null;this.eventPhase=0;this.timeStamp=Date.now();this._propagationStopped=false;this._immediatePropagationStopped=false; }
|
|
get isTrusted() { return _trustedEvents.has(this); }
|
|
preventDefault() { if (this.cancelable) this.defaultPrevented=true; } stopPropagation(){ this._propagationStopped=true; } stopImmediatePropagation(){ this._propagationStopped=true; this._immediatePropagationStopped=true; }
|
|
initEvent(type,bubbles,cancelable) { if (arguments.length < 1) throw new TypeError("Failed to execute 'initEvent' on 'Event': 1 argument required, but only 0 present."); this.type=String(type);this.bubbles=!!bubbles;this.cancelable=!!cancelable;this.defaultPrevented=false;this._propagationStopped=false;this._immediatePropagationStopped=false; }
|
|
composedPath() {
|
|
if (!this.target) return [];
|
|
const path = [];
|
|
let n = this.target;
|
|
while (n) { path.push(n); n = n.parentNode || null; }
|
|
if (typeof window !== "undefined" && window && path[path.length - 1] !== window) path.push(window);
|
|
return path;
|
|
}
|
|
};
|
|
_markNative(Event);
|
|
globalThis.CustomEvent = class extends Event {
|
|
constructor(t,o={}) { if (arguments.length < 1) throw new TypeError("Failed to construct 'CustomEvent': 1 argument required, but only 0 present."); super(t,o);this.detail=o.detail!==undefined?o.detail:null; }
|
|
// Legacy DOM Level 2 init; some libraries (Starbucks China bundle, older
|
|
// analytics shims) still call createEvent('CustomEvent') + initCustomEvent
|
|
// instead of new CustomEvent(...). See issue #41.
|
|
initCustomEvent(type,bubbles,cancelable,detail) {
|
|
this.type = type;
|
|
this.bubbles = !!bubbles;
|
|
this.cancelable = !!cancelable;
|
|
this.detail = detail;
|
|
}
|
|
};
|
|
globalThis.MouseEvent = class extends Event {
|
|
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.screenX=o.screenX||0;this.screenY=o.screenY||0;this.clientX=o.clientX||0;this.clientY=o.clientY||0;this.ctrlKey=!!o.ctrlKey;this.altKey=!!o.altKey;this.shiftKey=!!o.shiftKey;this.metaKey=!!o.metaKey;this.button=o.button||0;this.buttons=o.buttons||0;this.relatedTarget=o.relatedTarget||null; }
|
|
// Legacy DOM Level 2 initializer. Positional signature per UI Events spec.
|
|
initMouseEvent(type,canBubble,cancelable,view,detail,screenX,screenY,clientX,clientY,ctrlKey,altKey,shiftKey,metaKey,button,relatedTarget) {
|
|
if (arguments.length < 1) throw new TypeError("Failed to execute 'initMouseEvent' on 'MouseEvent': 1 argument required, but only 0 present.");
|
|
this.initEvent(type,canBubble,cancelable);
|
|
this.view=view===undefined?null:view;
|
|
this.detail=detail||0;
|
|
this.screenX=screenX||0;
|
|
this.screenY=screenY||0;
|
|
this.clientX=clientX||0;
|
|
this.clientY=clientY||0;
|
|
this.ctrlKey=!!ctrlKey;
|
|
this.altKey=!!altKey;
|
|
this.shiftKey=!!shiftKey;
|
|
this.metaKey=!!metaKey;
|
|
this.button=button||0;
|
|
this.relatedTarget=relatedTarget===undefined?null:relatedTarget;
|
|
}
|
|
};
|
|
globalThis.KeyboardEvent = class extends Event {
|
|
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.key=o.key||"";this.code=o.code||"";this.location=o.location||0;this.ctrlKey=!!o.ctrlKey;this.altKey=!!o.altKey;this.shiftKey=!!o.shiftKey;this.metaKey=!!o.metaKey;this.repeat=!!o.repeat; }
|
|
// Legacy DOM Level 3 initializer. Positional signature per the WebKit/Gecko form.
|
|
initKeyboardEvent(type,canBubble,cancelable,view,key,location,ctrlKey,altKey,shiftKey,metaKey) {
|
|
if (arguments.length < 1) throw new TypeError("Failed to execute 'initKeyboardEvent' on 'KeyboardEvent': 1 argument required, but only 0 present.");
|
|
this.initEvent(type,canBubble,cancelable);
|
|
this.view=view===undefined?null:view;
|
|
this.key=key===undefined?"":String(key);
|
|
this.location=location||0;
|
|
this.ctrlKey=!!ctrlKey;
|
|
this.altKey=!!altKey;
|
|
this.shiftKey=!!shiftKey;
|
|
this.metaKey=!!metaKey;
|
|
}
|
|
};
|
|
globalThis.FocusEvent = class extends Event { constructor(t,o={}) { super(t,o);this.relatedTarget=o.relatedTarget||null; } };
|
|
globalThis.InputEvent = class extends Event { constructor(t,o={}) { super(t,o);this.data=o.data||null;this.inputType=o.inputType||""; } };
|
|
globalThis.ErrorEvent = class extends Event { constructor(t,o={}) { super(t,o);this.message=o.message||"";this.error=o.error||null; } };
|
|
globalThis.PointerEvent = class extends Event { constructor(t,o={}) { super(t,o); } };
|
|
globalThis.AnimationEvent = class extends Event {};
|
|
globalThis.TransitionEvent = class extends Event {};
|
|
globalThis.UIEvent = class extends Event {
|
|
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0; }
|
|
// Legacy DOM Level 2 initializer. Positional signature per UI Events spec.
|
|
initUIEvent(type,canBubble,cancelable,view,detail) {
|
|
if (arguments.length < 1) throw new TypeError("Failed to execute 'initUIEvent' on 'UIEvent': 1 argument required, but only 0 present.");
|
|
this.initEvent(type,canBubble,cancelable);
|
|
this.view=view===undefined?null:view;
|
|
this.detail=detail||0;
|
|
}
|
|
};
|
|
globalThis.WheelEvent = class extends Event { constructor(t,o={}) { super(t,o);this.deltaX=o.deltaX||0;this.deltaY=o.deltaY||0;this.deltaZ=o.deltaZ||0;this.deltaMode=o.deltaMode||0; } };
|
|
|
|
globalThis.CompositionEvent = class extends Event {
|
|
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.data=o.data||""; }
|
|
// Legacy DOM Level 3 initializer. Positional signature per UI Events spec.
|
|
initCompositionEvent(type,canBubble,cancelable,view,data) {
|
|
if (arguments.length < 1) throw new TypeError("Failed to execute 'initCompositionEvent' on 'CompositionEvent': 1 argument required, but only 0 present.");
|
|
this.initEvent(type,canBubble,cancelable);
|
|
this.view=view===undefined?null:view;
|
|
this.data=data===undefined?"":String(data);
|
|
}
|
|
};
|
|
globalThis.PopStateEvent = class extends Event {
|
|
constructor(type, init) {
|
|
super(type, init || {});
|
|
// Real PopStateEvent exposes `state` from the entry being navigated to.
|
|
// The earlier stub inherited Event but never stored state, so
|
|
// `popstate.state` was always undefined and SPA routers reading
|
|
// `event.state` to restore route info would mis-render.
|
|
this.state = init && 'state' in init ? init.state : null;
|
|
}
|
|
};
|
|
globalThis.HashChangeEvent = class extends Event {};
|
|
globalThis.MessageEvent = class extends Event { constructor(t,o={}) { super(t,o);this.data=o.data; } };
|
|
globalThis.ProgressEvent = class ProgressEvent extends Event {
|
|
constructor(type, init) {
|
|
super(type, init || {});
|
|
const i = init || {};
|
|
this.lengthComputable = !!i.lengthComputable;
|
|
this.loaded = i.loaded != null ? Number(i.loaded) : 0;
|
|
this.total = i.total != null ? Number(i.total) : 0;
|
|
}
|
|
};
|
|
globalThis.ClipboardEvent = class extends Event {};
|
|
globalThis.SubmitEvent = class extends Event {};
|
|
|
|
// ToggleEvent backs the popover beforetoggle/toggle events. oldState and
|
|
// newState are "open"/"closed". These events do not bubble; beforetoggle is
|
|
// cancelable only for the closed -> open (show) transition, toggle is never
|
|
// cancelable. See HTML "popover" and html/semantics/popovers WPT.
|
|
globalThis.ToggleEvent = class ToggleEvent extends Event {
|
|
constructor(type, init = {}) {
|
|
super(type, init);
|
|
this.oldState = init.oldState !== undefined ? String(init.oldState) : "";
|
|
this.newState = init.newState !== undefined ? String(init.newState) : "";
|
|
}
|
|
};
|
|
_markNative(globalThis.ToggleEvent);
|
|
|
|
globalThis.PromiseRejectionEvent = class PromiseRejectionEvent extends Event {
|
|
constructor(type, init) {
|
|
if (arguments.length < 2 || init == null || !('promise' in Object(init))) {
|
|
throw new TypeError(
|
|
"Failed to construct 'PromiseRejectionEvent': required member promise is undefined."
|
|
);
|
|
}
|
|
super(type, init);
|
|
this.promise = init.promise;
|
|
this.reason = init.reason;
|
|
}
|
|
};
|
|
_markNative(globalThis.PromiseRejectionEvent);
|
|
|
|
globalThis.StorageEvent = class StorageEvent extends Event {
|
|
constructor(type, init = {}) {
|
|
super(type, init);
|
|
this.key = init.key !== undefined ? init.key : null;
|
|
this.oldValue = init.oldValue !== undefined ? init.oldValue : null;
|
|
this.newValue = init.newValue !== undefined ? init.newValue : null;
|
|
this.url = init.url || "";
|
|
this.storageArea = init.storageArea || null;
|
|
}
|
|
initStorageEvent(type, bubbles, cancelable, key, oldValue, newValue, url, storageArea) {
|
|
this.initEvent(type, bubbles, cancelable);
|
|
this.key = key !== undefined ? key : null;
|
|
this.oldValue = oldValue !== undefined ? oldValue : null;
|
|
this.newValue = newValue !== undefined ? newValue : null;
|
|
this.url = url || "";
|
|
this.storageArea = storageArea || null;
|
|
}
|
|
};
|
|
_markNative(globalThis.StorageEvent);
|
|
|
|
// AbortController / AbortSignal. AbortSignal is a real constructor with a
|
|
// prototype, so feature-detection and `AbortSignal.prototype` access work. It
|
|
// carries aborted/reason, supports throwIfAborted(), and fires "abort" to
|
|
// onabort and addEventListener listeners when the controller aborts.
|
|
(function () {
|
|
const BRAND = Symbol("AbortSignal");
|
|
function emit(signal, evt) {
|
|
if (typeof signal.onabort === "function") {
|
|
try { signal.onabort.call(signal, evt); } catch (_) {}
|
|
}
|
|
for (const cb of signal._listeners.slice()) {
|
|
const fn = typeof cb === "function" ? cb : cb && cb.handleEvent;
|
|
if (typeof fn === "function") { try { fn.call(signal, evt); } catch (_) {} }
|
|
}
|
|
}
|
|
function fire(signal, reason) {
|
|
if (signal._aborted) return;
|
|
signal._aborted = true;
|
|
signal._reason = reason !== undefined
|
|
? reason
|
|
: new DOMException("signal is aborted without reason", "AbortError");
|
|
const evt = typeof Event === "function" ? new Event("abort") : { type: "abort" };
|
|
try { evt.target = signal; evt.currentTarget = signal; } catch (_) {}
|
|
emit(signal, evt);
|
|
}
|
|
globalThis.AbortSignal = class AbortSignal {
|
|
constructor(brand) {
|
|
if (brand !== BRAND) {
|
|
throw new TypeError("Failed to construct 'AbortSignal': Illegal constructor");
|
|
}
|
|
this._aborted = false;
|
|
this._reason = undefined;
|
|
this._listeners = [];
|
|
this.onabort = null;
|
|
}
|
|
get aborted() { return this._aborted; }
|
|
get reason() { return this._reason; }
|
|
throwIfAborted() { if (this._aborted) throw this._reason; }
|
|
addEventListener(type, cb) {
|
|
if (type === "abort" && cb != null) this._listeners.push(cb);
|
|
}
|
|
removeEventListener(type, cb) {
|
|
if (type !== "abort") return;
|
|
const i = this._listeners.indexOf(cb);
|
|
if (i >= 0) this._listeners.splice(i, 1);
|
|
}
|
|
dispatchEvent(evt) {
|
|
if (evt && evt.type === "abort") emit(this, evt);
|
|
return true;
|
|
}
|
|
static abort(reason) {
|
|
const s = new AbortSignal(BRAND);
|
|
s._aborted = true;
|
|
s._reason = reason !== undefined
|
|
? reason
|
|
: new DOMException("signal is aborted without reason", "AbortError");
|
|
return s;
|
|
}
|
|
static timeout(ms) {
|
|
const s = new AbortSignal(BRAND);
|
|
setTimeout(() => fire(s, new DOMException("signal timed out", "TimeoutError")), ms);
|
|
return s;
|
|
}
|
|
static any(signals) {
|
|
const s = new AbortSignal(BRAND);
|
|
const list = Array.from(signals || []);
|
|
for (const sig of list) {
|
|
if (sig && sig.aborted) { s._aborted = true; s._reason = sig.reason; return s; }
|
|
}
|
|
for (const sig of list) {
|
|
if (sig && typeof sig.addEventListener === "function") {
|
|
sig.addEventListener("abort", () => fire(s, sig.reason));
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
};
|
|
globalThis.AbortController = class AbortController {
|
|
constructor() { this.signal = new globalThis.AbortSignal(BRAND); }
|
|
abort(reason) { fire(this.signal, reason); }
|
|
};
|
|
_markNative(globalThis.AbortSignal);
|
|
_markNative(globalThis.AbortController);
|
|
})();
|
|
// Normalize one Blob part to bytes. `native` newline normalization applies to
|
|
// string parts when the Blob/File `endings` option is "native".
|
|
function _blobPartToBytes(p, native) {
|
|
if (p == null) return new Uint8Array(0);
|
|
if (typeof Blob === "function" && p instanceof Blob) return p._bytes || new Uint8Array(0);
|
|
if (p instanceof ArrayBuffer) return new Uint8Array(p.slice(0));
|
|
if (ArrayBuffer.isView(p)) return new Uint8Array(p.buffer.slice(p.byteOffset, p.byteOffset + p.byteLength));
|
|
let s = String(p);
|
|
if (native) s = s.replace(/\r\n|\r|\n/g, "\n");
|
|
return new TextEncoder().encode(s);
|
|
}
|
|
function _bytesToBinaryString(bytes) { let s = ""; for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); return s; }
|
|
if (typeof Blob === "undefined") globalThis.Blob = class Blob {
|
|
constructor(parts, opts) {
|
|
opts = opts || {};
|
|
const endings = opts.endings != null ? String(opts.endings) : "transparent";
|
|
if (endings !== "transparent" && endings !== "native") throw new TypeError("Failed to construct 'Blob': The provided value '" + endings + "' is not a valid enum value of type EndingType.");
|
|
const native = endings === "native";
|
|
const chunks = []; let total = 0;
|
|
if (parts != null) {
|
|
if (typeof parts === "string" || typeof parts[Symbol.iterator] !== "function") throw new TypeError("Failed to construct 'Blob': The provided value cannot be converted to a sequence.");
|
|
for (const p of parts) { const b = _blobPartToBytes(p, native); chunks.push(b); total += b.length; }
|
|
}
|
|
const data = new Uint8Array(total); let off = 0;
|
|
for (const c of chunks) { data.set(c, off); off += c.length; }
|
|
this._bytes = data;
|
|
this.size = total;
|
|
const t = opts.type != null ? String(opts.type) : "";
|
|
this.type = /^[\x20-\x7e]*$/.test(t) ? t.toLowerCase() : "";
|
|
}
|
|
get [Symbol.toStringTag]() { return "Blob"; }
|
|
slice(start, end, contentType) {
|
|
const len = this.size;
|
|
const s = start === undefined ? 0 : (start < 0 ? Math.max(len + start, 0) : Math.min(start, len));
|
|
let e = end === undefined ? len : (end < 0 ? Math.max(len + end, 0) : Math.min(end, len));
|
|
if (e < s) e = s;
|
|
const out = new Blob([], contentType != null ? { type: contentType } : {});
|
|
out._bytes = this._bytes.slice(s, e);
|
|
out.size = out._bytes.length;
|
|
return out;
|
|
}
|
|
text() { return Promise.resolve(new TextDecoder().decode(this._bytes)); }
|
|
arrayBuffer() { return Promise.resolve(_arrayBufferFromBytes(this._bytes)); }
|
|
bytes() { return Promise.resolve(this._bytes.slice()); }
|
|
};
|
|
if (typeof File === "undefined") globalThis.File = class File extends Blob {
|
|
constructor(parts, name, opts) {
|
|
if (arguments.length < 2) throw new TypeError("Failed to construct 'File': 2 arguments required, but only " + arguments.length + " present.");
|
|
opts = opts || {};
|
|
super(parts, opts);
|
|
this.name = String(name);
|
|
this.lastModified = opts.lastModified != null ? Number(opts.lastModified) : Date.now();
|
|
}
|
|
get [Symbol.toStringTag]() { return "File"; }
|
|
};
|
|
if (typeof FormData === "undefined") globalThis.FormData = class FormData { constructor(){this._d=[];} append(k,v){this._d.push([k,v]);} get(k){const e=this._d.find(([a])=>a===k);return e?e[1]:null;} getAll(k){return this._d.filter(([a])=>a===k).map(([,v])=>v);} has(k){return this._d.some(([a])=>a===k);} entries(){return this._d[Symbol.iterator]();} forEach(cb){this._d.forEach(([k,v])=>cb(v,k));} };
|
|
// application/x-www-form-urlencoded serializer: like encodeURIComponent but
|
|
// space -> '+' and also percent-encoding the chars encodeURIComponent leaves
|
|
// bare ( ! ~ ' ( ) ), keeping the form-urlencoded safe set ( * - . _ ).
|
|
function _formEncode(s){
|
|
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 {
|
|
constructor(init=""){
|
|
this._p=[];
|
|
this._url=null; // set by URL.searchParams so mutations write back to the URL
|
|
if (typeof URLSearchParams === 'function' && init instanceof URLSearchParams) {
|
|
this._p = init._p.map(pair => [pair[0], pair[1]]);
|
|
} else if(typeof init==="string"){
|
|
this._parseString(init);
|
|
} else if (init && typeof init[Symbol.iterator] === 'function') {
|
|
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') {
|
|
Object.keys(init).forEach(k => this._p.push([String(k), String(init[k])]));
|
|
}
|
|
}
|
|
_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){
|
|
s = String(s).replace(/^\?/, "");
|
|
if (s === "") return;
|
|
for (const pair of s.split("&")) {
|
|
if (pair === "") continue;
|
|
const i = pair.indexOf("=");
|
|
const k = i === -1 ? pair : pair.slice(0, i);
|
|
const v = i === -1 ? "" : pair.slice(i + 1);
|
|
this._p.push([this._decode(k), this._decode(v)]);
|
|
}
|
|
}
|
|
_setFromString(s){ this._p = []; this._parseString(s); }
|
|
_notify(){ if (this._url) this._url._updateSearch(this.toString()); }
|
|
append(k,v){ this._p.push([String(k),String(v)]); this._notify(); }
|
|
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]);}
|
|
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,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,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(); }
|
|
get size(){ return this._p.length; }
|
|
toString(){return this._p.map(pair=>_formEncode(pair[0])+"="+_formEncode(pair[1])).join("&");}
|
|
forEach(cb,thisArg){this._p.slice().forEach(pair=>cb.call(thisArg,pair[1],pair[0],this));}
|
|
*entries(){ for (const pair of this._p) yield [pair[0],pair[1]]; }
|
|
*keys(){ for (const pair of this._p) yield pair[0]; }
|
|
*values(){ for (const pair of this._p) yield pair[1]; }
|
|
[Symbol.iterator](){ return this.entries(); }
|
|
};
|
|
|
|
// Real-enough DOMParser. The previous one-liner returned `globalThis.document`,
|
|
// so anything that did `new DOMParser().parseFromString(s, 'text/html')` and
|
|
// then read `.body.innerHTML` mutated the LIVE page (jQuery 3.x's selector
|
|
// feature-detect writes `<form></form>` and wiped real bodies). We parse the
|
|
// input into a detached `<html>` element and wrap it so the common Document
|
|
// API surface (body / head / documentElement / querySelector* / getElementById /
|
|
// getElementsByTagName / getElementsByClassName / title / cloneNode) works.
|
|
globalThis.DOMParser = class DOMParser {
|
|
parseFromString(source, mimeType) {
|
|
const html = String(source ?? "");
|
|
const isXml = typeof mimeType === "string" && /xml/i.test(mimeType);
|
|
const root = document.createElement("html");
|
|
// innerHTML parses children via html5ever fragment-parsing rules. Most
|
|
// HTML inputs start with `<!DOCTYPE>` / `<html>` / `<head>` etc.; the
|
|
// fragment parser strips the outer `<html>` and emits its head+body
|
|
// children, which is what callers want.
|
|
try { root.innerHTML = html; } catch (e) { /* leave empty on parse error */ }
|
|
|
|
// Helper: depth-first walk to find an element by predicate.
|
|
const walk = (node, pred) => {
|
|
if (!node) return null;
|
|
if (node.nodeType === 1 && pred(node)) return node;
|
|
const children = node.children || [];
|
|
for (let i = 0; i < children.length; i++) {
|
|
const r = walk(children[i], pred);
|
|
if (r) return r;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const findByTagName = (name) => walk(root, n => n.tagName === name);
|
|
|
|
const docNode = {
|
|
_root: root,
|
|
nodeName: "#document",
|
|
nodeType: 9,
|
|
contentType: isXml ? (mimeType || "application/xml") : "text/html",
|
|
get documentElement() { return root; },
|
|
get body() { return findByTagName("BODY"); },
|
|
get head() { return findByTagName("HEAD"); },
|
|
get title() {
|
|
const t = findByTagName("TITLE");
|
|
return t ? (t.textContent || "") : "";
|
|
},
|
|
get firstChild() { return root; },
|
|
get lastChild() { return root; },
|
|
get children() { 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); },
|
|
querySelectorAll(s) { return root.querySelectorAll(s); },
|
|
getElementById(id) {
|
|
return walk(root, n => n.getAttribute && n.getAttribute("id") === id);
|
|
},
|
|
getElementsByTagName(t) {
|
|
return root.querySelectorAll(t);
|
|
},
|
|
getElementsByClassName(c) {
|
|
return _getElementsByClassName(root, c);
|
|
},
|
|
getElementsByName(n) {
|
|
return root.querySelectorAll(`[name="${n}"]`);
|
|
},
|
|
createElement: (t) => document.createElement(t),
|
|
createElementNS: (ns, t) => document.createElement(t),
|
|
createTextNode: (t) => document.createTextNode(t),
|
|
createComment: (t) => document.createComment(t),
|
|
createDocumentFragment: () => document.createDocumentFragment(),
|
|
createRange: () => new Range(),
|
|
createEvent: (type) => document.createEvent(type),
|
|
createCDATASection: (data) => {
|
|
if (mimeType === "text/html") throw new DOMException("createCDATASection is not supported in HTML documents", "NotSupportedError");
|
|
const s = String(data);
|
|
if (s.indexOf("]]>") !== -1) throw new DOMException("CDATA section data must not contain ']]>'", "InvalidCharacterError");
|
|
return new CDATASection(+_dom("create_text_node", s));
|
|
},
|
|
createProcessingInstruction: (target, data) => {
|
|
const t = String(target), s = String(data);
|
|
if (!_isValidPITarget(t)) throw new DOMException("Invalid processing instruction target", "InvalidCharacterError");
|
|
if (s.indexOf("?>") !== -1) throw new DOMException("Processing instruction data must not contain '?>'", "InvalidCharacterError");
|
|
return new ProcessingInstruction(+_dom("create_text_node", s), t);
|
|
},
|
|
adoptNode: (n) => n,
|
|
importNode: (n) => n,
|
|
// Document-level node insertion. Detached docs from createHTMLDocument /
|
|
// createDocument back onto the same tree, so appending lands under the
|
|
// documentElement; enough for dom/common.js to build its Range fixtures.
|
|
appendChild: function (n) { try { root.appendChild(n); } catch (e) {} return n; },
|
|
removeChild: function (n) { try { root.removeChild(n); } catch (e) {} return n; },
|
|
insertBefore: function (n, ref) { try { root.insertBefore(n, ref); } catch (e) {} return n; },
|
|
_docType: null,
|
|
get doctype() { return this._docType; },
|
|
cloneNode: function (deep) {
|
|
return new DOMParser().parseFromString(root.outerHTML, mimeType);
|
|
},
|
|
contains(n) { return root.contains ? root.contains(n) : false; },
|
|
addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
|
|
};
|
|
return docNode;
|
|
}
|
|
};
|
|
globalThis.XMLSerializer = class XMLSerializer {
|
|
serializeToString(node) {
|
|
if (!node) return "";
|
|
if (node.nodeType === 10) {
|
|
let s = "<!DOCTYPE " + (node.name || "html");
|
|
if (node.publicId) s += ' PUBLIC "' + node.publicId + '"';
|
|
if (node.systemId) {
|
|
if (!node.publicId) s += " SYSTEM";
|
|
s += ' "' + node.systemId + '"';
|
|
}
|
|
s += ">";
|
|
return s;
|
|
}
|
|
if (node.outerHTML !== undefined) return node.outerHTML;
|
|
if (node.nodeType === 9) {
|
|
let s = "";
|
|
if (node.doctype) s += this.serializeToString(node.doctype);
|
|
if (node.documentElement) s += node.documentElement.outerHTML;
|
|
return s;
|
|
}
|
|
if (node.nodeType === 3) return node.textContent || "";
|
|
if (node.nodeType === 8) return "<!--" + (node.textContent || "") + "-->";
|
|
return "";
|
|
}
|
|
};
|
|
globalThis.performance = globalThis.performance || {
|
|
now: (function() {
|
|
// Monotonically non-decreasing: return the wall-clock offset, but never a
|
|
// value below the last one. Equal readings are allowed, and avoiding a
|
|
// synthetic per-call increment keeps tight loops from advancing the clock
|
|
// faster than real elapsed time.
|
|
var _last = -Infinity;
|
|
return function() {
|
|
var ms = Date.now() - (globalThis.performance.timeOrigin || 0);
|
|
if (ms < _last) return _last;
|
|
_last = ms;
|
|
return _last;
|
|
};
|
|
})(),
|
|
mark(){}, measure(){},
|
|
clearMarks(){}, clearMeasures(){}, clearResourceTimings(){},
|
|
getEntries(){return [];}, getEntriesByName(){return [];}, getEntriesByType(){return [];},
|
|
setResourceTimingBufferSize(){},
|
|
timeOrigin: 0,
|
|
timing: { navigationStart: 0, domContentLoadedEventEnd: 0, loadEventEnd: 0 },
|
|
navigation: { type: 0, redirectCount: 0 },
|
|
memory: {
|
|
jsHeapSizeLimit: 4294705152,
|
|
totalJSHeapSize: 19321856,
|
|
usedJSHeapSize: 16781520,
|
|
},
|
|
};
|
|
|
|
var _commonFonts = [
|
|
'Arial', 'Arial Black', 'Arial Narrow',
|
|
'Baskerville', 'Book Antiqua',
|
|
'Calibri', 'Cambria', 'Candara', 'Consolas', 'Courier New',
|
|
'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif',
|
|
'Futura',
|
|
'Garamond', 'Georgia', 'Gill Sans',
|
|
'Helvetica',
|
|
'Impact',
|
|
'Liberation Sans', 'Liberation Sans Mono', 'Liberation Serif',
|
|
'Lucida Console', 'Lucida Handwriting',
|
|
'Microsoft Sans Serif', 'Monaco',
|
|
'Noto Sans', 'Noto Serif',
|
|
'Palatino Linotype',
|
|
'Segoe UI',
|
|
'Tahoma', 'Times New Roman', 'Trebuchet MS',
|
|
'Verdana',
|
|
'Webdings', 'Wingdings',
|
|
];
|
|
Object.defineProperty(Document.prototype, 'fonts', {
|
|
get() {
|
|
const _set = _commonFonts.map((name, i) => ({
|
|
family: name, style: 'normal', weight: '400', stretch: 'normal',
|
|
status: 'loaded', loaded: Promise.resolve(this),
|
|
[Symbol.toStringTag]: 'FontFace',
|
|
}));
|
|
_set.forEach = (fn) => { _set.forEach(fn); };
|
|
_set.has = (f) => typeof f === 'string'
|
|
? _commonFonts.some(n => n.toLowerCase() === f.toLowerCase())
|
|
: _set.some(ff => ff.family === f?.family);
|
|
_set.delete = (f) => false;
|
|
_set.clear = () => {};
|
|
_set.add = () => {};
|
|
_set.load = () => Promise.resolve(_set);
|
|
_set.check = (font) => {
|
|
const m = typeof font === 'string' ? font.match(/["']([^"']+)["']/) : null;
|
|
return m ? _commonFonts.some(n => n.toLowerCase() === m[1].toLowerCase()) : true;
|
|
};
|
|
_set.ready = Promise.resolve(_set);
|
|
_set.status = 'loaded';
|
|
_set.addEventListener = () => {};
|
|
_set.removeEventListener = () => {};
|
|
_set.dispatchEvent = () => true;
|
|
return _set;
|
|
},
|
|
configurable: true,
|
|
});
|
|
globalThis.Crypto = class Crypto {
|
|
// Fill an integer TypedArray from the OS CSPRNG. Filling the underlying bytes
|
|
// (not per-element Math.random) keeps the distribution uniform across every
|
|
// typed-array width and is actually cryptographically random.
|
|
getRandomValues(arr) {
|
|
if (!ArrayBuffer.isView(arr) || arr instanceof DataView ||
|
|
arr instanceof Float32Array || arr instanceof Float64Array ||
|
|
(typeof Float16Array !== 'undefined' && arr instanceof Float16Array)) {
|
|
throw new DOMException("The provided ArrayBufferView is not an integer-typed array", "TypeMismatchError");
|
|
}
|
|
if (arr.byteLength > 65536) {
|
|
throw new DOMException("The requested length exceeds 65536 bytes", "QuotaExceededError");
|
|
}
|
|
const bytes = Deno.core.ops.op_random_bytes(arr.byteLength);
|
|
new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength).set(bytes);
|
|
return arr;
|
|
}
|
|
randomUUID() {
|
|
const b = Deno.core.ops.op_random_bytes(16);
|
|
b[6] = (b[6] & 0x0f) | 0x40; // version 4
|
|
b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
|
|
let s = "";
|
|
for (let i = 0; i < 16; i++) {
|
|
s += (b[i] + 0x100).toString(16).slice(1);
|
|
if (i === 3 || i === 5 || i === 7 || i === 9) s += "-";
|
|
}
|
|
return s;
|
|
}
|
|
};
|
|
globalThis.crypto = globalThis.crypto || new globalThis.Crypto();
|
|
// Real structured clone (not JSON). JSON.parse(JSON.stringify) silently drops
|
|
// ArrayBuffer/TypedArray (they serialize to {}), so Cloudflare's turnstile
|
|
// orchestrate loses every byte it tries to round-trip through postMessage and
|
|
// the challenge never completes (issue #389). Clone buffers, typed arrays,
|
|
// maps/sets, dates, errors, and plain objects recursively; CryptoKey and other
|
|
// types that register a clone hook (see crypto.subtle below) are routed there.
|
|
function _structuredClone(value, seen) {
|
|
// Functions and symbols are not structured-cloneable (HTML structured clone,
|
|
// DataCloneError). This must run before the primitive early-return below,
|
|
// which would otherwise pass them through by reference.
|
|
if (typeof value === "function" || typeof value === "symbol") {
|
|
throw new DOMException("Failed to execute 'structuredClone': value could not be cloned.", "DataCloneError");
|
|
}
|
|
if (value === null || typeof value !== "object") return value;
|
|
if (seen.has(value)) return seen.get(value);
|
|
// Typed arrays: copy the underlying buffer slice. DataView has no .slice(),
|
|
// so slice its buffer over the view's range and wrap a fresh view.
|
|
if (ArrayBuffer.isView(value)) {
|
|
if (value instanceof DataView) {
|
|
const buf = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
const copy = new DataView(buf);
|
|
seen.set(value, copy);
|
|
return copy;
|
|
}
|
|
const Ctor = value.constructor;
|
|
const copy = new Ctor(value.slice());
|
|
seen.set(value, copy);
|
|
return copy;
|
|
}
|
|
if (value instanceof ArrayBuffer) {
|
|
const copy = value.slice(0);
|
|
seen.set(value, copy);
|
|
return copy;
|
|
}
|
|
if (value instanceof SharedArrayBuffer) {
|
|
return value; // transferable, not copyable
|
|
}
|
|
if (value instanceof Date) return new Date(value.getTime());
|
|
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
|
if (value instanceof Map) {
|
|
const m = new Map();
|
|
seen.set(value, m);
|
|
for (const [k, v] of value) m.set(_structuredClone(k, seen), _structuredClone(v, seen));
|
|
return m;
|
|
}
|
|
if (value instanceof Set) {
|
|
const s = new Set();
|
|
seen.set(value, s);
|
|
for (const v of value) s.add(_structuredClone(v, seen));
|
|
return s;
|
|
}
|
|
if (value instanceof Error) {
|
|
const Ctor = value.constructor || Error;
|
|
const e = new Ctor(value.message);
|
|
// Record the clone before recursing into `cause`, otherwise a cycle
|
|
// through the error (e.cause === e) recurses until the stack overflows.
|
|
seen.set(value, e);
|
|
if (value.name) e.name = value.name;
|
|
if (value.stack) e.stack = value.stack;
|
|
if (value.cause !== undefined) e.cause = _structuredClone(value.cause, seen);
|
|
return e;
|
|
}
|
|
// Platform objects that carry internal slots opt into cloning via a hook
|
|
// (CryptoKey re-registers its key material so the clone stays usable by
|
|
// crypto.subtle). Anything else with a registered hook takes that path.
|
|
if (typeof value[Symbol.toStringTag] === "string" && globalThis.__obscura_clone_hooks) {
|
|
const hook = globalThis.__obscura_clone_hooks[value[Symbol.toStringTag]];
|
|
if (typeof hook === "function") return hook(value, seen);
|
|
}
|
|
// Plain objects clone onto Object.prototype (like Chrome), not the source's
|
|
// prototype. Define each property instead of assigning it: a source with an
|
|
// own enumerable `__proto__` data prop (what JSON.parse('{"__proto__":…}')
|
|
// yields) would otherwise hit the inherited __proto__ setter and reparent
|
|
// the clone instead of copying the property.
|
|
const out = Array.isArray(value) ? [] : {};
|
|
seen.set(value, out);
|
|
for (const k in value) {
|
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
const cloned = _structuredClone(value[k], seen);
|
|
// Only `__proto__` needs defineProperty: plain assignment would hit the
|
|
// inherited prototype setter and reparent the clone instead of adding an
|
|
// own data property. Every other key takes the fast assignment path.
|
|
if (k === "__proto__") {
|
|
Object.defineProperty(out, k, {
|
|
value: cloned,
|
|
writable: true,
|
|
enumerable: true,
|
|
configurable: true,
|
|
});
|
|
} else {
|
|
out[k] = cloned;
|
|
}
|
|
}
|
|
}
|
|
// Symbols are not enumerable via for-in; copy own symbol-keyed properties.
|
|
const syms = Object.getOwnPropertySymbols(value);
|
|
for (const s of syms) {
|
|
const d = Object.getOwnPropertyDescriptor(value, s);
|
|
if (d && "value" in d) out[s] = _structuredClone(d.value, seen);
|
|
}
|
|
return out;
|
|
}
|
|
globalThis.structuredClone = globalThis.structuredClone || ((v) => _structuredClone(v, new Map()));
|
|
globalThis.reportError = globalThis.reportError || ((e) => console.error(e));
|
|
|
|
// WHATWG Storage as a legacy platform object: a Proxy routes property access
|
|
// (localStorage.foo, localStorage["foo"], delete, `in`, Object.keys) through
|
|
// the named getter/setter so length/key()/iteration stay in sync with the
|
|
// backing map. Plain prototype methods alone could not intercept direct
|
|
// property access, so `localStorage.foo = x` never updated length before.
|
|
globalThis.Storage = function Storage() {};
|
|
Storage.prototype.getItem = function(k) { k = String(k); return Object.prototype.hasOwnProperty.call(this._data, k) ? this._data[k] : null; };
|
|
Storage.prototype.setItem = function(k, v) { this._data[String(k)] = String(v); };
|
|
Storage.prototype.removeItem = function(k) { delete this._data[String(k)]; };
|
|
Storage.prototype.clear = function() { const d = this._data; for (const k in d) delete d[k]; };
|
|
Storage.prototype.key = function(i) { const ks = Object.keys(this._data); i = i >>> 0; return i < ks.length ? ks[i] : null; };
|
|
Object.defineProperty(Storage.prototype, 'length', { get: function() { return Object.keys(this._data).length; }, configurable: true });
|
|
|
|
const _mkStore = () => {
|
|
const target = Object.create(Storage.prototype);
|
|
Object.defineProperty(target, '_data', { value: Object.create(null), writable: true, enumerable: false, configurable: true });
|
|
const isReal = (p) => p === '_data' || p === 'constructor' || (p in Storage.prototype);
|
|
return new Proxy(target, {
|
|
get(t, p, recv) { if (typeof p === 'symbol' || isReal(p)) return Reflect.get(t, p, recv); const v = t.getItem(p); return v === null ? undefined : v; },
|
|
set(t, p, v, recv) { if (typeof p === 'symbol' || isReal(p)) return Reflect.set(t, p, v, recv); t.setItem(p, v); return true; },
|
|
has(t, p) { if (typeof p === 'symbol' || isReal(p)) return true; return Object.prototype.hasOwnProperty.call(t._data, p); },
|
|
deleteProperty(t, p) { if (typeof p === 'symbol' || isReal(p)) return Reflect.deleteProperty(t, p); t.removeItem(p); return true; },
|
|
ownKeys(t) { return Object.keys(t._data); },
|
|
getOwnPropertyDescriptor(t, p) {
|
|
if (typeof p !== 'symbol' && Object.prototype.hasOwnProperty.call(t._data, p))
|
|
return { value: t._data[p], writable: true, enumerable: true, configurable: true };
|
|
return Reflect.getOwnPropertyDescriptor(t, p);
|
|
},
|
|
});
|
|
};
|
|
globalThis.localStorage = _mkStore();
|
|
globalThis.sessionStorage = _mkStore();
|
|
|
|
globalThis.btoa = globalThis.btoa || ((s) => { const b = new TextEncoder().encode(s); const c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let r=""; for(let i=0;i<b.length;i+=3){const a=b[i],bb=b[i+1]??0,cc=b[i+2]??0; r+=c[a>>2]+c[((a&3)<<4)|(bb>>4)]+(i+1<b.length?c[((bb&15)<<2)|(cc>>6)]:"=")+(i+2<b.length?c[cc&63]:"=");} return r; });
|
|
globalThis.atob = globalThis.atob || ((s) => { const c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let r=[]; for(let i=0;i<s.length;i+=4){const a=c.indexOf(s[i]),b=c.indexOf(s[i+1]),cc=c.indexOf(s[i+2]),d=c.indexOf(s[i+3]); r.push((a<<2)|(b>>4)); if(cc>=0)r.push(((b&15)<<4)|(cc>>2)); if(d>=0)r.push(((cc&3)<<6)|d);} return String.fromCharCode(...r); });
|
|
|
|
// Functional History API. The earlier stub returned constant state and was a
|
|
// no-op on push/replace, so any SPA that tried to update its URL (Next.js
|
|
// client router, React Router, vue-router, hash-based routers) silently
|
|
// failed: location.href stayed pinned to the initial page, useLocation hooks
|
|
// never updated, and popstate-driven UI froze.
|
|
//
|
|
// Internally we keep a tiny in-memory stack of {state, url} entries. push/
|
|
// replace mutate the stack and set globalThis.__virtualUrl so location.href
|
|
// reads the new URL. Real Chrome doesn't fire popstate on push/replace,
|
|
// only on user-driven back/forward — we match that exactly.
|
|
(() => {
|
|
const stack = [{state: null, url: undefined}]; // initial entry; url=undefined means "use document URL"
|
|
let idx = 0;
|
|
const resolveOrFallback = (url) => {
|
|
// A missing url (pushState/replaceState called with < 3 args) keeps the
|
|
// current document URL per the HTML spec — capture it so the entry does not
|
|
// reset location back to the original document URL.
|
|
if (url === null || url === undefined) return __currentUrl();
|
|
try { return new URL(String(url), __currentUrl()).href; } catch (e) { return String(url); }
|
|
};
|
|
const applyVirtual = () => {
|
|
const entry = stack[idx];
|
|
globalThis.__virtualUrl = entry.url ?? null;
|
|
};
|
|
const fireHashChangeIfNeeded = (prevUrl) => {
|
|
try {
|
|
const next = __currentUrl();
|
|
if (!prevUrl || !next) return;
|
|
const a = new URL(prevUrl), b = new URL(next);
|
|
if (a.origin === b.origin && a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash) {
|
|
const ev = new Event('hashchange');
|
|
ev.oldURL = prevUrl; ev.newURL = next;
|
|
try { globalThis.dispatchEvent(ev); } catch {}
|
|
}
|
|
} catch {}
|
|
};
|
|
globalThis.history = {
|
|
get length() { return stack.length; },
|
|
get state() { return stack[idx].state; },
|
|
scrollRestoration: "auto",
|
|
pushState(state, _title, url) {
|
|
const prevUrl = __currentUrl();
|
|
const resolved = resolveOrFallback(url);
|
|
// Truncate forward entries (real Chrome drops the forward stack on a
|
|
// new push) then append + advance.
|
|
stack.length = idx + 1;
|
|
stack.push({state: state ?? null, url: resolved});
|
|
idx = stack.length - 1;
|
|
applyVirtual();
|
|
fireHashChangeIfNeeded(prevUrl);
|
|
},
|
|
replaceState(state, _title, url) {
|
|
const prevUrl = __currentUrl();
|
|
const resolved = resolveOrFallback(url);
|
|
stack[idx] = {state: state ?? null, url: resolved};
|
|
applyVirtual();
|
|
fireHashChangeIfNeeded(prevUrl);
|
|
},
|
|
go(n) {
|
|
n = (n | 0);
|
|
if (n === 0) return; // real spec: go(0) reloads. We don't reload SPAs.
|
|
const next = Math.max(0, Math.min(stack.length - 1, idx + n));
|
|
if (next === idx) return;
|
|
const prevUrl = __currentUrl();
|
|
idx = next;
|
|
applyVirtual();
|
|
// Real Chrome fires popstate on back/forward with the destination entry's state.
|
|
try {
|
|
const ev = new PopStateEvent('popstate', {state: stack[idx].state});
|
|
globalThis.dispatchEvent(ev);
|
|
} catch {}
|
|
fireHashChangeIfNeeded(prevUrl);
|
|
},
|
|
back() { this.go(-1); },
|
|
forward() { this.go(1); },
|
|
};
|
|
})();
|
|
globalThis.screenX = 0; globalThis.screenY = 0;
|
|
globalThis.screenLeft = 0; globalThis.screenTop = 0;
|
|
globalThis.pageXOffset = 0; globalThis.pageYOffset = 0;
|
|
globalThis.scrollX = 0; globalThis.scrollY = 0;
|
|
|
|
globalThis.CSS = {
|
|
supports(prop, value){
|
|
try {
|
|
var p, v;
|
|
if (arguments.length >= 2) { p = String(prop).trim(); v = String(value).trim(); }
|
|
else {
|
|
var cond = String(prop).trim().replace(/^\(+|\)+$/g, "").trim();
|
|
var idx = cond.indexOf(":");
|
|
if (idx === -1) return false;
|
|
p = cond.slice(0, idx).trim(); v = cond.slice(idx + 1).trim();
|
|
}
|
|
if (!p || !v) return false;
|
|
// The engine renders standard CSS; report it as supported so feature-gated
|
|
// SPAs don't bail to /unsupported. (Previous stub always returned false.)
|
|
return true;
|
|
} catch (e) { return false; }
|
|
},
|
|
escape(s){ return s; }
|
|
};
|
|
|
|
globalThis.HTMLElement = Element;
|
|
globalThis.HTMLDivElement = Element;
|
|
globalThis.HTMLSpanElement = Element;
|
|
globalThis.HTMLParagraphElement = Element;
|
|
globalThis.HTMLAnchorElement = Element;
|
|
globalThis.HTMLImageElement = Element;
|
|
globalThis.HTMLInputElement = Element;
|
|
globalThis.HTMLButtonElement = Element;
|
|
globalThis.HTMLFormElement = class HTMLFormElement extends Element {
|
|
get elements() { return HTMLCollection._from(this.querySelectorAll("input, select, textarea, button, fieldset, output, object")); }
|
|
get length() { return this.elements.length; }
|
|
// Inherit submit() from Element.prototype: it dispatches the cancelable
|
|
// 'submit' event and (if not prevented) builds form data and navigates.
|
|
reset() { for (const f of this.elements) { if ('value' in f) f.value = ''; } }
|
|
};
|
|
globalThis.HTMLSelectElement = Element;
|
|
globalThis.HTMLTextAreaElement = Element;
|
|
globalThis.HTMLLabelElement = Element;
|
|
globalThis.HTMLTableElement = Element;
|
|
globalThis.HTMLIFrameElement = Element;
|
|
globalThis.HTMLCanvasElement = Element;
|
|
// HTMLVideoElement and HTMLAudioElement are defined above with canPlayType support.
|
|
globalThis.HTMLScriptElement = Element;
|
|
globalThis.HTMLStyleElement = Element;
|
|
globalThis.HTMLLinkElement = Element;
|
|
globalThis.HTMLMetaElement = Element;
|
|
globalThis.HTMLHeadElement = Element;
|
|
globalThis.HTMLBodyElement = Element;
|
|
globalThis.HTMLHtmlElement = Element;
|
|
globalThis.HTMLBRElement = Element;
|
|
globalThis.HTMLHRElement = Element;
|
|
globalThis.HTMLUListElement = Element;
|
|
globalThis.HTMLOListElement = Element;
|
|
globalThis.HTMLLIElement = Element;
|
|
globalThis.HTMLPreElement = Element;
|
|
globalThis.HTMLHeadingElement = Element;
|
|
globalThis.HTMLTemplateElement = Element;
|
|
globalThis.HTMLSlotElement = Element;
|
|
globalThis.HTMLOptionElement = Element;
|
|
globalThis.HTMLDataListElement = Element;
|
|
globalThis.HTMLFieldSetElement = Element;
|
|
globalThis.HTMLLegendElement = Element;
|
|
globalThis.HTMLProgressElement = Element;
|
|
globalThis.HTMLDetailsElement = Element;
|
|
globalThis.HTMLDialogElement = Element;
|
|
// SVGAnimatedString backs the className and href reflections on SVG elements.
|
|
// baseVal and animVal both read the live attribute (no SMIL animation), and
|
|
// baseVal is writable. Used by the SVG-aware get className()/get href() above.
|
|
function SVGAnimatedString(el, attr, fallbackAttr) {
|
|
this._el = el;
|
|
this._attr = attr;
|
|
this._fallback = fallbackAttr || null;
|
|
}
|
|
SVGAnimatedString.prototype._read = function() {
|
|
let v = this._el.getAttribute(this._attr);
|
|
if (v === null && this._fallback) v = this._el.getAttribute(this._fallback);
|
|
return v == null ? '' : v;
|
|
};
|
|
Object.defineProperty(SVGAnimatedString.prototype, 'baseVal', {
|
|
get() { return this._read(); },
|
|
set(v) { this._el.setAttribute(this._attr, String(v)); },
|
|
configurable: true, enumerable: true,
|
|
});
|
|
Object.defineProperty(SVGAnimatedString.prototype, 'animVal', {
|
|
get() { return this._read(); },
|
|
configurable: true, enumerable: true,
|
|
});
|
|
Object.defineProperty(SVGAnimatedString.prototype, Symbol.toStringTag, { value: 'SVGAnimatedString', configurable: true });
|
|
_markNative(SVGAnimatedString);
|
|
|
|
globalThis.SVGElement = Element;
|
|
globalThis.SVGSVGElement = Element;
|
|
globalThis.CharacterData = CharacterData;
|
|
globalThis.Text = Text;
|
|
globalThis.Comment = Comment;
|
|
|
|
globalThis.CDATASection = CDATASection;
|
|
globalThis.ProcessingInstruction = ProcessingInstruction;
|
|
// True when the document was loaded from an XML/XHTML source. Obscura has no
|
|
// native XML tree, so this is inferred from contentType (derived from the URL).
|
|
function _isXMLDocument(doc) {
|
|
const ct = (doc && doc.contentType) || "text/html";
|
|
return ct !== "text/html";
|
|
}
|
|
// XML Name production, sufficient for createProcessingInstruction targets.
|
|
const _piNameStart = "A-Za-z_:\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
|
|
const _piNameChar = _piNameStart + "0-9.\\u00B7\\u0300-\\u036F\\u203F-\\u2040\\-";
|
|
const _piNameRe = new RegExp("^[" + _piNameStart + "][" + _piNameChar + "]*$");
|
|
function _isValidPITarget(target) {
|
|
return typeof target === "string" && target.length > 0 && _piNameRe.test(target);
|
|
}
|
|
globalThis.DocumentFragment = DocumentFragment;
|
|
globalThis.DocumentType = DocumentType;
|
|
globalThis.Node = Node;
|
|
globalThis.Element = Element;
|
|
globalThis.Document = Document;
|
|
// CSSStyleDeclaration is the type of element.style and getComputedStyle(); it is
|
|
// pre-declared non-enumerable in _preHideInternals, but unlike the other WebIDL
|
|
// interfaces it had no value assignment, leaving `window.CSSStyleDeclaration`
|
|
// undefined (so `el.style instanceof CSSStyleDeclaration` threw). Assigning here
|
|
// only fills the value; the property stays enumerable:false, matching Chrome.
|
|
globalThis.CSSStyleDeclaration = CSSStyleDeclaration;
|
|
globalThis.XPathResult = globalThis.XPathResult || class XPathResult {};
|
|
Object.assign(globalThis.XPathResult, {
|
|
ANY_TYPE: 0,
|
|
NUMBER_TYPE: 1,
|
|
STRING_TYPE: 2,
|
|
BOOLEAN_TYPE: 3,
|
|
UNORDERED_NODE_ITERATOR_TYPE: 4,
|
|
ORDERED_NODE_ITERATOR_TYPE: 5,
|
|
UNORDERED_NODE_SNAPSHOT_TYPE: 6,
|
|
ORDERED_NODE_SNAPSHOT_TYPE: 7,
|
|
ANY_UNORDERED_NODE_TYPE: 8,
|
|
FIRST_ORDERED_NODE_TYPE: 9,
|
|
});
|
|
// 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
|
|
// share Element's append / prepend / replaceChildren.
|
|
for (const _proto of [Document.prototype, DocumentFragment.prototype]) {
|
|
_proto.append = Element.prototype.append;
|
|
_proto.prepend = Element.prototype.prepend;
|
|
_proto.replaceChildren = Element.prototype.replaceChildren;
|
|
}
|
|
globalThis.EventTarget = Node;
|
|
globalThis.HTMLCollection = class HTMLCollection extends Array {
|
|
item(i) {
|
|
i = i >>> 0;
|
|
return this[i] != null ? this[i] : null;
|
|
}
|
|
namedItem(name) {
|
|
if (name === undefined || name === null || name === "") return null;
|
|
name = String(name);
|
|
for (let i = 0; i < this.length; i++) {
|
|
const el = this[i];
|
|
if (!el) continue;
|
|
// id always contributes; name only for HTML elements in HTML documents.
|
|
if (el.id === name) return el;
|
|
if (_isHTMLEl(el) && typeof el.getAttribute === "function" && el.getAttribute("name") === name) return el;
|
|
}
|
|
return null;
|
|
}
|
|
// Factory: build an HTMLCollection from an array of elements. Named access
|
|
// (collection[name]) is served lazily by a Proxy so there is NO per-element
|
|
// work at build time (eager defineProperty per id was an O(n) build cost that
|
|
// made querySelectorAll on large result sets ~26x slower). The Proxy only
|
|
// resolves a name when an unknown string key is actually read.
|
|
static _from(arr) {
|
|
const c = new HTMLCollection();
|
|
if (arr) for (let i = 0; i < arr.length; i++) { if (arr[i]) c[c.length] = arr[i]; }
|
|
return new Proxy(c, _htmlCollectionProxy);
|
|
}
|
|
};
|
|
_markNative(HTMLCollection.prototype.item);
|
|
_markNative(HTMLCollection.prototype.namedItem);
|
|
// Shared (allocated once) Proxy traps for HTMLCollection named access. Indices,
|
|
// length, and inherited methods resolve normally via Reflect; only an unknown
|
|
// non-numeric string key falls back to namedItem(), so item/namedItem and the
|
|
// Array methods are never shadowed and id="namedItem" cannot recurse.
|
|
const _htmlCollectionProxy = {
|
|
get(t, k, r) {
|
|
const v = Reflect.get(t, k, r);
|
|
if (v !== undefined || typeof k !== "string") return v;
|
|
return t.namedItem ? (t.namedItem(k) || undefined) : undefined;
|
|
},
|
|
has(t, k) {
|
|
if (Reflect.has(t, k)) return true;
|
|
return typeof k === "string" && !!(t.namedItem && t.namedItem(k));
|
|
},
|
|
};
|
|
// True for elements in the HTML namespace (the only ones whose name attribute
|
|
// contributes to an HTMLCollection's supported property names).
|
|
function _isHTMLEl(el) {
|
|
return !!el && (el.namespaceURI === undefined || el.namespaceURI === "http://www.w3.org/1999/xhtml");
|
|
}
|
|
// Build a NodeList (no named access, per spec) for querySelectorAll and
|
|
// childNodes. Kept light on purpose: querySelectorAll is the hottest query API.
|
|
function _nodeList(els) {
|
|
const nl = new NodeList();
|
|
for (let i = 0; i < els.length; i++) nl[i] = els[i];
|
|
nl.length = els.length;
|
|
return nl;
|
|
}
|
|
globalThis.DOMTokenList = DOMTokenList;
|
|
// NodeList is its own type, not an Array subclass: in a real browser
|
|
// Array.isArray(nodeList) is false and Object.prototype.toString reports
|
|
// "[object NodeList]". Fingerprinting and feature-detection scripts check both.
|
|
// It keeps the array-like surface scripts actually use: indexed access, length,
|
|
// item(), forEach(), entries/keys/values, and iteration (so spread and for..of
|
|
// work).
|
|
globalThis.NodeList = class NodeList {
|
|
constructor() { this.length = 0; }
|
|
item(i) { i = i >>> 0; return this[i] != null ? this[i] : null; }
|
|
forEach(cb, thisArg) {
|
|
for (let i = 0; i < this.length; i++) cb.call(thisArg, this[i], i, this);
|
|
}
|
|
*[Symbol.iterator]() { for (let i = 0; i < this.length; i++) yield this[i]; }
|
|
*entries() { for (let i = 0; i < this.length; i++) yield [i, this[i]]; }
|
|
*keys() { for (let i = 0; i < this.length; i++) yield i; }
|
|
*values() { for (let i = 0; i < this.length; i++) yield this[i]; }
|
|
get [Symbol.toStringTag]() { return 'NodeList'; }
|
|
};
|
|
_markNative(NodeList);
|
|
_markNative(NodeList.prototype.item);
|
|
_markNative(NodeList.prototype.forEach);
|
|
// Live Range over the real DOM tree. dom/ranges/* tests are pure boundary-point
|
|
// algorithms (no layout, no editing engine), so a property-storing Range with
|
|
// correct tree-order comparison passes them. Mutating ops (extract/delete/
|
|
// insert/surround) are kept minimal: they do not throw, but do not rewrite the
|
|
// tree (that is the editing mega-bucket, out of scope).
|
|
function _rngNodeLength(n) {
|
|
const t = n.nodeType;
|
|
if (t === 3 || t === 4 || t === 8 || t === 7) return (n.data || n.nodeValue || "").length;
|
|
return n.childNodes.length;
|
|
}
|
|
// Index among siblings, computed in Rust (one op) instead of serializing the
|
|
// whole childNodes list per call: the Range matrices call this heavily.
|
|
function _rngNodeIndex(n) {
|
|
if (!n.parentNode) return 0;
|
|
return +_dom("node_index", n._nid);
|
|
}
|
|
function _rngSame(a, b) { return a === b || (!!a && !!b && a._nid === b._nid); }
|
|
// Root nid in one op (callers only read ._nid), instead of an O(depth) walk.
|
|
function _rngRoot(n) { return { _nid: +_dom("node_root", n._nid) }; }
|
|
function _rngAncestors(n) { const a = []; let c = n; while (c) { a.push(c); c = c.parentNode; } return a; }
|
|
// document (preorder) tree order: -1 if a precedes b, 1 if a follows b, 0 same.
|
|
// Computed in Rust (one op) rather than walking ancestor chains over per-step
|
|
// DOM ops, which made the large dom/ranges matrices time out.
|
|
function _rngOrder(a, b) {
|
|
if (_rngSame(a, b)) return 0;
|
|
return +_dom("compare_order", a._nid, b._nid) || 0;
|
|
}
|
|
// Position of (nA,oA) relative to (nB,oB): -1 before, 0 equal, 1 after.
|
|
function _rngCmp(nA, oA, nB, oB) {
|
|
if (_rngSame(nA, nB)) return oA < oB ? -1 : (oA > oB ? 1 : 0);
|
|
if (_rngOrder(nA, nB) > 0) return -_rngCmp(nB, oB, nA, oA);
|
|
if (nA.contains && nA.contains(nB)) { // nA is a strict ancestor of nB
|
|
let child = nB;
|
|
while (child && child.parentNode && child.parentNode._nid !== nA._nid) child = child.parentNode;
|
|
if (child && child.parentNode && child.parentNode._nid === nA._nid && _rngNodeIndex(child) < oA) return 1;
|
|
return -1;
|
|
}
|
|
return -1;
|
|
}
|
|
function _rngCheckOffset(n, o) {
|
|
if (n && n.nodeType === 10) throw new DOMException("Range boundary cannot be a DocumentType", "InvalidNodeTypeError");
|
|
if (o < 0 || o > _rngNodeLength(n)) throw new DOMException("Range offset out of bounds", "IndexSizeError");
|
|
}
|
|
globalThis.Range = class Range {
|
|
constructor() {
|
|
const d = globalThis.document || null;
|
|
this._sc = d; this._so = 0; this._ec = d; this._eo = 0;
|
|
}
|
|
get startContainer() { return this._sc; }
|
|
get startOffset() { return this._so; }
|
|
get endContainer() { return this._ec; }
|
|
get endOffset() { return this._eo; }
|
|
get collapsed() { return _rngSame(this._sc, this._ec) && this._so === this._eo; }
|
|
get commonAncestorContainer() {
|
|
if (!this._sc || !this._ec) return null;
|
|
const setA = new Set(_rngAncestors(this._sc).map(n => n._nid));
|
|
let c = this._ec;
|
|
while (c) { if (setA.has(c._nid)) return c; c = c.parentNode; }
|
|
return null;
|
|
}
|
|
setStart(n, o) { _rngCheckOffset(n, o); this._sc = n; this._so = o; if (_rngRoot(n)._nid !== _rngRoot(this._ec)._nid || _rngCmp(this._sc, this._so, this._ec, this._eo) > 0) { this._ec = n; this._eo = o; } }
|
|
setEnd(n, o) { _rngCheckOffset(n, o); this._ec = n; this._eo = o; if (_rngRoot(n)._nid !== _rngRoot(this._sc)._nid || _rngCmp(this._sc, this._so, this._ec, this._eo) > 0) { this._sc = n; this._so = o; } }
|
|
setStartBefore(n) { const p = n.parentNode; if (!p) throw new DOMException("node has no parent", "InvalidNodeTypeError"); this.setStart(p, _rngNodeIndex(n)); }
|
|
setStartAfter(n) { const p = n.parentNode; if (!p) throw new DOMException("node has no parent", "InvalidNodeTypeError"); this.setStart(p, _rngNodeIndex(n) + 1); }
|
|
setEndBefore(n) { const p = n.parentNode; if (!p) throw new DOMException("node has no parent", "InvalidNodeTypeError"); this.setEnd(p, _rngNodeIndex(n)); }
|
|
setEndAfter(n) { const p = n.parentNode; if (!p) throw new DOMException("node has no parent", "InvalidNodeTypeError"); this.setEnd(p, _rngNodeIndex(n) + 1); }
|
|
collapse(toStart) { if (toStart) { this._ec = this._sc; this._eo = this._so; } else { this._sc = this._ec; this._so = this._eo; } }
|
|
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; }
|
|
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 (n.nodeType === 10) throw new DOMException("node is a DocumentType", "InvalidNodeTypeError");
|
|
if (o > _rngNodeLength(n)) throw new DOMException("offset out of bounds", "IndexSizeError");
|
|
if (_rngCmp(n, o, this._sc, this._so) < 0) return -1;
|
|
if (_rngCmp(n, o, this._ec, this._eo) > 0) return 1;
|
|
return 0;
|
|
}
|
|
isPointInRange(n, o) {
|
|
o = o >>> 0;
|
|
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 (o > _rngNodeLength(n)) throw new DOMException("offset out of bounds", "IndexSizeError");
|
|
return _rngCmp(n, o, this._sc, this._so) >= 0 && _rngCmp(n, o, this._ec, this._eo) <= 0;
|
|
}
|
|
compareBoundaryPoints(how, other) {
|
|
// `how` is a WebIDL `unsigned short`: ToUint16-convert before validating,
|
|
// so NaN/Infinity become 0 (START_TO_START) rather than throwing.
|
|
let h = Math.trunc(Number(how));
|
|
if (!Number.isFinite(h)) h = 0;
|
|
h = ((h % 65536) + 65536) % 65536;
|
|
let a, b;
|
|
switch (h) {
|
|
case 0: a = [this._sc, this._so]; b = [other._sc, other._so]; break; // START_TO_START
|
|
case 1: a = [this._ec, this._eo]; b = [other._sc, other._so]; break; // START_TO_END
|
|
case 2: a = [this._ec, this._eo]; b = [other._ec, other._eo]; break; // END_TO_END
|
|
case 3: a = [this._sc, this._so]; b = [other._ec, other._eo]; break; // END_TO_START
|
|
default: throw new DOMException("invalid comparison type", "NotSupportedError");
|
|
}
|
|
// Different roots -> WrongDocumentError. Guard so a null/foreign container
|
|
// raises that DOMException rather than a raw TypeError from _rngRoot.
|
|
let differ;
|
|
try { differ = _rngRoot(a[0])._nid !== _rngRoot(b[0])._nid; }
|
|
catch (e) { differ = true; }
|
|
if (differ) throw new DOMException("The two Ranges are not in the same tree.", "WrongDocumentError");
|
|
return _rngCmp(a[0], a[1], b[0], b[1]);
|
|
}
|
|
intersectsNode(n) {
|
|
if (_rngRoot(n)._nid !== _rngRoot(this._sc)._nid) return false;
|
|
const p = n.parentNode;
|
|
if (!p) return true;
|
|
const o = _rngNodeIndex(n);
|
|
return _rngCmp(p, o, this._ec, this._eo) < 0 && _rngCmp(p, o + 1, this._sc, this._so) > 0;
|
|
}
|
|
cloneRange() { const r = new Range(); r._sc = this._sc; r._so = this._so; r._ec = this._ec; r._eo = this._eo; return r; }
|
|
createContextualFragment(html) {
|
|
if (arguments.length < 1) throw new TypeError("Failed to execute 'createContextualFragment' on 'Range': 1 argument required, but only 0 present.");
|
|
const node = this._sc;
|
|
const ownerDoc = (node && node.ownerDocument) || globalThis.document;
|
|
const frag = ownerDoc.createDocumentFragment();
|
|
frag.innerHTML = String(html);
|
|
return frag;
|
|
}
|
|
toString() {
|
|
const sc = this._sc, ec = this._ec;
|
|
if (!sc) return "";
|
|
if (_rngSame(sc, ec) && (sc.nodeType === 3 || sc.nodeType === 4)) return (sc.data || "").slice(this._so, this._eo);
|
|
let s = "";
|
|
if (sc.nodeType === 3 || sc.nodeType === 4) s += (sc.data || "").slice(this._so);
|
|
const cac = this.commonAncestorContainer;
|
|
if (cac) {
|
|
const walk = (node) => {
|
|
if (node.nodeType === 3 || node.nodeType === 4) {
|
|
if (!_rngSame(node, sc) && !_rngSame(node, ec) &&
|
|
_rngCmp(node, 0, this._sc, this._so) >= 0 && _rngCmp(node, _rngNodeLength(node), this._ec, this._eo) <= 0) {
|
|
s += (node.data || "");
|
|
}
|
|
}
|
|
const kids = node.childNodes;
|
|
for (let i = 0; i < kids.length; i++) if (kids[i]) walk(kids[i]);
|
|
};
|
|
walk(cac);
|
|
}
|
|
if (!_rngSame(sc, ec) && (ec.nodeType === 3 || ec.nodeType === 4)) s += (ec.data || "").slice(0, this._eo);
|
|
return s;
|
|
}
|
|
cloneContents() { return (globalThis.document || document).createDocumentFragment(); }
|
|
extractContents() { return (globalThis.document || document).createDocumentFragment(); }
|
|
deleteContents() {}
|
|
insertNode(node) { if (node && this._sc && this._sc.insertBefore) { const kids = this._sc.childNodes; this._sc.insertBefore(node, kids[this._so] || null); } }
|
|
surroundContents(node) { this.insertNode(node); }
|
|
detach() {}
|
|
getBoundingClientRect() {
|
|
if (this.collapsed) return new DOMRect();
|
|
let cac = this.commonAncestorContainer;
|
|
while (cac && cac.nodeType !== 1 && cac.nodeType !== 9) cac = cac.parentNode;
|
|
if (cac && cac.getBoundingClientRect) {
|
|
const r = cac.getBoundingClientRect();
|
|
return new DOMRect(r.x, r.y, r.width, r.height);
|
|
}
|
|
return new DOMRect();
|
|
}
|
|
getClientRects() {
|
|
if (this.collapsed) return new DOMRectList([]);
|
|
return new DOMRectList([this.getBoundingClientRect()]);
|
|
}
|
|
static get START_TO_START() { return 0; }
|
|
static get START_TO_END() { return 1; }
|
|
static get END_TO_END() { return 2; }
|
|
static get END_TO_START() { return 3; }
|
|
};
|
|
Object.assign(globalThis.Range.prototype, { START_TO_START: 0, START_TO_END: 1, END_TO_END: 2, END_TO_START: 3 });
|
|
globalThis.StaticRange = class StaticRange {
|
|
constructor(init) {
|
|
if (!init || init.startContainer == null || init.endContainer == null)
|
|
throw new TypeError("Failed to construct 'StaticRange': required members are undefined");
|
|
const sc = init.startContainer, ec = init.endContainer;
|
|
if (sc.nodeType === 10 || ec.nodeType === 10 || sc.nodeType === 7 || ec.nodeType === 7)
|
|
throw new DOMException("StaticRange endpoints cannot be DocumentType or ProcessingInstruction", "InvalidNodeTypeError");
|
|
this._sc = sc; this._so = init.startOffset >>> 0; this._ec = ec; this._eo = init.endOffset >>> 0;
|
|
}
|
|
get startContainer() { return this._sc; }
|
|
get startOffset() { return this._so; }
|
|
get endContainer() { return this._ec; }
|
|
get endOffset() { return this._eo; }
|
|
get collapsed() { return _rngSame(this._sc, this._ec) && this._so === this._eo; }
|
|
};
|
|
// Live Selection over the real Range: at most one range + a direction, one
|
|
// instance per document. Everything except modify() (needs visual line/word
|
|
// layout) is layout-free, built on the Range boundary-point helpers above.
|
|
globalThis.Selection = class Selection {
|
|
constructor(doc) { this._doc = doc; this._range = null; this._direction = 'none'; }
|
|
_setRange(r, dir) { this._range = r; this._direction = dir; }
|
|
_inDoc(node) { return !!(node && this._doc && this._doc.contains && this._doc.contains(node)); }
|
|
get rangeCount() { return this._range ? 1 : 0; }
|
|
get isCollapsed() { return !this._range || this._range.collapsed; }
|
|
get type() { return !this._range ? 'None' : (this._range.collapsed ? 'Caret' : 'Range'); }
|
|
get _anchor() { const r = this._range; if (!r) return null; return this._direction === 'backwards' ? [r.endContainer, r.endOffset] : [r.startContainer, r.startOffset]; }
|
|
get _focus() { const r = this._range; if (!r) return null; return this._direction === 'backwards' ? [r.startContainer, r.startOffset] : [r.endContainer, r.endOffset]; }
|
|
get anchorNode() { return this._anchor ? this._anchor[0] : null; }
|
|
get anchorOffset() { return this._anchor ? this._anchor[1] : 0; }
|
|
get focusNode() { return this._focus ? this._focus[0] : null; }
|
|
get focusOffset() { return this._focus ? this._focus[1] : 0; }
|
|
getRangeAt(i) { i = +i; if (!this._range || i < 0 || i > 0) throw new DOMException('The index provided is out of range.', 'IndexSizeError'); return this._range; }
|
|
addRange(range) { if (this._range) return; if (!(range instanceof Range)) return; if (!this._inDoc(range.startContainer) || !this._inDoc(range.endContainer)) return; this._setRange(range, 'forwards'); }
|
|
removeRange(range) { if (!(range instanceof Range)) throw new TypeError("Failed to execute 'removeRange' on 'Selection': parameter 1 is not a Range."); if (this._range === range) this._setRange(null, 'none'); else throw new DOMException('The range was not found.', 'NotFoundError'); }
|
|
removeAllRanges() { this._setRange(null, 'none'); }
|
|
empty() { this.removeAllRanges(); }
|
|
collapse(node, offset) { if (node == null) { this.removeAllRanges(); return; } offset = offset >>> 0; _rngCheckOffset(node, offset); if (!this._inDoc(node)) return; const r = new Range(); r.setStart(node, offset); r.setEnd(node, offset); this._setRange(r, 'forwards'); }
|
|
setPosition(node, offset) { this.collapse(node, offset); }
|
|
collapseToStart() { if (!this._range) throw new DOMException('There is no selection to collapse.', 'InvalidStateError'); const r = new Range(); r.setStart(this._range.startContainer, this._range.startOffset); r.setEnd(this._range.startContainer, this._range.startOffset); this._setRange(r, 'forwards'); }
|
|
collapseToEnd() { if (!this._range) throw new DOMException('There is no selection to collapse.', 'InvalidStateError'); const r = new Range(); r.setStart(this._range.endContainer, this._range.endOffset); r.setEnd(this._range.endContainer, this._range.endOffset); this._setRange(r, 'forwards'); }
|
|
extend(node, offset) { if (!this._range) throw new DOMException('There is no selection to extend.', 'InvalidStateError'); if (!this._inDoc(node)) return; offset = offset >>> 0; _rngCheckOffset(node, offset); const a = this._anchor; const r = new Range(); if (_rngRoot(node)._nid !== _rngRoot(a[0])._nid) { r.setStart(node, offset); r.setEnd(node, offset); this._setRange(r, 'forwards'); return; } if (_rngCmp(a[0], a[1], node, offset) <= 0) { r.setStart(a[0], a[1]); r.setEnd(node, offset); this._setRange(r, 'forwards'); } else { r.setStart(node, offset); r.setEnd(a[0], a[1]); this._setRange(r, 'backwards'); } }
|
|
setBaseAndExtent(aN, aO, fN, fO) { if (arguments.length < 4) throw new TypeError("Failed to execute 'setBaseAndExtent' on 'Selection': 4 arguments required."); if (aN == null || fN == null) throw new TypeError("Failed to execute 'setBaseAndExtent' on 'Selection': nodes must not be null."); aO = +aO; fO = +fO; if (aO < 0 || aO > _rngNodeLength(aN)) throw new DOMException('anchor offset out of range', 'IndexSizeError'); if (fO < 0 || fO > _rngNodeLength(fN)) throw new DOMException('focus offset out of range', 'IndexSizeError'); if (!this._inDoc(aN) || !this._inDoc(fN)) { this.removeAllRanges(); return; } const r = new Range(); if (_rngCmp(aN, aO, fN, fO) <= 0) { r.setStart(aN, aO); r.setEnd(fN, fO); this._setRange(r, 'forwards'); } else { r.setStart(fN, fO); r.setEnd(aN, aO); this._setRange(r, 'backwards'); } }
|
|
selectAllChildren(node) { if (node && node.nodeType === 10) throw new DOMException('cannot selectAllChildren of a DocumentType', 'InvalidNodeTypeError'); if (!this._inDoc(node)) return; const len = _rngNodeLength(node); const r = new Range(); r.setStart(node, 0); r.setEnd(node, len); this._setRange(r, 'forwards'); }
|
|
containsNode(node, allowPartial) { const r = this._range; if (!r || !node) return false; if (_rngRoot(node)._nid !== _rngRoot(r.startContainer)._nid) return false; const len = _rngNodeLength(node); if (allowPartial) return _rngCmp(node, len, r.startContainer, r.startOffset) > 0 && _rngCmp(node, 0, r.endContainer, r.endOffset) < 0; return _rngCmp(node, 0, r.startContainer, r.startOffset) >= 0 && _rngCmp(node, len, r.endContainer, r.endOffset) <= 0; }
|
|
deleteFromDocument() { if (this._range) this._range.deleteContents(); }
|
|
toString() { return this._range ? this._range.toString() : ''; }
|
|
modify() {}
|
|
};
|
|
_markNative(globalThis.Selection);
|
|
|
|
[
|
|
navigator.getBattery, navigator.getGamepads, navigator.sendBeacon,
|
|
navigator.javaEnabled, navigator.geolocation?.getCurrentPosition,
|
|
navigator.geolocation?.watchPosition,
|
|
navigator.serviceWorker?.register,
|
|
navigator.permissions?.query, navigator.credentials?.get,
|
|
navigator.storage?.estimate, navigator.storage?.persist, navigator.storage?.persisted,
|
|
globalThis.fetch, globalThis.matchMedia, globalThis.getComputedStyle,
|
|
globalThis.getSelection, globalThis.requestAnimationFrame,
|
|
globalThis.cancelAnimationFrame, globalThis.setTimeout, globalThis.clearTimeout,
|
|
globalThis.setInterval, globalThis.clearInterval, globalThis.queueMicrotask,
|
|
globalThis.structuredClone, globalThis.reportError,
|
|
globalThis.btoa, globalThis.atob,
|
|
console.log, console.warn, console.error, console.info, console.debug,
|
|
console.dir, console.assert,
|
|
Element.prototype.getAttribute, Element.prototype.setAttribute,
|
|
Element.prototype.removeAttribute, Element.prototype.hasAttribute,
|
|
Element.prototype.querySelector, Element.prototype.querySelectorAll,
|
|
Element.prototype.getElementsByTagName, Element.prototype.getElementsByClassName,
|
|
Element.prototype.matches, Element.prototype.closest,
|
|
Element.prototype.getBoundingClientRect, Element.prototype.getClientRects,
|
|
Element.prototype.checkVisibility,
|
|
Element.prototype.addEventListener, Element.prototype.removeEventListener,
|
|
Element.prototype.dispatchEvent, Element.prototype.click,
|
|
Element.prototype.focus, Element.prototype.blur,
|
|
Element.prototype.showPopover, Element.prototype.hidePopover, Element.prototype.togglePopover,
|
|
Element.prototype.cloneNode, Element.prototype.attachShadow,
|
|
Element.prototype.insertAdjacentHTML, Element.prototype.insertAdjacentText,
|
|
Element.prototype.insertAdjacentElement, Element.prototype.scrollIntoView,
|
|
Element.prototype.scrollTo, Element.prototype.scrollBy, Element.prototype.scroll,
|
|
Element.prototype.append, Element.prototype.prepend, Element.prototype.remove,
|
|
Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith,
|
|
HTMLFormElement.prototype.reset,
|
|
Element.prototype.getContext, Element.prototype.toDataURL, Element.prototype.toBlob,
|
|
Element.prototype.getBBox,
|
|
Node.prototype.appendChild, Node.prototype.removeChild,
|
|
Node.prototype.replaceChild, Node.prototype.insertBefore,
|
|
Node.prototype.contains, Node.prototype.hasChildNodes, Node.prototype.cloneNode,
|
|
CharacterData.prototype.before, CharacterData.prototype.after,
|
|
CharacterData.prototype.replaceWith, CharacterData.prototype.remove,
|
|
Document.prototype.getElementById, Document.prototype.querySelector,
|
|
Document.prototype.querySelectorAll, Document.prototype.getElementsByTagName,
|
|
Document.prototype.createElement, Document.prototype.createElementNS,
|
|
Document.prototype.createTextNode, Document.prototype.createComment,
|
|
Document.prototype.createCDATASection, Document.prototype.createProcessingInstruction,
|
|
Document.prototype.createDocumentFragment, Document.prototype.createEvent,
|
|
Document.prototype.hasFocus,
|
|
Storage, Storage.prototype.getItem, Storage.prototype.setItem,
|
|
Storage.prototype.removeItem, Storage.prototype.clear, Storage.prototype.key,
|
|
Notification, Notification.requestPermission,
|
|
window.chrome?.csi, window.chrome?.loadTimes,
|
|
MutationObserver, ResizeObserver, IntersectionObserver, PerformanceObserver,
|
|
XMLSerializer, XMLSerializer.prototype.serializeToString,
|
|
].forEach(fn => { if (typeof fn === 'function') _markNative(fn); });
|
|
|
|
class _IframeDocument {
|
|
constructor(html, url, iframeEl) {
|
|
this._url = url;
|
|
this._iframeEl = iframeEl;
|
|
this.nodeType = 9;
|
|
this.nodeName = '#document';
|
|
this.readyState = 'complete';
|
|
this.characterSet = 'UTF-8';
|
|
this.contentType = 'text/html';
|
|
this.visibilityState = 'visible';
|
|
this.hidden = false;
|
|
|
|
this._root = document.createElement('html');
|
|
this._head = document.createElement('head');
|
|
this._body = document.createElement('body');
|
|
this._root.appendChild(this._head);
|
|
this._root.appendChild(this._body);
|
|
var bodyContent = html
|
|
.replace(/^<!DOCTYPE[^>]*>/i, '')
|
|
.replace(/<\/?html[^>]*>/gi, '')
|
|
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
.replace(/<\/?body[^>]*>/gi, '')
|
|
.replace(/^\s+/, ''); // trim leading whitespace (before <body> content)
|
|
if (bodyContent) {
|
|
this._body.innerHTML = bodyContent;
|
|
}
|
|
|
|
this._title = '';
|
|
if (this._head) {
|
|
const titleEl = this._head.querySelector('title');
|
|
if (titleEl) this._title = titleEl.textContent;
|
|
}
|
|
}
|
|
|
|
get documentElement() { return this._root; }
|
|
get head() { return this._head; }
|
|
get body() { return this._body; }
|
|
get title() { return this._title; }
|
|
set title(v) { this._title = v; }
|
|
get URL() { return this._url; }
|
|
get documentURI() { return this._url; }
|
|
get location() { return this._iframeEl?.contentWindow?.location; }
|
|
get defaultView() { return this._iframeEl?.contentWindow; }
|
|
get ownerDocument() { return null; }
|
|
get compatMode() { return 'CSS1Compat'; }
|
|
get activeElement() { return this._body; }
|
|
|
|
getElementById(id) {
|
|
return this._root.querySelector('#' + id);
|
|
}
|
|
querySelector(sel) {
|
|
return this._root.querySelector(sel);
|
|
}
|
|
querySelectorAll(sel) {
|
|
return this._root.querySelectorAll(sel);
|
|
}
|
|
getElementsByTagName(tag) {
|
|
return this._root.querySelectorAll(tag);
|
|
}
|
|
getElementsByClassName(cls) {
|
|
return _getElementsByClassName(this._root, cls);
|
|
}
|
|
createElement(tag) { return document.createElement(tag); }
|
|
createElementNS(ns, tag) { return document.createElementNS(ns, tag); }
|
|
createTextNode(text) { return document.createTextNode(text); }
|
|
createComment(text) { return document.createComment(text); }
|
|
createDocumentFragment() { return document.createDocumentFragment(); }
|
|
createEvent(type) { return document.createEvent(type); }
|
|
createRange() { return new Range(); }
|
|
hasFocus() { return false; }
|
|
|
|
get cookie() { return ''; }
|
|
set cookie(v) {}
|
|
get implementation() { return document.implementation; }
|
|
get styleSheets() { return []; }
|
|
|
|
addEventListener(type, listener) {
|
|
if (typeof listener !== 'function') return;
|
|
if (!this._listeners) this._listeners = Object.create(null);
|
|
const list = this._listeners[type] || (this._listeners[type] = []);
|
|
if (!list.includes(listener)) list.push(listener);
|
|
}
|
|
removeEventListener(type, listener) {
|
|
const list = this._listeners && this._listeners[type];
|
|
if (!list) return;
|
|
const index = list.indexOf(listener);
|
|
if (index !== -1) list.splice(index, 1);
|
|
}
|
|
dispatchEvent(event) {
|
|
const type = event && event.type;
|
|
if (!type) return true;
|
|
const list = this._listeners && this._listeners[type];
|
|
if (list) {
|
|
for (const listener of list.slice()) {
|
|
try { listener.call(this, event); } catch (error) { console.error(error); }
|
|
}
|
|
}
|
|
const handler = this['on' + type];
|
|
if (typeof handler === 'function') {
|
|
try { handler.call(this, event); } catch (error) { console.error(error); }
|
|
}
|
|
return !event.defaultPrevented;
|
|
}
|
|
|
|
write(html) {
|
|
if (this._body) this._body.innerHTML += html;
|
|
}
|
|
writeln(html) { this.write(html + '\n'); }
|
|
open() { if (this._body) this._body.innerHTML = ''; }
|
|
close() {}
|
|
}
|
|
|
|
class _IframeWindow {
|
|
constructor(doc, url) {
|
|
this.document = doc;
|
|
this._url = url;
|
|
this.self = this;
|
|
this.top = globalThis;
|
|
this.parent = globalThis;
|
|
this.window = this;
|
|
this.frames = this;
|
|
this.frameElement = null;
|
|
this.length = 0;
|
|
this.name = '';
|
|
this.closed = false;
|
|
this.navigator = globalThis.navigator;
|
|
this.screen = globalThis.screen;
|
|
this.innerWidth = 300;
|
|
this.innerHeight = 150;
|
|
this.outerWidth = 300;
|
|
this.outerHeight = 150;
|
|
this.devicePixelRatio = globalThis.devicePixelRatio;
|
|
this.localStorage = globalThis.localStorage;
|
|
this.sessionStorage = globalThis.sessionStorage;
|
|
this.performance = globalThis.performance;
|
|
this.crypto = globalThis.crypto;
|
|
this.console = globalThis.console;
|
|
this.chrome = globalThis.chrome;
|
|
|
|
try {
|
|
const u = new URL(url);
|
|
this.location = {
|
|
href: url, origin: u.origin, protocol: u.protocol,
|
|
host: u.host, hostname: u.hostname, port: u.port,
|
|
pathname: u.pathname, search: u.search, hash: u.hash,
|
|
toString() { return url; }, assign(){}, reload(){}, replace(){},
|
|
};
|
|
} catch(e) {
|
|
this.location = { href: url, origin: '', protocol: '', host: '', hostname: '', port: '', pathname: '/', search: '', hash: '', toString() { return url; }, assign(){}, reload(){}, replace(){} };
|
|
}
|
|
}
|
|
|
|
postMessage(data, origin) {
|
|
const event = new MessageEvent('message', {
|
|
data: data,
|
|
origin: this.location.origin,
|
|
source: this,
|
|
});
|
|
Promise.resolve().then(() => {
|
|
globalThis.dispatchEvent?.(event);
|
|
});
|
|
}
|
|
|
|
setTimeout(fn, ms) { return globalThis.setTimeout(fn, ms); }
|
|
clearTimeout(id) { globalThis.clearTimeout(id); }
|
|
setInterval(fn, ms) { return globalThis.setInterval(fn, ms); }
|
|
clearInterval(id) { globalThis.clearInterval(id); }
|
|
requestAnimationFrame(fn) { return globalThis.requestAnimationFrame(fn); }
|
|
|
|
addEventListener(type, fn) {
|
|
if (!this._listeners) this._listeners = {};
|
|
if (!this._listeners[type]) this._listeners[type] = [];
|
|
this._listeners[type].push(fn);
|
|
}
|
|
removeEventListener(type, fn) {
|
|
if (this._listeners?.[type]) {
|
|
this._listeners[type] = this._listeners[type].filter(h => h !== fn);
|
|
}
|
|
}
|
|
dispatchEvent(event) {
|
|
const handlers = this._listeners?.[event?.type] || [];
|
|
for (const h of handlers) { try { h.call(this, event); } catch(e) {} }
|
|
return true;
|
|
}
|
|
|
|
getComputedStyle(el) { return globalThis.getComputedStyle(el); }
|
|
matchMedia(q) { return globalThis.matchMedia(q); }
|
|
getSelection() { return globalThis.getSelection(); }
|
|
fetch(input, init) { return globalThis.fetch(input, init); }
|
|
close() { this.closed = true; }
|
|
focus() {}
|
|
blur() {}
|
|
}
|
|
|
|
// Encode an RGBA pixel buffer into a valid PNG data URL.
|
|
// Uses stored-block DEFLATE (no compression) wrapped in zlib.
|
|
// This produces a larger file than a real browser but the hash is unique
|
|
// per session (from _fpNoise) and valid, so it does not match the known
|
|
// headless stub.
|
|
function _encodePNG(w, h, rgba) {
|
|
// RGB scanlines: filter byte (0) + 3 bytes per pixel
|
|
var rowLen = 1 + w * 3;
|
|
var raw = new Uint8Array(h * rowLen);
|
|
for (var y = 0; y < h; y++) {
|
|
var base = y * rowLen;
|
|
raw[base] = 0;
|
|
for (var x = 0; x < w; x++) {
|
|
var s = (y * w + x) << 2, d = base + 1 + x * 3;
|
|
raw[d] = rgba[s]; raw[d+1] = rgba[s+1]; raw[d+2] = rgba[s+2];
|
|
}
|
|
}
|
|
// Adler32 of raw
|
|
var s1 = 1, s2 = 0, M = 65521;
|
|
for (var i = 0; i < raw.length; i++) { s1 = (s1 + raw[i]) % M; s2 = (s2 + s1) % M; }
|
|
var adler = ((s2 << 16) | s1) >>> 0;
|
|
// Stored DEFLATE blocks (zlib level 0)
|
|
var MAXB = 65535, nb = Math.ceil(raw.length / MAXB) || 1;
|
|
var dlen = 2 + nb * 5 + raw.length + 4;
|
|
var def = new Uint8Array(dlen), dp = 0;
|
|
def[dp++] = 0x78; def[dp++] = 0x01;
|
|
for (var bi = 0; bi < nb; bi++) {
|
|
var bs = bi * MAXB, be = Math.min(raw.length, bs + MAXB), bl = be - bs;
|
|
def[dp++] = bi === nb-1 ? 1 : 0;
|
|
def[dp++] = bl&0xff; def[dp++] = (bl>>8)&0xff;
|
|
def[dp++] = (~bl)&0xff; def[dp++] = (~bl>>8)&0xff;
|
|
def.set(raw.subarray(bs, be), dp); dp += bl;
|
|
}
|
|
def[dp++]=(adler>>24)&0xff; def[dp++]=(adler>>16)&0xff; def[dp++]=(adler>>8)&0xff; def[dp]=adler&0xff;
|
|
// CRC32 (lazy table)
|
|
if (!_encodePNG._t) {
|
|
var t = new Uint32Array(256);
|
|
for (var n = 0; n < 256; n++) { var c = n; for (var k=0;k<8;k++) c=c&1?0xEDB88320^(c>>>1):(c>>>1); t[n]=c; }
|
|
_encodePNG._t = t;
|
|
}
|
|
var T = _encodePNG._t;
|
|
function crc32(a, st, ln) { var c=0xFFFFFFFF; for(var i=st,e=st+ln;i<e;i++) c=T[(c^a[i])&0xff]^(c>>>8); return (c^0xFFFFFFFF)>>>0; }
|
|
function putChunk(out, off, type, data) {
|
|
var dl = data.length;
|
|
out[off]=(dl>>24)&0xff; out[off+1]=(dl>>16)&0xff; out[off+2]=(dl>>8)&0xff; out[off+3]=dl&0xff;
|
|
out[off+4]=type.charCodeAt(0); out[off+5]=type.charCodeAt(1); out[off+6]=type.charCodeAt(2); out[off+7]=type.charCodeAt(3);
|
|
out.set(data, off+8);
|
|
var cr = crc32(out, off+4, 4+dl);
|
|
out[off+8+dl]=(cr>>24)&0xff; out[off+9+dl]=(cr>>16)&0xff; out[off+10+dl]=(cr>>8)&0xff; out[off+11+dl]=cr&0xff;
|
|
return off+12+dl;
|
|
}
|
|
var ihd = new Uint8Array(13);
|
|
ihd[0]=(w>>24)&0xff; ihd[1]=(w>>16)&0xff; ihd[2]=(w>>8)&0xff; ihd[3]=w&0xff;
|
|
ihd[4]=(h>>24)&0xff; ihd[5]=(h>>16)&0xff; ihd[6]=(h>>8)&0xff; ihd[7]=h&0xff;
|
|
ihd[8]=8; ihd[9]=2; // 8-bit RGB
|
|
var png = new Uint8Array(8 + 25 + (12+dlen) + 12);
|
|
png.set([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]);
|
|
var p = 8;
|
|
p = putChunk(png, p, 'IHDR', ihd);
|
|
p = putChunk(png, p, 'IDAT', def);
|
|
putChunk(png, p, 'IEND', new Uint8Array(0));
|
|
// Base64 encode
|
|
var C = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
var b64 = 'data:image/png;base64,';
|
|
for (var i = 0; i < png.length; i += 3) {
|
|
var a=png[i], b=i+1<png.length?png[i+1]:0, c=i+2<png.length?png[i+2]:0;
|
|
b64 += C[a>>2] + C[((a&3)<<4)|(b>>4)] + (i+1<png.length?C[((b&15)<<2)|(c>>6)]:'=') + (i+2<png.length?C[c&63]:'=');
|
|
}
|
|
return b64;
|
|
}
|
|
|
|
globalThis.__ariaQuerySelector = function(root, selector) { return null; };
|
|
globalThis.__ariaQuerySelectorAll = async function*(root, selector) { /* yields nothing */ };
|
|
class _Canvas2D {
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
this._w = parseInt(canvas.getAttribute('width')) || 300;
|
|
this._h = parseInt(canvas.getAttribute('height')) || 150;
|
|
this._buf = new Uint8ClampedArray(this._w * this._h * 4);
|
|
for (let i = 0; i < this._w * this._h; i++) {
|
|
this._buf[i*4+0] = 255 + Math.floor(_fpNoise(i % this._w, Math.floor(i / this._w), 0));
|
|
this._buf[i*4+1] = 255 + Math.floor(_fpNoise(i % this._w, Math.floor(i / this._w), 1));
|
|
this._buf[i*4+2] = 255 + Math.floor(_fpNoise(i % this._w, Math.floor(i / this._w), 2));
|
|
this._buf[i*4+3] = 255;
|
|
}
|
|
this.fillStyle = '#000000';
|
|
this.strokeStyle = '#000000';
|
|
this.lineWidth = 1;
|
|
this.font = '10px sans-serif';
|
|
this.textAlign = 'start';
|
|
this.textBaseline = 'alphabetic';
|
|
this.globalAlpha = 1;
|
|
this.globalCompositeOperation = 'source-over';
|
|
this._stateStack = [];
|
|
}
|
|
_parseColor(css) {
|
|
if (!css || typeof css !== 'string' || css === 'none') return [0,0,0,0];
|
|
if (css.startsWith('#')) {
|
|
const hex = css.slice(1);
|
|
if (hex.length === 3) return [parseInt(hex[0]+hex[0],16),parseInt(hex[1]+hex[1],16),parseInt(hex[2]+hex[2],16),255];
|
|
if (hex.length === 6) return [parseInt(hex.slice(0,2),16),parseInt(hex.slice(2,4),16),parseInt(hex.slice(4,6),16),255];
|
|
if (hex.length === 8) return [parseInt(hex.slice(0,2),16),parseInt(hex.slice(2,4),16),parseInt(hex.slice(4,6),16),parseInt(hex.slice(6,8),16)];
|
|
}
|
|
const m = css.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
if (m) return [+m[1],+m[2],+m[3],m[4]!==undefined?Math.round(+m[4]*255):255];
|
|
const named = {red:[255,0,0,255],green:[0,128,0,255],blue:[0,0,255,255],white:[255,255,255,255],black:[0,0,0,255],yellow:[255,255,0,255],orange:[255,165,0,255],gray:[128,128,128,255],transparent:[0,0,0,0]};
|
|
return named[css] || [0,0,0,255];
|
|
}
|
|
_setPixel(x, y, r, g, b, a) {
|
|
x = Math.round(x); y = Math.round(y);
|
|
if (x < 0 || x >= this._w || y < 0 || y >= this._h) return;
|
|
const idx = (y * this._w + x) * 4;
|
|
const alpha = (a / 255) * this.globalAlpha;
|
|
if (this.globalCompositeOperation === 'multiply') {
|
|
this._buf[idx+0] = Math.round((r/255) * (this._buf[idx+0]/255) * 255);
|
|
this._buf[idx+1] = Math.round((g/255) * (this._buf[idx+1]/255) * 255);
|
|
this._buf[idx+2] = Math.round((b/255) * (this._buf[idx+2]/255) * 255);
|
|
this._buf[idx+3] = Math.min(255, this._buf[idx+3] + Math.round(a * alpha));
|
|
} else {
|
|
this._buf[idx+0] = Math.round(r * alpha + this._buf[idx+0] * (1 - alpha));
|
|
this._buf[idx+1] = Math.round(g * alpha + this._buf[idx+1] * (1 - alpha));
|
|
this._buf[idx+2] = Math.round(b * alpha + this._buf[idx+2] * (1 - alpha));
|
|
this._buf[idx+3] = Math.min(255, Math.round(a * alpha + this._buf[idx+3] * (1 - alpha)));
|
|
}
|
|
}
|
|
fillRect(x, y, w, h) {
|
|
const [r,g,b,a] = this._parseColor(this.fillStyle);
|
|
x=Math.round(x); y=Math.round(y); w=Math.round(w); h=Math.round(h);
|
|
for (let py = Math.max(0,y); py < Math.min(this._h, y+h); py++) {
|
|
for (let px = Math.max(0,x); px < Math.min(this._w, x+w); px++) {
|
|
this._setPixel(px, py, r, g, b, a);
|
|
}
|
|
}
|
|
}
|
|
clearRect(x, y, w, h) {
|
|
x=Math.round(x); y=Math.round(y); w=Math.round(w); h=Math.round(h);
|
|
for (let py = Math.max(0,y); py < Math.min(this._h, y+h); py++) {
|
|
for (let px = Math.max(0,x); px < Math.min(this._w, x+w); px++) {
|
|
const idx = (py * this._w + px) * 4;
|
|
this._buf[idx] = this._buf[idx+1] = this._buf[idx+2] = this._buf[idx+3] = 0;
|
|
}
|
|
}
|
|
}
|
|
strokeRect(x, y, w, h) {
|
|
const [r,g,b,a] = this._parseColor(this.strokeStyle);
|
|
const lw = this.lineWidth;
|
|
for (let px = Math.round(x); px < Math.round(x+w); px++) {
|
|
for (let l = 0; l < lw; l++) { this._setPixel(px, Math.round(y)+l, r,g,b,a); this._setPixel(px, Math.round(y+h)-1-l, r,g,b,a); }
|
|
}
|
|
for (let py = Math.round(y); py < Math.round(y+h); py++) {
|
|
for (let l = 0; l < lw; l++) { this._setPixel(Math.round(x)+l, py, r,g,b,a); this._setPixel(Math.round(x+w)-1-l, py, r,g,b,a); }
|
|
}
|
|
}
|
|
fillText(text, x, y) {
|
|
const [r,g,b,a] = this._parseColor(this.fillStyle);
|
|
const fontSize = parseInt(this.font) || 10;
|
|
const scale = Math.max(1, Math.round(fontSize / 10));
|
|
const str = String(text);
|
|
let cx = Math.round(x);
|
|
for (let i = 0; i < str.length; i++) {
|
|
const code = str.charCodeAt(i);
|
|
for (let row = 0; row < 7; row++) {
|
|
for (let col = 0; col < 5; col++) {
|
|
const on = ((_fpRand(code * 100 + row * 10 + col) > 0.45) &&
|
|
(row > 0 && row < 6 && col > 0 && col < 4)) ||
|
|
(_fpRand(code * 200 + row * 7 + col) > 0.7);
|
|
if (on) {
|
|
for (let sy = 0; sy < scale; sy++) {
|
|
for (let sx = 0; sx < scale; sx++) {
|
|
this._setPixel(cx + col*scale + sx, Math.round(y) - 7*scale + row*scale + sy, r, g, b, a);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cx += 6 * scale;
|
|
}
|
|
}
|
|
strokeText(text, x, y) { this.fillText(text, x, y); }
|
|
measureText(t) {
|
|
const fontSize = parseInt(this.font) || 10;
|
|
const scale = Math.max(1, Math.round(fontSize / 10));
|
|
return { width: String(t).length * 6 * scale, actualBoundingBoxAscent: 7*scale, actualBoundingBoxDescent: 2*scale };
|
|
}
|
|
getImageData(x, y, w, h) {
|
|
x=Math.round(x); y=Math.round(y); w=Math.round(w); h=Math.round(h);
|
|
const data = new Uint8ClampedArray(w * h * 4);
|
|
for (let py = 0; py < h; py++) {
|
|
for (let px = 0; px < w; px++) {
|
|
const srcX = x + px, srcY = y + py;
|
|
const dstIdx = (py * w + px) * 4;
|
|
if (srcX >= 0 && srcX < this._w && srcY >= 0 && srcY < this._h) {
|
|
const srcIdx = (srcY * this._w + srcX) * 4;
|
|
data[dstIdx] = this._buf[srcIdx];
|
|
data[dstIdx+1] = this._buf[srcIdx+1];
|
|
data[dstIdx+2] = this._buf[srcIdx+2];
|
|
data[dstIdx+3] = this._buf[srcIdx+3];
|
|
}
|
|
}
|
|
}
|
|
return { data, width: w, height: h };
|
|
}
|
|
putImageData(imageData, dx, dy) {
|
|
dx=Math.round(dx); dy=Math.round(dy);
|
|
const {data, width: w, height: h} = imageData;
|
|
for (let py = 0; py < h; py++) {
|
|
for (let px = 0; px < w; px++) {
|
|
const srcIdx = (py * w + px) * 4;
|
|
const x = dx + px, y = dy + py;
|
|
if (x >= 0 && x < this._w && y >= 0 && y < this._h) {
|
|
const dstIdx = (y * this._w + x) * 4;
|
|
this._buf[dstIdx] = data[srcIdx];
|
|
this._buf[dstIdx+1] = data[srcIdx+1];
|
|
this._buf[dstIdx+2] = data[srcIdx+2];
|
|
this._buf[dstIdx+3] = data[srcIdx+3];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
createImageData(w, h) { return { data: new Uint8ClampedArray(w*h*4), width: w, height: h }; }
|
|
drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh) {
|
|
if (img && img._ctx && img._ctx._buf) {
|
|
const src = img._ctx;
|
|
dx = dx ?? sx; dy = dy ?? sy; dw = dw ?? (sw ?? src._w); dh = dh ?? (sh ?? src._h);
|
|
for (let py = 0; py < dh; py++) {
|
|
for (let px = 0; px < dw; px++) {
|
|
const srcX = Math.floor((sx||0) + px * (sw||src._w) / dw);
|
|
const srcY = Math.floor((sy||0) + py * (sh||src._h) / dh);
|
|
if (srcX >= 0 && srcX < src._w && srcY >= 0 && srcY < src._h) {
|
|
const srcIdx = (srcY * src._w + srcX) * 4;
|
|
this._setPixel(dx+px, dy+py, src._buf[srcIdx], src._buf[srcIdx+1], src._buf[srcIdx+2], src._buf[srcIdx+3]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
beginPath() { this._path = []; }
|
|
closePath() {}
|
|
moveTo(x, y) { if (this._path) this._path.push({t:'M',x,y}); }
|
|
lineTo(x, y) { if (this._path) this._path.push({t:'L',x,y}); }
|
|
bezierCurveTo() {} quadraticCurveTo() {}
|
|
arc(x, y, r, s, e) { if (this._path) this._path.push({t:'A',x,y,r}); }
|
|
arcTo() {}
|
|
rect(x, y, w, h) { this.fillRect(x, y, w, h); }
|
|
fill() {
|
|
if (!this._path) return;
|
|
const [r,g,b,a] = this._parseColor(this.fillStyle);
|
|
for (const seg of this._path) {
|
|
if (seg.t === 'A') {
|
|
const cx = Math.round(seg.x), cy = Math.round(seg.y), rad = seg.r;
|
|
const r2 = rad * rad;
|
|
for (let py = Math.max(0, cy - rad); py <= Math.min(this._h - 1, cy + rad); py++) {
|
|
for (let px = Math.max(0, cx - rad); px <= Math.min(this._w - 1, cx + rad); px++) {
|
|
if ((px-cx)*(px-cx) + (py-cy)*(py-cy) <= r2) this._setPixel(px, py, r, g, b, a);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this._path = [];
|
|
}
|
|
stroke() {}
|
|
clip() {}
|
|
save() { this._stateStack.push({fillStyle: this.fillStyle, strokeStyle: this.strokeStyle, globalAlpha: this.globalAlpha, font: this.font, lineWidth: this.lineWidth}); }
|
|
restore() { const s = this._stateStack.pop(); if (s) Object.assign(this, s); }
|
|
translate() {} rotate() {} scale() {}
|
|
setTransform() {} resetTransform() {} transform() {}
|
|
createLinearGradient(x0,y0,x1,y1) { return { addColorStop(){}, _x0:x0,_y0:y0,_x1:x1,_y1:y1 }; }
|
|
createRadialGradient() { return { addColorStop(){} }; }
|
|
createPattern() { return {}; }
|
|
isPointInPath() { return false; }
|
|
isPointInStroke() { return false; }
|
|
// Line-dash plus a few path/style methods that charting libraries (Highcharts,
|
|
// ECharts) call on every animation frame. A missing setLineDash threw
|
|
// "is not a function" from a timer each tick, spamming errors (#258).
|
|
setLineDash() {}
|
|
getLineDash() { return []; }
|
|
ellipse() {}
|
|
roundRect() {}
|
|
createConicGradient() { return { addColorStop(){} }; }
|
|
getContextAttributes() { return { alpha: true, desynchronized: false, colorSpace: "srgb", willReadFrequently: false }; }
|
|
}
|
|
|
|
Element.prototype.getContext = function getContext(type) {
|
|
if (type === '2d') {
|
|
if (!this._ctx) {
|
|
this._ctx = new _Canvas2D(this);
|
|
}
|
|
return this._ctx;
|
|
}
|
|
if (type === 'webgl' || type === 'experimental-webgl' || type === 'webgl2') {
|
|
return {
|
|
canvas: this,
|
|
MAX_VIEWPORT_DIMS: 0x0D33,
|
|
MAX_TEXTURE_SIZE: 0x0D33,
|
|
MAX_RENDERBUFFER_SIZE: 0x84E8,
|
|
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84EA,
|
|
MAX_DRAW_BUFFERS_WEBGL: 0x8824,
|
|
getContextAttributes() { return { alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: false, powerPreference: "default", premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: true, desynchronized: false }; },
|
|
uniform2f() {},
|
|
getExtension(name) {
|
|
if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };
|
|
return null;
|
|
},
|
|
getParameter(pname) {
|
|
if (pname === 0x9245) return _fp('gpuVendor');
|
|
if (pname === 0x9246) return _fp('gpu');
|
|
if (pname === 0x1F01) return 'WebKit WebGL'; // GL_RENDERER
|
|
if (pname === 0x1F00) return 'WebKit'; // GL_VENDOR
|
|
if (pname === 0x1F02) return 'OpenGL ES 3.0 (ANGLE)'; // GL_VERSION
|
|
if (pname === 0x8B8C) return 'WebGL GLSL ES 3.00 (ANGLE)'; // GL_SHADING_LANGUAGE_VERSION
|
|
if (pname === undefined) return [0, 0];
|
|
// Some properties like MAX_VIEWPORT_DIMS return arrays
|
|
if (pname === 0x0D33) return [8192, 8192];
|
|
if (pname === 0x8A2A) return [8192, 8192];
|
|
return 0;
|
|
},
|
|
getSupportedExtensions() { return ['WEBGL_debug_renderer_info','EXT_texture_filter_anisotropic','WEBGL_compressed_texture_s3tc','WEBGL_lose_context']; },
|
|
getShaderPrecisionFormat() { return { rangeMin: 127, rangeMax: 127, precision: 23 }; },
|
|
createBuffer() { return {}; }, createShader() { return {}; }, createProgram() { return {}; },
|
|
shaderSource() {}, compileShader() {}, attachShader() {}, linkProgram() {},
|
|
getProgramParameter() { return true; }, useProgram() {}, deleteShader() {},
|
|
bindBuffer() {}, bufferData() {}, enableVertexAttribArray() {}, vertexAttribPointer() {},
|
|
drawArrays() {}, drawElements() {}, viewport() {}, clear() {}, clearColor() {},
|
|
enable() {}, disable() {}, blendFunc() {}, depthFunc() {},
|
|
getUniformLocation() { return {}; }, getAttribLocation() { return 0; },
|
|
uniform1f() {}, uniform1i() {}, uniformMatrix4fv() {},
|
|
createTexture() { return {}; }, bindTexture() {}, texImage2D() {}, texParameteri() {},
|
|
activeTexture() {}, pixelStorei() {}, generateMipmap() {},
|
|
createFramebuffer() { return {}; }, bindFramebuffer() {}, framebufferTexture2D() {},
|
|
readPixels(x,y,w,h,f,t,d) { if(d) for(let i=0;i<d.length;i++) d[i]=Math.floor(Math.random()*256); },
|
|
VERTEX_SHADER: 0x8B31, FRAGMENT_SHADER: 0x8B30, LINK_STATUS: 0x8B82,
|
|
ARRAY_BUFFER: 0x8892, STATIC_DRAW: 0x88E4, FLOAT: 0x1406,
|
|
TRIANGLES: 0x0004, COLOR_BUFFER_BIT: 0x4000, DEPTH_BUFFER_BIT: 0x100,
|
|
TEXTURE_2D: 0x0DE1, RGBA: 0x1908, UNSIGNED_BYTE: 0x1401,
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
Element.prototype.toDataURL = function(type) {
|
|
if (this._ctx && this._ctx._buf) {
|
|
const ctx = this._ctx;
|
|
return _encodePNG(ctx._w, ctx._h, ctx._buf);
|
|
}
|
|
return _fp('canvasFingerprint');
|
|
};
|
|
Element.prototype.toBlob = function(cb, type, q) { cb(new Blob([''])); };
|
|
Element.prototype.getBBox = function() { return { x: 0, y: 0, width: 0, height: 0 }; };
|
|
Element.prototype.getComputedTextLength = function() { return 0; };
|
|
Element.prototype.getExtentOfChar = function(ch) { return { x: 0, y: 0, width: 0, height: 0 }; };
|
|
Element.prototype.getSubStringLength = function(ch, len) { return 0; };
|
|
|
|
_markNative(Element.prototype.getContext);
|
|
_markNative(Element.prototype.toDataURL);
|
|
_markNative(Element.prototype.toBlob);
|
|
|
|
Element.prototype.attachShadow = function attachShadow(opts) {
|
|
var _mode = opts == null ? undefined : opts.mode;
|
|
if (_mode !== 'open' && _mode !== 'closed') {
|
|
throw new TypeError('Failed to execute attachShadow on Element: the mode value is not a valid ShadowRootMode.');
|
|
}
|
|
var _ln = (this.localName || '').toLowerCase();
|
|
if (!globalThis.__obscura_shadowHostNames.has(_ln) && _ln.indexOf('-') === -1) {
|
|
throw new DOMException('Failed to execute attachShadow on Element: this element does not support attachShadow', 'NotSupportedError');
|
|
}
|
|
if (this._shadowRoot) {
|
|
throw new DOMException('Failed to execute attachShadow on Element: the element already hosts a shadow tree.', 'NotSupportedError');
|
|
}
|
|
const host = this;
|
|
const children = [];
|
|
const shadow = {
|
|
mode: opts.mode,
|
|
host: host,
|
|
get innerHTML() { return children.map(c => c.outerHTML || c.textContent || '').join(''); },
|
|
set innerHTML(v) {
|
|
children.length = 0;
|
|
if (v) {
|
|
const tmp = document.createElement('div');
|
|
tmp.innerHTML = v;
|
|
for (let i = 0; i < tmp.childNodes.length; i++) children.push(tmp.childNodes[i]);
|
|
}
|
|
},
|
|
get childNodes() { return children; },
|
|
get firstChild() { return children[0] || null; },
|
|
get lastChild() { return children[children.length - 1] || null; },
|
|
get firstElementChild() { return children.find(c => c.nodeType === 1) || null; },
|
|
get children() { return children.filter(c => c.nodeType === 1); },
|
|
appendChild(c) {
|
|
if (c) {
|
|
children.push(c);
|
|
try { c.parentNode = shadow; } catch (_) { /* parentNode is getter-only on Node, ignore */ }
|
|
}
|
|
return c;
|
|
},
|
|
insertBefore(n, ref) {
|
|
if (!n) return n;
|
|
if (!ref) { shadow.appendChild(n); return n; }
|
|
const idx = children.indexOf(ref);
|
|
if (idx >= 0) {
|
|
children.splice(idx, 0, n);
|
|
try { n.parentNode = shadow; } catch (_) {}
|
|
}
|
|
else shadow.appendChild(n);
|
|
return n;
|
|
},
|
|
removeChild(c) { const idx = children.indexOf(c); if (idx >= 0) children.splice(idx, 1); return c; },
|
|
replaceChild(n, o) {
|
|
const idx = children.indexOf(o);
|
|
if (idx >= 0) {
|
|
children[idx] = n;
|
|
try { n.parentNode = shadow; } catch (_) {}
|
|
}
|
|
return o;
|
|
},
|
|
querySelector(s) {
|
|
for (const c of children) {
|
|
if (c.matches && c.matches(s)) return c;
|
|
if (c.querySelector) { const r = c.querySelector(s); if (r) return r; }
|
|
}
|
|
return null;
|
|
},
|
|
querySelectorAll(s) {
|
|
const results = [];
|
|
for (const c of children) {
|
|
if (c.matches && c.matches(s)) results.push(c);
|
|
if (c.querySelectorAll) results.push(...c.querySelectorAll(s));
|
|
}
|
|
return results;
|
|
},
|
|
getElementById(id) { return shadow.querySelector('#' + id); },
|
|
contains(n) { return children.includes(n); },
|
|
getRootNode() { return shadow; },
|
|
get ownerDocument() { return document; },
|
|
get nodeType() { return 11; }, // DOCUMENT_FRAGMENT_NODE
|
|
get nodeName() { return '#document-fragment'; },
|
|
addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
|
|
setHTMLUnsafe(v) { this.innerHTML = String(v == null ? "" : v); },
|
|
getHTML() { return this.innerHTML; },
|
|
// Own textContent: ShadowRoot now extends DocumentFragment, so without
|
|
// these the inherited Node accessors run against this._nid. The setter in
|
|
// particular would target the host document and wipe it. Operate on the
|
|
// shadow's own `children` store instead.
|
|
get textContent() { return children.map(c => c.textContent || "").join(""); },
|
|
set textContent(v) {
|
|
children.length = 0;
|
|
if (v != null && v !== "") children.push(document.createTextNode(String(v)));
|
|
},
|
|
hasChildNodes() { return children.length > 0; },
|
|
// A detached fragment id backs any inherited nid-based method we do not
|
|
// override, so they stay non-destructive (operate on an empty fragment)
|
|
// rather than falling through to node 0 / the document.
|
|
_nid: +_dom("create_document_fragment"),
|
|
activeElement: null,
|
|
get styleSheets() { return []; },
|
|
cloneNode() { throw new DOMException('Failed to execute cloneNode on Node: ShadowRoot nodes are not clonable.', 'NotSupportedError'); },
|
|
};
|
|
Object.setPrototypeOf(shadow, ShadowRoot.prototype);
|
|
this._shadowRoot = shadow;
|
|
return shadow;
|
|
};
|
|
|
|
_markNative(Element.prototype.attachShadow);
|
|
|
|
Object.defineProperty(Element.prototype, 'shadowRoot', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function () {
|
|
var sr = this._shadowRoot;
|
|
return sr && sr.mode === 'open' ? sr : null;
|
|
},
|
|
});
|
|
|
|
// setHTMLUnsafe / getHTML: shims over innerHTML. setHTMLUnsafe parses markup
|
|
// like innerHTML (declarative shadow roots inside are not expanded yet, but the
|
|
// call no longer throws so the rest of a test file can run); getHTML serializes
|
|
// like innerHTML.
|
|
Element.prototype.setHTMLUnsafe = function setHTMLUnsafe(html) { this.innerHTML = String(html == null ? "" : html); };
|
|
Element.prototype.getHTML = function getHTML() { return this.innerHTML; };
|
|
_markNative(Element.prototype.setHTMLUnsafe);
|
|
_markNative(Element.prototype.getHTML);
|
|
// Document.parseHTMLUnsafe(html): static that parses into a new HTML document.
|
|
if (typeof Document !== 'undefined' && typeof Document.parseHTMLUnsafe !== 'function') {
|
|
Document.parseHTMLUnsafe = function parseHTMLUnsafe(html) {
|
|
return new DOMParser().parseFromString(String(html == null ? "" : html), "text/html");
|
|
};
|
|
_markNative(Document.parseHTMLUnsafe);
|
|
}
|
|
|
|
globalThis.AudioBuffer = class AudioBuffer {
|
|
constructor(opts) {
|
|
var o = (typeof opts === 'object' && opts !== null) ? opts : {};
|
|
this.numberOfChannels = o.numberOfChannels || 1;
|
|
this.length = o.length || 0;
|
|
this.sampleRate = o.sampleRate || 44100;
|
|
this.duration = this.length / (this.sampleRate || 44100);
|
|
this._chs = [];
|
|
for (var c = 0; c < this.numberOfChannels; c++) this._chs.push(new Float32Array(this.length));
|
|
}
|
|
getChannelData(c) { return this._chs[c] || this._chs[0] || new Float32Array(0); }
|
|
copyFromChannel(dst, ch, start) { var s=this._chs[ch]||this._chs[0]; start=start||0; for(var i=0;i<dst.length;i++) dst[i]=(s&&s[start+i])||0; }
|
|
copyToChannel(src, ch, start) { var d=this._chs[ch]||this._chs[0]; start=start||0; if(d) for(var i=0;i<src.length;i++) d[start+i]=src[i]; }
|
|
};
|
|
globalThis.AudioContext = class AudioContext {
|
|
constructor() { this.sampleRate=_fp('audioSampleRate'); this.state='running'; this.currentTime=0; this.baseLatency=_fp('audioBaseLatency'); this.destination={maxChannelCount:2,numberOfInputs:1,numberOfOutputs:0,channelCount:2}; this._listeners={}; }
|
|
addEventListener(type, fn) { if (!this._listeners[type]) this._listeners[type]=[]; this._listeners[type].push(fn); }
|
|
removeEventListener(type, fn) { if (this._listeners[type]) this._listeners[type]=this._listeners[type].filter(h=>h!==fn); }
|
|
_ap(v, min=-3.4028235e38, max=3.4028235e38) { return { value: v, defaultValue: v, minValue: min, maxValue: max, setValueAtTime(){} }; }
|
|
createOscillator() { return {context:this,type:'sine',frequency:this._ap(440, -22050, 22050),detune:this._ap(0, -153600, 153600),connect(){},start(){},stop(){},disconnect(){},addEventListener(){},removeEventListener(){}}; }
|
|
createDynamicsCompressor() { return {context:this,threshold:this._ap(_fp('compThreshold'), -100, 0),knee:this._ap(_fp('compKnee'), 0, 40),ratio:this._ap(_fp('compRatio'), 1, 20),attack:this._ap(0.003, 0, 1),release:this._ap(0.25, 0, 1),reduction:0,connect(){},disconnect(){}}; }
|
|
createAnalyser() {
|
|
return {context:this,fftSize:2048,frequencyBinCount:1024,channelCount:2,channelCountMode:'max',channelInterpretation:'speakers',maxDecibels:-30,minDecibels:-100,numberOfInputs:1,numberOfOutputs:1,smoothingTimeConstant:0.8,connect(){},disconnect(){},
|
|
getByteFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=Math.floor(_fpRand(600+i)*10);},
|
|
getFloatFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=-100+_fpRand(700+i)*5;}
|
|
};
|
|
}
|
|
createGain() { return {context:this,gain:this._ap(1),connect(){},disconnect(){}}; }
|
|
createBiquadFilter() { return {context:this,type:'lowpass',frequency:this._ap(350, 0, 22050),Q:this._ap(1, 0.0001, 1000),gain:this._ap(0, -40, 40),connect(){},disconnect(){}}; }
|
|
createBufferSource() { return {context:this,buffer:null,connect(){},start(){},stop(){},disconnect(){},loop:false}; }
|
|
createBuffer(ch,len,rate) { return new globalThis.AudioBuffer({numberOfChannels:ch||1,length:len||0,sampleRate:rate||44100}); }
|
|
createScriptProcessor() { return {connect(){},disconnect(){},onaudioprocess:null}; }
|
|
decodeAudioData(buf) { return Promise.resolve(this.createBuffer(2,44100,44100)); }
|
|
resume() { this.state='running'; return Promise.resolve(); }
|
|
suspend() { this.state='suspended'; return Promise.resolve(); }
|
|
close() { this.state='closed'; return Promise.resolve(); }
|
|
};
|
|
globalThis.OfflineAudioContext = class OfflineAudioContext extends AudioContext {
|
|
constructor(ch,len,rate) {
|
|
super();
|
|
if (typeof ch === 'object' && ch !== null) {
|
|
this.length = ch.length || 44100;
|
|
this.sampleRate = ch.sampleRate || 44100;
|
|
} else {
|
|
this.length = len || 44100;
|
|
this.sampleRate = rate || 44100;
|
|
}
|
|
this.oncomplete = null;
|
|
}
|
|
startRendering() {
|
|
var self = this;
|
|
var buf = this.createBuffer(1, self.length, 44100);
|
|
var data = buf.getChannelData(0);
|
|
// Simulate compressed triangle wave at 10kHz.
|
|
// Target: sum(|data[4500..5000]|) matches Chrome Linux (~124.04347527516074).
|
|
var target = 124.04347527516074 + (_fpRand(9991) - 0.5) * 0.002;
|
|
var freq = 10000, sr = 44100;
|
|
for (var i = 0; i < self.length; i++) {
|
|
var phase = ((i * freq / sr) % 1 + 1) % 1;
|
|
data[i] = phase < 0.5 ? 4*phase - 1 : 3 - 4*phase;
|
|
}
|
|
var s = 0;
|
|
for (var i = 4500; i < 5000; i++) s += Math.abs(data[i]);
|
|
var scale = s > 0 ? target / s : 0;
|
|
for (var i = 0; i < self.length; i++) data[i] *= scale;
|
|
// Fire oncomplete + 'complete' listeners on next microtask so callers
|
|
// can register handlers synchronously after startRendering().
|
|
var p = Promise.resolve().then(function() {
|
|
var evt = {renderedBuffer: buf, target: self, type: 'complete'};
|
|
if (typeof self.oncomplete === 'function') {
|
|
try { self.oncomplete(evt); } catch(e) {}
|
|
}
|
|
var listeners = (self._listeners && self._listeners['complete']) || [];
|
|
for (var i = 0; i < listeners.length; i++) {
|
|
try { listeners[i](evt); } catch(e) {}
|
|
}
|
|
return buf;
|
|
});
|
|
return p;
|
|
}
|
|
};
|
|
globalThis.webkitAudioContext = globalThis.AudioContext;
|
|
|
|
globalThis.speechSynthesis = {
|
|
speaking: false, pending: false, paused: false,
|
|
getVoices() { return [{ name:'Google US English', lang:'en-US', default:true, localService:true, voiceURI:'Google US English' }]; },
|
|
speak() {}, cancel() {}, pause() {}, resume() {},
|
|
addEventListener() {}, removeEventListener() {},
|
|
onvoiceschanged: null,
|
|
};
|
|
globalThis.SpeechSynthesisUtterance = class SpeechSynthesisUtterance { constructor(t){this.text=t;this.lang='en-US';this.rate=1;this.pitch=1;this.volume=1;} };
|
|
|
|
globalThis.MediaStream = class MediaStream { constructor(){this.id='';this.active=true;} getTracks(){return [];} getAudioTracks(){return [];} getVideoTracks(){return [];} addTrack(){} removeTrack(){} clone(){return new MediaStream();} };
|
|
globalThis.MediaStreamTrack = class MediaStreamTrack { constructor(){this.kind='';this.enabled=true;this.readyState='live';} stop(){} clone(){return new MediaStreamTrack();} };
|
|
globalThis.RTCPeerConnection = class RTCPeerConnection {
|
|
constructor(){this.localDescription=null;this.remoteDescription=null;this.iceConnectionState='new';this.iceGatheringState='new';this.signalingState='stable';this.connectionState='new';}
|
|
createOffer(){return Promise.resolve({type:'offer',sdp:''});}
|
|
createAnswer(){return Promise.resolve({type:'answer',sdp:''});}
|
|
setLocalDescription(){return Promise.resolve();}
|
|
setRemoteDescription(){return Promise.resolve();}
|
|
addIceCandidate(){return Promise.resolve();}
|
|
close(){}
|
|
createDataChannel(){return {close(){},send(){},addEventListener(){},removeEventListener(){}};}
|
|
addEventListener(){} removeEventListener(){}
|
|
getStats(){return Promise.resolve(new Map());}
|
|
};
|
|
globalThis.RTCSessionDescription = class RTCSessionDescription { constructor(d){this.type=d?.type;this.sdp=d?.sdp;} };
|
|
globalThis.RTCIceCandidate = class RTCIceCandidate { constructor(d){this.candidate=d?.candidate||'';} };
|
|
|
|
// Minimal but spec-shape-correct IndexedDB shim. We don't persist anything,
|
|
// but authentication libraries (Firebase, Supabase, dexie) hang forever on
|
|
// the first `get` because their request's `onsuccess` is never called. Fire
|
|
// `onsuccess` asynchronously with `null` so reads complete-but-empty, which
|
|
// most libraries treat as a cache miss and fall back to the network.
|
|
function _idbRequest(produceResult) {
|
|
const req = {
|
|
result: undefined,
|
|
error: null,
|
|
source: null,
|
|
transaction: null,
|
|
readyState: 'pending',
|
|
onsuccess: null,
|
|
onerror: null,
|
|
addEventListener(type, fn) { req['on' + type] = fn; },
|
|
removeEventListener(type, fn) { if (req['on' + type] === fn) req['on' + type] = null; },
|
|
};
|
|
Promise.resolve().then(() => {
|
|
try {
|
|
req.result = produceResult();
|
|
req.readyState = 'done';
|
|
if (typeof req.onsuccess === 'function') {
|
|
try { req.onsuccess({ target: req, type: 'success' }); } catch (e) {}
|
|
}
|
|
} catch (e) {
|
|
req.error = e; req.readyState = 'done';
|
|
if (typeof req.onerror === 'function') {
|
|
try { req.onerror({ target: req, type: 'error' }); } catch (e2) {}
|
|
}
|
|
}
|
|
});
|
|
return req;
|
|
}
|
|
|
|
function _idbObjectStore(name) {
|
|
const data = new Map();
|
|
return {
|
|
name,
|
|
keyPath: null,
|
|
autoIncrement: false,
|
|
indexNames: { contains() { return false; }, length: 0, item() { return null; } },
|
|
transaction: null,
|
|
add(value, key) { const k = key ?? Date.now(); data.set(k, value); return _idbRequest(() => k); },
|
|
put(value, key) { const k = key ?? Date.now(); data.set(k, value); return _idbRequest(() => k); },
|
|
get(key) { return _idbRequest(() => data.get(key) ?? undefined); },
|
|
getAll() { return _idbRequest(() => Array.from(data.values())); },
|
|
getAllKeys() { return _idbRequest(() => Array.from(data.keys())); },
|
|
getKey(key) { return _idbRequest(() => (data.has(key) ? key : undefined)); },
|
|
delete(key) { return _idbRequest(() => { data.delete(key); return undefined; }); },
|
|
clear() { return _idbRequest(() => { data.clear(); return undefined; }); },
|
|
count() { return _idbRequest(() => data.size); },
|
|
openCursor() { return _idbRequest(() => null); },
|
|
openKeyCursor() { return _idbRequest(() => null); },
|
|
createIndex() { return { name: '', keyPath: '', unique: false, multiEntry: false, get() { return _idbRequest(() => undefined); } }; },
|
|
index() { return { get() { return _idbRequest(() => undefined); }, getAll() { return _idbRequest(() => []); }, count() { return _idbRequest(() => 0); }, openCursor() { return _idbRequest(() => null); } }; },
|
|
deleteIndex() {},
|
|
};
|
|
}
|
|
|
|
function _idbTransaction(storeNames) {
|
|
const stores = new Map();
|
|
const names = Array.isArray(storeNames) ? storeNames : [storeNames];
|
|
for (const n of names) stores.set(String(n), _idbObjectStore(String(n)));
|
|
const tx = {
|
|
db: null,
|
|
mode: 'readonly',
|
|
objectStoreNames: { contains: (n) => stores.has(String(n)), length: stores.size },
|
|
onabort: null, oncomplete: null, onerror: null,
|
|
error: null,
|
|
objectStore(name) {
|
|
let s = stores.get(name);
|
|
if (!s) { s = _idbObjectStore(name); stores.set(name, s); }
|
|
s.transaction = tx;
|
|
return s;
|
|
},
|
|
abort() {},
|
|
commit() {},
|
|
addEventListener(type, fn) { tx['on' + type] = fn; },
|
|
removeEventListener(type, fn) { if (tx['on' + type] === fn) tx['on' + type] = null; },
|
|
};
|
|
Promise.resolve().then(() => {
|
|
if (typeof tx.oncomplete === 'function') {
|
|
try { tx.oncomplete({ target: tx, type: 'complete' }); } catch (e) {}
|
|
}
|
|
});
|
|
return tx;
|
|
}
|
|
|
|
function _idbDatabase(name, version) {
|
|
return {
|
|
name,
|
|
version,
|
|
objectStoreNames: { contains() { return false; }, length: 0, item() { return null; } },
|
|
createObjectStore(n) { return _idbObjectStore(n); },
|
|
deleteObjectStore() {},
|
|
transaction(storeNames, mode) {
|
|
const tx = _idbTransaction(storeNames);
|
|
tx.mode = mode || 'readonly';
|
|
return tx;
|
|
},
|
|
close() {},
|
|
onversionchange: null, onabort: null, onerror: null, onclose: null,
|
|
addEventListener() {}, removeEventListener() {},
|
|
};
|
|
}
|
|
|
|
globalThis.indexedDB = {
|
|
open(name, version) {
|
|
return _idbRequest(() => _idbDatabase(name, version || 1));
|
|
},
|
|
deleteDatabase(_name) { return _idbRequest(() => undefined); },
|
|
databases() { return Promise.resolve([]); },
|
|
cmp(a, b) { return a < b ? -1 : a > b ? 1 : 0; },
|
|
};
|
|
globalThis.IDBKeyRange = {
|
|
only(v) { return { lower: v, upper: v, lowerOpen: false, upperOpen: false, includes(x) { return x === v; } }; },
|
|
lowerBound(v, open) { return { lower: v, upper: null, lowerOpen: !!open, upperOpen: false, includes(x) { return open ? x > v : x >= v; } }; },
|
|
upperBound(v, open) { return { lower: null, upper: v, lowerOpen: false, upperOpen: !!open, includes(x) { return open ? x < v : x <= v; } }; },
|
|
bound(l, u, lo, uo) { return { lower: l, upper: u, lowerOpen: !!lo, upperOpen: !!uo, includes(x) { return (lo ? x > l : x >= l) && (uo ? x < u : x <= u); } }; },
|
|
};
|
|
|
|
globalThis.caches = {
|
|
open() { return Promise.resolve({ match(){return Promise.resolve(undefined);}, put(){return Promise.resolve();}, delete(){return Promise.resolve(false);}, keys(){return Promise.resolve([]);} }); },
|
|
match() { return Promise.resolve(undefined); },
|
|
has() { return Promise.resolve(false); },
|
|
delete() { return Promise.resolve(false); },
|
|
keys() { return Promise.resolve([]); },
|
|
};
|
|
|
|
_markNative(AudioContext); _markNative(OfflineAudioContext);
|
|
_markNative(SpeechSynthesisUtterance);
|
|
_markNative(MediaStream); _markNative(MediaStreamTrack);
|
|
_markNative(RTCPeerConnection); _markNative(RTCSessionDescription); _markNative(RTCIceCandidate);
|
|
|
|
// Timezone is driven by the process TZ (set by the CLI, default Europe/Berlin),
|
|
// so native Intl.DateTimeFormat and Date report the same zone. No JS override:
|
|
// forcing a fixed zone here only on Intl left Date on UTC, which is the exact
|
|
// cross-surface mismatch a fingerprinting script looks for.
|
|
|
|
if (typeof PointerEvent === 'undefined') {
|
|
globalThis.PointerEvent = class PointerEvent extends MouseEvent {
|
|
constructor(type, opts={}) { super(type, opts); this.pointerId = opts.pointerId || 0; this.width = opts.width || 1; this.height = opts.height || 1; this.pressure = opts.pressure || 0; this.pointerType = opts.pointerType || 'mouse'; }
|
|
};
|
|
}
|
|
|
|
if (typeof navigator.credentials === 'undefined') {
|
|
navigator.credentials = { get(){return Promise.resolve(null);}, create(){return Promise.resolve(null);}, store(){return Promise.resolve();}, preventSilentAccess(){return Promise.resolve();} };
|
|
}
|
|
|
|
navigator.mediaCapabilities = {
|
|
decodingInfo(cfg) {
|
|
return Promise.resolve({ supported: true, smooth: true, powerEfficient: true, keySystemAccess: null, configuration: cfg });
|
|
},
|
|
encodingInfo(cfg) {
|
|
return Promise.resolve({ supported: true, smooth: true, powerEfficient: true, configuration: cfg });
|
|
},
|
|
};
|
|
navigator.locks = {
|
|
request(name, opts, cb) {
|
|
if (typeof opts === 'function') { cb = opts; opts = {}; }
|
|
if (typeof cb === 'function') return Promise.resolve(cb({ name, mode: (opts && opts.mode) || 'exclusive' }));
|
|
return Promise.resolve(null);
|
|
},
|
|
query() { return Promise.resolve({ held: [], pending: [] }); },
|
|
};
|
|
navigator.keyboard = {
|
|
getLayoutMap() { return Promise.resolve(new Map()); },
|
|
lock() { return Promise.resolve(); },
|
|
unlock() {},
|
|
};
|
|
navigator.gpu = { requestAdapter() { return Promise.resolve(null); } };
|
|
navigator.wakeLock = { request() { return Promise.reject(new DOMException('Not allowed', 'NotAllowedError')); } };
|
|
|
|
globalThis.opener = null;
|
|
|
|
globalThis.Worker = class Worker {
|
|
constructor(url) {
|
|
this.onmessage = null;
|
|
this.onerror = null;
|
|
this._terminated = false;
|
|
this._listeners = {};
|
|
const worker = this;
|
|
|
|
let resolvedUrl = url;
|
|
if (typeof url === 'string') {
|
|
const blob = globalThis.__blobStore?.[url];
|
|
if (blob) {
|
|
worker._code = blob;
|
|
// Auto-start on next tick so caller can set onmessage first.
|
|
setTimeout(() => worker._autoRun(), 0);
|
|
return;
|
|
}
|
|
// Resolve relative URLs against the current page.
|
|
if (!url.startsWith('http') && !url.startsWith('blob:') && !url.startsWith('data:')) {
|
|
try { resolvedUrl = new URL(url, globalThis.location?.href || '').href; } catch(e) {}
|
|
}
|
|
(async () => {
|
|
try {
|
|
const resp = await fetch(resolvedUrl);
|
|
worker._code = await resp.text();
|
|
if (!worker._terminated) worker._autoRun();
|
|
} catch(e) { if (worker.onerror) worker.onerror(e); }
|
|
})();
|
|
}
|
|
}
|
|
_makeScope() {
|
|
const worker = this;
|
|
// WorkerGlobalScope defined + no document property → IS_WORKER_SCOPE = true in creepjs
|
|
const scope = {
|
|
WorkerGlobalScope: function WorkerGlobalScope() {},
|
|
DedicatedWorkerGlobalScope: function DedicatedWorkerGlobalScope() {},
|
|
postMessage: (msg) => {
|
|
if (worker._terminated) return;
|
|
const evt = { data: msg };
|
|
if (worker.onmessage) worker.onmessage(evt);
|
|
const ls = worker._listeners['message'] || [];
|
|
for (const h of ls) h(evt);
|
|
},
|
|
addEventListener: (type, fn) => {
|
|
if (!scope._ev) scope._ev = {};
|
|
if (!scope._ev[type]) scope._ev[type] = [];
|
|
scope._ev[type].push(fn);
|
|
},
|
|
close: () => { worker._terminated = true; },
|
|
crypto: globalThis.crypto,
|
|
Crypto: globalThis.Crypto,
|
|
TextEncoder: globalThis.TextEncoder,
|
|
TextDecoder: globalThis.TextDecoder,
|
|
atob: globalThis.atob,
|
|
btoa: globalThis.btoa,
|
|
setTimeout: globalThis.setTimeout,
|
|
setInterval: globalThis.setInterval,
|
|
clearTimeout: globalThis.clearTimeout,
|
|
clearInterval: globalThis.clearInterval,
|
|
fetch: globalThis.fetch,
|
|
console: globalThis.console,
|
|
performance: globalThis.performance,
|
|
location: globalThis.location,
|
|
};
|
|
scope.self = scope;
|
|
return scope;
|
|
}
|
|
_autoRun() {
|
|
if (this._terminated || !this._code) return;
|
|
const worker = this;
|
|
const scope = worker._makeScope();
|
|
try {
|
|
const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code);
|
|
fn(scope, scope.postMessage, scope.addEventListener, scope.close);
|
|
} catch(e) {
|
|
console.error('Worker error:', e.message);
|
|
if (worker.onerror) worker.onerror(e);
|
|
}
|
|
}
|
|
postMessage(data) {
|
|
if (this._terminated) return;
|
|
const worker = this;
|
|
setTimeout(() => {
|
|
if (worker._terminated || !worker._code) return;
|
|
const scope = worker._makeScope();
|
|
try {
|
|
const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code);
|
|
fn(scope, scope.postMessage, scope.addEventListener, scope.close);
|
|
const evs = (scope._ev && scope._ev['message']) || [];
|
|
if (evs.length) { for (const h of evs) h({ data }); }
|
|
else if (scope.onmessage) scope.onmessage({ data });
|
|
} catch(e) {
|
|
console.error('Worker error:', e.message);
|
|
if (worker.onerror) worker.onerror(e);
|
|
}
|
|
}, 0);
|
|
}
|
|
terminate() { this._terminated = true; }
|
|
addEventListener(type, fn) {
|
|
if (!this._listeners[type]) this._listeners[type] = [];
|
|
this._listeners[type].push(fn);
|
|
}
|
|
removeEventListener(type, fn) {
|
|
if (this._listeners[type]) this._listeners[type] = this._listeners[type].filter(h => h !== fn);
|
|
}
|
|
};
|
|
|
|
globalThis.__blobStore = globalThis.__blobStore || {};
|
|
URL.createObjectURL = function(blob) {
|
|
if (blob) {
|
|
const id = 'blob:obscura/' + Math.random().toString(36).substring(2);
|
|
// Store synchronously so a Worker built from the blob URL in the same
|
|
// tick sees its source. Blob-URL Worker construction is synchronous in
|
|
// real browsers; the previous async blob.text().then() store raced the
|
|
// Worker constructor, so new Worker(blobURL) fell through to fetch() and
|
|
// failed (net::ERR_FAILED), which broke AWS WAF's proof-of-work worker.
|
|
// The obscura Blob materializes _bytes in its constructor; fall back to
|
|
// the async text() store only for foreign Blob shims without _bytes.
|
|
if (blob._bytes) {
|
|
let text = '';
|
|
try { text = new TextDecoder().decode(blob._bytes); } catch (e) {}
|
|
globalThis.__blobStore[id] = text;
|
|
} else if (typeof blob.text === 'function') {
|
|
blob.text().then(text => { globalThis.__blobStore[id] = text; });
|
|
} else {
|
|
globalThis.__blobStore[id] = '';
|
|
}
|
|
return id;
|
|
}
|
|
return 'blob:obscura/fallback';
|
|
};
|
|
URL.revokeObjectURL = function(url) {
|
|
delete globalThis.__blobStore[url];
|
|
};
|
|
|
|
// Window-level scrolling (issue #468). #431 gave elements functional
|
|
// scrollTop/scrollLeft plus scroll methods, but left these three as no-ops, so
|
|
// the dominant infinite-scroll idiom -- window.scrollTo(0, body.scrollHeight),
|
|
// window.scrollBy(0, 500), then a window 'scroll' listener -- did nothing at
|
|
// all: the offset never moved and no event ever fired.
|
|
//
|
|
// The page offset is stored on the scrolling element rather than in separate
|
|
// window state, so window.scrollY and document.scrollingElement.scrollTop are
|
|
// two views of one value, which is what pages assume. As with #431 there is no
|
|
// layout, so the offset still cannot be clamped to a real maximum.
|
|
function _scrollRoot() {
|
|
const doc = globalThis.document;
|
|
return (doc && doc.scrollingElement) || null;
|
|
}
|
|
function _windowScroll(x, y, relative) {
|
|
const root = _scrollRoot();
|
|
if (!root) return;
|
|
let left, top;
|
|
if (x !== null && typeof x === 'object') { left = x.left; top = x.top; }
|
|
else { left = x; top = y; }
|
|
if (left !== undefined) {
|
|
root.scrollLeft = (relative ? (root.scrollLeft || 0) : 0) + (+left || 0);
|
|
}
|
|
if (top !== undefined) {
|
|
root.scrollTop = (relative ? (root.scrollTop || 0) : 0) + (+top || 0);
|
|
}
|
|
// Async, matching the element path #431 added. Dispatched at the document
|
|
// AND the window: a page scroll event reaches both in Chrome, but
|
|
// Document.dispatchEvent here runs only its own listeners and does not
|
|
// propagate, so firing once would strand half the listeners.
|
|
setTimeout(() => {
|
|
try {
|
|
const doc = globalThis.document;
|
|
if (doc) doc.dispatchEvent(new Event('scroll', { bubbles: false }));
|
|
globalThis.dispatchEvent(new Event('scroll', { bubbles: false }));
|
|
} catch (e) {}
|
|
}, 0);
|
|
}
|
|
globalThis.scrollTo = function(x, y) { _windowScroll(x, y, false); };
|
|
globalThis.scrollBy = function(x, y) { _windowScroll(x, y, true); };
|
|
globalThis.scroll = function(x, y) { _windowScroll(x, y, false); };
|
|
_markNative(globalThis.scrollTo);
|
|
_markNative(globalThis.scrollBy);
|
|
_markNative(globalThis.scroll);
|
|
// Read-only accessors, as on a real Window: assigning window.scrollY does not
|
|
// scroll the page. These replace the hard-coded 0 data properties defined
|
|
// earlier, so they must stay after them.
|
|
for (const [name, offset] of [
|
|
['scrollX', 'scrollLeft'], ['pageXOffset', 'scrollLeft'],
|
|
['scrollY', 'scrollTop'], ['pageYOffset', 'scrollTop'],
|
|
]) {
|
|
Object.defineProperty(globalThis, name, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() { const root = _scrollRoot(); return root ? (root[offset] || 0) : 0; },
|
|
});
|
|
}
|
|
globalThis.focus = function() {}; _markNative(globalThis.focus);
|
|
globalThis.blur = function() {}; _markNative(globalThis.blur);
|
|
globalThis.print = function() {}; _markNative(globalThis.print);
|
|
globalThis.alert = function() {}; _markNative(globalThis.alert);
|
|
globalThis.confirm = function() { return true; }; _markNative(globalThis.confirm);
|
|
globalThis.prompt = function() { return null; }; _markNative(globalThis.prompt);
|
|
globalThis.open = function() { return null; }; _markNative(globalThis.open);
|
|
globalThis.close = function() {}; _markNative(globalThis.close);
|
|
globalThis.stop = function() {}; _markNative(globalThis.stop);
|
|
globalThis.postMessage = function() {}; _markNative(globalThis.postMessage);
|
|
globalThis.requestIdleCallback = globalThis.requestIdleCallback || function(cb) { return setTimeout(cb, 0); };
|
|
globalThis.cancelIdleCallback = globalThis.cancelIdleCallback || function(id) { clearTimeout(id); };
|
|
if (typeof ReadableStream === 'undefined') {
|
|
globalThis.ReadableStream = class ReadableStream {
|
|
constructor(source = {}, strategy = {}) {
|
|
this._source = source; this._queue = []; this._closed = false;
|
|
this.locked = false;
|
|
if (source.start) source.start({ enqueue: (chunk) => this._queue.push(chunk), close: () => { this._closed = true; }, error: () => {} });
|
|
}
|
|
getReader() {
|
|
this.locked = true;
|
|
const stream = this;
|
|
return {
|
|
read() {
|
|
if (stream._queue.length > 0) return Promise.resolve({ value: stream._queue.shift(), done: false });
|
|
if (stream._closed) return Promise.resolve({ value: undefined, done: true });
|
|
return Promise.resolve({ value: undefined, done: true });
|
|
},
|
|
releaseLock() { stream.locked = false; },
|
|
cancel() { stream._closed = true; return Promise.resolve(); },
|
|
get closed() { return stream._closed ? Promise.resolve() : new Promise(() => {}); },
|
|
};
|
|
}
|
|
cancel() { this._closed = true; return Promise.resolve(); }
|
|
pipeTo(dest) { return Promise.resolve(); }
|
|
pipeThrough(transform) { return transform.readable || new ReadableStream(); }
|
|
tee() { return [new ReadableStream(), new ReadableStream()]; }
|
|
[Symbol.asyncIterator]() {
|
|
const reader = this.getReader();
|
|
return { next: () => reader.read(), return: () => { reader.releaseLock(); return Promise.resolve({done:true}); } };
|
|
}
|
|
};
|
|
}
|
|
if (typeof WritableStream === 'undefined') {
|
|
globalThis.WritableStream = class WritableStream {
|
|
constructor(sink = {}) { this._sink = sink; this.locked = false; }
|
|
getWriter() {
|
|
this.locked = true;
|
|
const stream = this;
|
|
return {
|
|
write(chunk) { if (stream._sink.write) stream._sink.write(chunk); return Promise.resolve(); },
|
|
close() { if (stream._sink.close) stream._sink.close(); return Promise.resolve(); },
|
|
abort() { return Promise.resolve(); },
|
|
releaseLock() { stream.locked = false; },
|
|
get ready() { return Promise.resolve(); },
|
|
get closed() { return Promise.resolve(); },
|
|
get desiredSize() { return 1; },
|
|
};
|
|
}
|
|
close() { return Promise.resolve(); }
|
|
abort() { return Promise.resolve(); }
|
|
};
|
|
}
|
|
if (typeof TransformStream === 'undefined') {
|
|
globalThis.TransformStream = class TransformStream {
|
|
constructor(transformer = {}) {
|
|
this.readable = new ReadableStream();
|
|
this.writable = new WritableStream();
|
|
}
|
|
};
|
|
}
|
|
|
|
if (!globalThis.crypto) globalThis.crypto = {};
|
|
if (!globalThis.crypto.subtle) {
|
|
// Real WebCrypto for the secret-key algorithms sites actually use: HMAC,
|
|
// AES-GCM/CBC/CTR, PBKDF2 and HKDF, plus raw/JWK-oct key handling. The crypto
|
|
// itself runs in Rust ops (RustCrypto); this shim only marshals bytes and
|
|
// normalizes algorithm parameters. Public-key algorithms (RSA*, ECDSA, ECDH)
|
|
// and non-symmetric key formats (pkcs8/spki) are not implemented and throw
|
|
// NotSupportedError rather than returning fake data.
|
|
const keyMaterial = new WeakMap();
|
|
|
|
class CryptoKey {
|
|
constructor() { throw new TypeError("Illegal constructor"); }
|
|
get [Symbol.toStringTag]() { return "CryptoKey"; }
|
|
}
|
|
function makeKey(type, extractable, algorithm, usages, bytes) {
|
|
const k = Object.create(CryptoKey.prototype);
|
|
Object.defineProperty(k, "type", { value: type, enumerable: true });
|
|
Object.defineProperty(k, "extractable", { value: !!extractable, enumerable: true });
|
|
Object.defineProperty(k, "algorithm", { value: algorithm, enumerable: true });
|
|
Object.defineProperty(k, "usages", { value: Object.freeze((usages || []).slice()), enumerable: true });
|
|
keyMaterial.set(k, bytes);
|
|
return k;
|
|
}
|
|
function keyBytes(key) {
|
|
if (!(key instanceof CryptoKey) || !keyMaterial.has(key)) {
|
|
throw new DOMException("Argument is not a valid CryptoKey", "InvalidAccessError");
|
|
}
|
|
return keyMaterial.get(key);
|
|
}
|
|
// A CryptoKey cloned via structuredClone or postMessage is a different
|
|
// object, so the WeakMap lookup above misses and crypto.subtle throws
|
|
// "Argument is not a valid CryptoKey". Re-register the (cloned) key's
|
|
// material so the clone stays usable. The clone hook is dispatched by
|
|
// _structuredClone via Symbol.toStringTag ("CryptoKey"); registered lazily
|
|
// because structuredClone is defined before this block (issue #389).
|
|
globalThis.__obscura_clone_hooks = globalThis.__obscura_clone_hooks || {};
|
|
// `seen` is the clone memo _structuredClone hands every hook. Populate it so
|
|
// one key reached twice in a graph clones to one shared object (and its key
|
|
// material is registered once), matching structuredClone's identity rules.
|
|
globalThis.__obscura_clone_hooks["CryptoKey"] = function (src, seen) {
|
|
if (seen && seen.has(src)) return seen.get(src);
|
|
const copy = makeKey(src.type, src.extractable, src.algorithm, src.usages, keyBytes(src));
|
|
if (seen) seen.set(src, copy);
|
|
return copy;
|
|
};
|
|
|
|
const toBytes = (data) => {
|
|
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
return new Uint8Array(data || []);
|
|
};
|
|
const bufferOf = (u8) => new Uint8Array(u8).buffer;
|
|
|
|
const ALGO_CANON = {
|
|
"AES-CTR": "AES-CTR", "AES-CBC": "AES-CBC", "AES-GCM": "AES-GCM", "AES-KW": "AES-KW",
|
|
"HMAC": "HMAC", "PBKDF2": "PBKDF2", "HKDF": "HKDF",
|
|
"RSASSA-PKCS1-V1_5": "RSASSA-PKCS1-v1_5", "RSA-PSS": "RSA-PSS", "RSA-OAEP": "RSA-OAEP",
|
|
"ECDSA": "ECDSA", "ECDH": "ECDH",
|
|
};
|
|
function normalizeAlgo(algorithm) {
|
|
const a = typeof algorithm === "string" ? { name: algorithm } : (algorithm || {});
|
|
const upper = String(a.name || "").toUpperCase();
|
|
const name = ALGO_CANON[upper] || upper;
|
|
return Object.assign({}, a, { name });
|
|
}
|
|
// SubtleCrypto hashes for HMAC/PBKDF2/HKDF and digest (SHA-1/256/384/512).
|
|
function normalizeHash(h) {
|
|
const n = (typeof h === "string" ? h : (h && h.name) || "").toUpperCase().replace("_", "-");
|
|
if (n === "SHA-1" || n === "SHA-256" || n === "SHA-384" || n === "SHA-512") return n;
|
|
throw new DOMException("Unsupported hash algorithm: " + (h && (h.name || h)), "NotSupportedError");
|
|
}
|
|
const hashBlockSize = (hash) => (hash === "SHA-384" || hash === "SHA-512" ? 128 : 64);
|
|
|
|
function b64urlToBytes(s) {
|
|
s = String(s).replace(/-/g, "+").replace(/_/g, "/");
|
|
while (s.length % 4) s += "=";
|
|
const bin = atob(s);
|
|
const out = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
return out;
|
|
}
|
|
function bytesToB64url(bytes) {
|
|
let bin = "";
|
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
}
|
|
|
|
// Run an op, converting a Rust-side failure (bad GCM tag, bad CBC padding)
|
|
// into the OperationError the WebCrypto spec requires. DOMExceptions we raise
|
|
// ourselves pass through unchanged.
|
|
function runOp(fn) {
|
|
try { return fn(); }
|
|
catch (e) {
|
|
if (e instanceof DOMException) throw e;
|
|
throw new DOMException(String((e && e.message) || e), "OperationError");
|
|
}
|
|
}
|
|
|
|
function keyAlgorithmFor(alg, bytes) {
|
|
switch (alg.name) {
|
|
case "HMAC":
|
|
return { name: "HMAC", hash: { name: normalizeHash(alg.hash) }, length: bytes.length * 8 };
|
|
case "AES-CTR": case "AES-CBC": case "AES-GCM": case "AES-KW":
|
|
if (bytes.length !== 16 && bytes.length !== 24 && bytes.length !== 32) {
|
|
throw new DOMException("AES key data must be 128, 192, or 256 bits", "DataError");
|
|
}
|
|
return { name: alg.name, length: bytes.length * 8 };
|
|
case "PBKDF2": return { name: "PBKDF2" };
|
|
case "HKDF": return { name: "HKDF" };
|
|
default:
|
|
throw new DOMException("Unsupported key algorithm: " + alg.name, "NotSupportedError");
|
|
}
|
|
}
|
|
|
|
const subtle = {
|
|
async digest(algorithm, data) {
|
|
const name = (typeof algorithm === "string" ? algorithm : algorithm && algorithm.name || "").toUpperCase().replace("_", "-");
|
|
if (name !== "SHA-1" && name !== "SHA-256" && name !== "SHA-384" && name !== "SHA-512" &&
|
|
name !== "SHA-512/224" && name !== "SHA-512/256") {
|
|
throw new DOMException("Unrecognized algorithm name", "NotSupportedError");
|
|
}
|
|
return bufferOf(Deno.core.ops.op_subtle_digest(name, toBytes(data)));
|
|
},
|
|
|
|
async importKey(format, keyData, algorithm, extractable, keyUsages) {
|
|
const alg = normalizeAlgo(algorithm);
|
|
let bytes;
|
|
if (format === "raw") {
|
|
bytes = toBytes(keyData);
|
|
} else if (format === "jwk") {
|
|
if (!keyData || keyData.kty !== "oct" || typeof keyData.k !== "string") {
|
|
throw new DOMException("Only symmetric 'oct' JWK keys are supported", "NotSupportedError");
|
|
}
|
|
bytes = b64urlToBytes(keyData.k);
|
|
} else {
|
|
throw new DOMException("Only 'raw' and symmetric 'jwk' key formats are supported", "NotSupportedError");
|
|
}
|
|
return makeKey("secret", extractable, keyAlgorithmFor(alg, bytes), keyUsages, bytes);
|
|
},
|
|
|
|
async exportKey(format, key) {
|
|
const bytes = keyBytes(key);
|
|
if (!key.extractable) throw new DOMException("Key is not extractable", "InvalidAccessError");
|
|
if (format === "raw") return bufferOf(bytes);
|
|
if (format === "jwk") {
|
|
const jwk = { kty: "oct", k: bytesToB64url(bytes), ext: key.extractable, key_ops: key.usages.slice() };
|
|
if (key.algorithm.name && key.algorithm.name.indexOf("AES-") === 0) {
|
|
jwk.alg = "A" + (bytes.length * 8) + key.algorithm.name.slice(4);
|
|
} else if (key.algorithm.name === "HMAC") {
|
|
jwk.alg = "HS" + key.algorithm.hash.name.slice(4);
|
|
}
|
|
return jwk;
|
|
}
|
|
throw new DOMException("Only 'raw' and 'jwk' export is supported", "NotSupportedError");
|
|
},
|
|
|
|
async generateKey(algorithm, extractable, keyUsages) {
|
|
const alg = normalizeAlgo(algorithm);
|
|
if (alg.name === "HMAC") {
|
|
const hash = normalizeHash(alg.hash);
|
|
const len = alg.length ? Math.ceil(alg.length / 8) : hashBlockSize(hash);
|
|
const bytes = Deno.core.ops.op_random_bytes(len);
|
|
return makeKey("secret", extractable, { name: "HMAC", hash: { name: hash }, length: len * 8 }, keyUsages, bytes);
|
|
}
|
|
if (alg.name === "AES-CTR" || alg.name === "AES-CBC" || alg.name === "AES-GCM" || alg.name === "AES-KW") {
|
|
if (alg.length !== 128 && alg.length !== 192 && alg.length !== 256) {
|
|
throw new DOMException("AES key length must be 128, 192, or 256 bits", "OperationError");
|
|
}
|
|
const bytes = Deno.core.ops.op_random_bytes(alg.length / 8);
|
|
return makeKey("secret", extractable, { name: alg.name, length: alg.length }, keyUsages, bytes);
|
|
}
|
|
throw new DOMException("generateKey does not support " + alg.name, "NotSupportedError");
|
|
},
|
|
|
|
async sign(algorithm, key, data) {
|
|
const alg = normalizeAlgo(algorithm);
|
|
const bytes = keyBytes(key);
|
|
if (alg.name === "HMAC") {
|
|
const hash = key.algorithm && key.algorithm.hash ? key.algorithm.hash.name : normalizeHash(alg.hash);
|
|
return bufferOf(runOp(() => Deno.core.ops.op_subtle_hmac(hash, bytes, toBytes(data))));
|
|
}
|
|
throw new DOMException("sign does not support " + alg.name, "NotSupportedError");
|
|
},
|
|
|
|
async verify(algorithm, key, signature, data) {
|
|
const alg = normalizeAlgo(algorithm);
|
|
const bytes = keyBytes(key);
|
|
if (alg.name === "HMAC") {
|
|
const hash = key.algorithm && key.algorithm.hash ? key.algorithm.hash.name : normalizeHash(alg.hash);
|
|
const mac = runOp(() => Deno.core.ops.op_subtle_hmac(hash, bytes, toBytes(data)));
|
|
const sig = toBytes(signature);
|
|
if (sig.length !== mac.length) return false;
|
|
let diff = 0;
|
|
for (let i = 0; i < mac.length; i++) diff |= mac[i] ^ sig[i];
|
|
return diff === 0;
|
|
}
|
|
throw new DOMException("verify does not support " + alg.name, "NotSupportedError");
|
|
},
|
|
|
|
async encrypt(algorithm, key, data) { return aesCipher(true, algorithm, key, data); },
|
|
async decrypt(algorithm, key, data) { return aesCipher(false, algorithm, key, data); },
|
|
|
|
async deriveBits(algorithm, baseKey, length) {
|
|
const alg = normalizeAlgo(algorithm);
|
|
const bytes = keyBytes(baseKey);
|
|
const lenBytes = Math.ceil((length || 0) / 8);
|
|
if (alg.name === "PBKDF2") {
|
|
const hash = normalizeHash(alg.hash);
|
|
const salt = toBytes(alg.salt);
|
|
const iterations = alg.iterations >>> 0;
|
|
return bufferOf(runOp(() => Deno.core.ops.op_subtle_pbkdf2(hash, bytes, salt, iterations, lenBytes)));
|
|
}
|
|
if (alg.name === "HKDF") {
|
|
const hash = normalizeHash(alg.hash);
|
|
const salt = alg.salt != null ? toBytes(alg.salt) : new Uint8Array(0);
|
|
const info = alg.info != null ? toBytes(alg.info) : new Uint8Array(0);
|
|
return bufferOf(runOp(() => Deno.core.ops.op_subtle_hkdf(hash, bytes, salt, info, lenBytes)));
|
|
}
|
|
throw new DOMException("deriveBits does not support " + alg.name, "NotSupportedError");
|
|
},
|
|
|
|
async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
|
|
const dAlg = normalizeAlgo(derivedKeyAlgorithm);
|
|
let bits;
|
|
if (dAlg.name === "HMAC") {
|
|
bits = dAlg.length || hashBlockSize(normalizeHash(dAlg.hash)) * 8;
|
|
} else if (dAlg.name === "AES-CTR" || dAlg.name === "AES-CBC" || dAlg.name === "AES-GCM" || dAlg.name === "AES-KW") {
|
|
bits = dAlg.length;
|
|
if (bits !== 128 && bits !== 192 && bits !== 256) {
|
|
throw new DOMException("AES key length must be 128, 192, or 256 bits", "OperationError");
|
|
}
|
|
} else {
|
|
throw new DOMException("deriveKey does not support deriving " + dAlg.name, "NotSupportedError");
|
|
}
|
|
const derivedBits = await this.deriveBits(algorithm, baseKey, bits);
|
|
return this.importKey("raw", derivedBits, derivedKeyAlgorithm, extractable, keyUsages);
|
|
},
|
|
|
|
async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
|
|
const exported = await this.exportKey(format, key);
|
|
const bytes = format === "jwk"
|
|
? new TextEncoder().encode(JSON.stringify(exported))
|
|
: new Uint8Array(exported);
|
|
return this.encrypt(wrapAlgorithm, wrappingKey, bytes);
|
|
},
|
|
|
|
async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
|
|
const decrypted = await this.decrypt(unwrapAlgorithm, unwrappingKey, wrappedKey);
|
|
const keyData = format === "jwk"
|
|
? JSON.parse(new TextDecoder().decode(new Uint8Array(decrypted)))
|
|
: decrypted;
|
|
return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
|
|
},
|
|
};
|
|
|
|
function aesCipher(encrypt, algorithm, key, data) {
|
|
const alg = normalizeAlgo(algorithm);
|
|
const bytes = keyBytes(key);
|
|
const input = toBytes(data);
|
|
if (alg.name === "AES-GCM") {
|
|
const iv = toBytes(alg.iv);
|
|
const aad = alg.additionalData != null ? toBytes(alg.additionalData) : new Uint8Array(0);
|
|
const tagLength = alg.tagLength == null ? 128 : alg.tagLength;
|
|
if (tagLength !== 128) {
|
|
throw new DOMException("Only a 128-bit AES-GCM tag length is supported", "NotSupportedError");
|
|
}
|
|
return bufferOf(runOp(() => Deno.core.ops.op_subtle_aes_gcm(encrypt, bytes, iv, aad, input)));
|
|
}
|
|
if (alg.name === "AES-CBC") {
|
|
const iv = toBytes(alg.iv);
|
|
return bufferOf(runOp(() => Deno.core.ops.op_subtle_aes_cbc(encrypt, bytes, iv, input)));
|
|
}
|
|
if (alg.name === "AES-CTR") {
|
|
const counter = toBytes(alg.counter);
|
|
const length = alg.length >>> 0;
|
|
return bufferOf(runOp(() => Deno.core.ops.op_subtle_aes_ctr(bytes, counter, length, input)));
|
|
}
|
|
throw new DOMException((encrypt ? "encrypt" : "decrypt") + " does not support " + alg.name, "NotSupportedError");
|
|
}
|
|
|
|
globalThis.CryptoKey = CryptoKey;
|
|
globalThis.SubtleCrypto = function SubtleCrypto() { throw new TypeError("Illegal constructor"); };
|
|
Object.setPrototypeOf(subtle, globalThis.SubtleCrypto.prototype);
|
|
globalThis.crypto.subtle = subtle;
|
|
}
|
|
|
|
if (typeof DOMRect === 'undefined') {
|
|
globalThis.DOMRect = class DOMRect {
|
|
constructor(x=0,y=0,w=0,h=0) { this.x=x;this.y=y;this.width=w;this.height=h;this.top=y;this.right=x+w;this.bottom=y+h;this.left=x; }
|
|
toJSON() { return {x:this.x,y:this.y,width:this.width,height:this.height,top:this.top,right:this.right,bottom:this.bottom,left:this.left}; }
|
|
static fromRect(r={}) { return new DOMRect(r.x,r.y,r.width,r.height); }
|
|
};
|
|
}
|
|
|
|
if (typeof DOMRectList === 'undefined') {
|
|
globalThis.DOMRectList = class DOMRectList {
|
|
constructor(arr=[]) {
|
|
this.length = arr.length;
|
|
for (let i = 0; i < arr.length; i++) this[i] = arr[i];
|
|
}
|
|
item(i) { return this[i] || null; }
|
|
[Symbol.iterator]() {
|
|
let i = 0, self = this;
|
|
return { next() { const done = i >= self.length; return { value: done ? undefined : self[i++], done }; } };
|
|
}
|
|
};
|
|
}
|
|
if (typeof DOMPoint === 'undefined') {
|
|
globalThis.DOMPoint = class DOMPoint {
|
|
constructor(x=0,y=0,z=0,w=1) { this.x=x;this.y=y;this.z=z;this.w=w; }
|
|
static fromPoint(p={}) { return new DOMPoint(p.x,p.y,p.z,p.w); }
|
|
};
|
|
}
|
|
if (typeof DOMMatrix === 'undefined') {
|
|
globalThis.DOMMatrix = class DOMMatrix {
|
|
constructor() { this.a=1;this.b=0;this.c=0;this.d=1;this.e=0;this.f=0;this.is2D=true;this.isIdentity=true; }
|
|
static fromMatrix() { return new DOMMatrix(); }
|
|
static fromFloat32Array() { return new DOMMatrix(); }
|
|
static fromFloat64Array() { return new DOMMatrix(); }
|
|
multiply() { return new DOMMatrix(); }
|
|
inverse() { return new DOMMatrix(); }
|
|
translate() { return new DOMMatrix(); }
|
|
scale() { return new DOMMatrix(); }
|
|
rotate() { return new DOMMatrix(); }
|
|
transformPoint(p) { return new DOMPoint(p?.x||0,p?.y||0); }
|
|
};
|
|
}
|
|
|
|
if (typeof Image === 'undefined') {
|
|
// In a real browser `new Image()` is `document.createElement('img')`, i.e. a
|
|
// full HTMLImageElement. The old plain-class shim had no `.style`, so
|
|
// `new Image().style` was `undefined` and libraries that touch it on a
|
|
// detached image threw (issue #350). Build a real element so `.style`,
|
|
// attribute reflection, and event dispatch all come for free.
|
|
const _imgSrcDesc = Object.getOwnPropertyDescriptor(globalThis.HTMLImageElement.prototype, 'src');
|
|
globalThis.Image = function Image(width, height) {
|
|
const img = document.createElement('img');
|
|
img.onload = null; img.onerror = null;
|
|
img.complete = false; img.naturalWidth = 0; img.naturalHeight = 0;
|
|
img.width = width !== undefined ? (width >>> 0) : 0;
|
|
img.height = height !== undefined ? (height >>> 0) : 0;
|
|
// There is no real image decoder, so emulate a successful decode: assigning
|
|
// `.src` flips `complete` and fires `load` on a microtask-later tick. Lazy
|
|
// loaders and preloaders that create `new Image()`, set `.src`, and wait for
|
|
// `onload` (or addEventListener('load')) would hang forever otherwise.
|
|
// Anti-bot scripts (Booking.com, issue #394) pre-define a non-configurable
|
|
// own `src` on <img> elements; redefining it throws "Cannot redefine
|
|
// property: src" and kills the constructor. Skip the load emulation then:
|
|
// a page that owns `src` is instrumenting loads itself.
|
|
const ownSrc = Object.getOwnPropertyDescriptor(img, 'src');
|
|
if (!ownSrc || ownSrc.configurable) {
|
|
Object.defineProperty(img, 'src', {
|
|
configurable: true, enumerable: true,
|
|
get() { return _imgSrcDesc.get.call(img); },
|
|
set(v) {
|
|
_imgSrcDesc.set.call(img, v);
|
|
if (!img.getAttribute('src')) return;
|
|
img.complete = false;
|
|
setTimeout(function () {
|
|
img.complete = true;
|
|
img.naturalWidth = img.naturalWidth || img.width || 0;
|
|
img.naturalHeight = img.naturalHeight || img.height || 0;
|
|
try { img.dispatchEvent(new Event('load')); } catch (e) {}
|
|
}, 0);
|
|
},
|
|
});
|
|
}
|
|
return img;
|
|
};
|
|
globalThis.Image.prototype = globalThis.HTMLImageElement.prototype;
|
|
}
|
|
|
|
if (typeof Audio === 'undefined') {
|
|
globalThis.Audio = class Audio {
|
|
constructor(src) { this.src = src || ''; this.paused = true; this.volume = 1; this.currentTime = 0; this.duration = 0; }
|
|
play() { return Promise.resolve(); } pause() { this.paused = true; } load() {}
|
|
addEventListener() {} removeEventListener() {}
|
|
};
|
|
}
|
|
|
|
if (typeof FileReader === 'undefined') {
|
|
globalThis.FileReader = class FileReader {
|
|
constructor() {
|
|
this.result = null; this.error = null; this.readyState = 0; // EMPTY
|
|
this.onloadstart = null; this.onprogress = null; this.onload = null;
|
|
this.onabort = null; this.onerror = null; this.onloadend = null;
|
|
this._listeners = {};
|
|
}
|
|
get [Symbol.toStringTag]() { return "FileReader"; }
|
|
_read(blob, kind, encoding) {
|
|
// Spec: reading while LOADING throws InvalidStateError.
|
|
if (this.readyState === 1) throw new DOMException("The object is already busy reading Blobs.", "InvalidStateError");
|
|
this.readyState = 1; // LOADING
|
|
this.result = null; this.error = null;
|
|
this._fire("loadstart");
|
|
const self = this;
|
|
Promise.resolve().then(function () {
|
|
if (self.readyState !== 1) return; // aborted before completion
|
|
const bytes = (blob && blob._bytes) ? blob._bytes : new Uint8Array(0);
|
|
try {
|
|
if (kind === "text") self.result = new TextDecoder(encoding || "utf-8").decode(bytes);
|
|
else if (kind === "binary") self.result = _bytesToBinaryString(bytes);
|
|
else if (kind === "dataurl") self.result = "data:" + ((blob && blob.type) || "application/octet-stream") + ";base64," + btoa(_bytesToBinaryString(bytes));
|
|
else self.result = _arrayBufferFromBytes(bytes);
|
|
} catch (e) { self.error = e; }
|
|
self.readyState = 2; // DONE
|
|
self._fire("progress"); self._fire("load"); self._fire("loadend");
|
|
});
|
|
}
|
|
readAsText(blob, encoding) { this._read(blob, "text", encoding); }
|
|
readAsDataURL(blob) { this._read(blob, "dataurl"); }
|
|
readAsArrayBuffer(blob) { this._read(blob, "arraybuffer"); }
|
|
readAsBinaryString(blob) { this._read(blob, "binary"); }
|
|
abort() {
|
|
const wasReading = this.readyState === 1;
|
|
this.readyState = 0; this.result = null;
|
|
if (wasReading) { this._fire("abort"); this._fire("loadend"); }
|
|
}
|
|
_fire(type) {
|
|
const ev = { type: type, target: this, currentTarget: this, lengthComputable: false, loaded: 0, total: 0 };
|
|
const h = this["on" + type]; if (typeof h === "function") { try { h.call(this, ev); } catch (e) {} }
|
|
const ls = this._listeners[type]; if (ls) for (const fn of ls.slice()) { try { fn.call(this, ev); } catch (e) {} }
|
|
}
|
|
addEventListener(t, fn) { if (typeof fn === "function") (this._listeners[t] = this._listeners[t] || []).push(fn); }
|
|
removeEventListener(t, fn) { const ls = this._listeners[t]; if (ls) { const i = ls.indexOf(fn); if (i >= 0) ls.splice(i, 1); } }
|
|
dispatchEvent() { return true; }
|
|
};
|
|
globalThis.FileReader.EMPTY = 0; globalThis.FileReader.LOADING = 1; globalThis.FileReader.DONE = 2;
|
|
Object.assign(globalThis.FileReader.prototype, { EMPTY: 0, LOADING: 1, DONE: 2 });
|
|
}
|
|
|
|
// Real network sockets aren't implemented; we don't have a runtime WS / SSE
|
|
// client in V8. But pages that wait for an `open` event (Vite HMR clients
|
|
// embedded on docs sites, live-dashboards, anything calling
|
|
// `await new Promise(r => ws.addEventListener('open', r))`) silently hang
|
|
// forever otherwise. Fire `open` after a microtask so the consumer at least
|
|
// proceeds; subsequent messages never arrive, which is no worse than the
|
|
// current "no signal whatsoever" behaviour.
|
|
// Minimal EventTarget shared by socket-like classes. Real `EventTarget` is
|
|
// currently aliased to `Node`, which would drag DOM-tree assumptions into a
|
|
// `WebSocket`. Defining a private shim avoids that.
|
|
function _makeListenerBox(self) {
|
|
const map = new Map();
|
|
self.addEventListener = function (type, fn) {
|
|
if (typeof fn !== 'function') return;
|
|
let bucket = map.get(type);
|
|
if (!bucket) { bucket = []; map.set(type, bucket); }
|
|
bucket.push(fn);
|
|
};
|
|
self.removeEventListener = function (type, fn) {
|
|
const bucket = map.get(type);
|
|
if (!bucket) return;
|
|
const i = bucket.indexOf(fn);
|
|
if (i >= 0) bucket.splice(i, 1);
|
|
};
|
|
self.dispatchEvent = function (event) {
|
|
const bucket = map.get(event.type);
|
|
if (!bucket) return true;
|
|
for (const fn of bucket.slice()) {
|
|
try { fn.call(self, event); } catch (e) { /* swallow */ }
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
if (typeof EventSource === 'undefined') {
|
|
globalThis.EventSource = class EventSource {
|
|
constructor(url, init) {
|
|
this.url = url;
|
|
this.readyState = 0; // CONNECTING
|
|
this.withCredentials = !!(init && init.withCredentials);
|
|
this.onopen = null; this.onmessage = null; this.onerror = null;
|
|
_makeListenerBox(this);
|
|
Promise.resolve().then(() => {
|
|
if (this.readyState !== 0) return;
|
|
this.readyState = 1; // OPEN
|
|
const ev = new Event('open');
|
|
if (typeof this.onopen === 'function') { try { this.onopen(ev); } catch (e) {} }
|
|
try { this.dispatchEvent(ev); } catch (e) {}
|
|
});
|
|
}
|
|
close() { this.readyState = 2; }
|
|
static CONNECTING = 0; static OPEN = 1; static CLOSED = 2;
|
|
};
|
|
}
|
|
|
|
if (typeof WebSocket === 'undefined') {
|
|
globalThis.WebSocket = class WebSocket {
|
|
constructor(url, protocols) {
|
|
// Validate URL scheme per spec — Chrome throws SyntaxError for non-ws/wss URLs
|
|
if (typeof url !== 'string' || !/^wss?:\/\//i.test(url)) {
|
|
throw new DOMException(
|
|
"Failed to construct 'WebSocket': The URL '" + url + "' is invalid.",
|
|
'SyntaxError'
|
|
);
|
|
}
|
|
this.url = url;
|
|
this.readyState = 0; // CONNECTING
|
|
this.bufferedAmount = 0;
|
|
this.binaryType = 'blob';
|
|
this.extensions = '';
|
|
this.protocol = Array.isArray(protocols) ? (protocols[0] || '') : (protocols || '');
|
|
this.onopen = null; this.onmessage = null; this.onerror = null; this.onclose = null;
|
|
_makeListenerBox(this);
|
|
Promise.resolve().then(() => {
|
|
if (this.readyState !== 0) return;
|
|
this.readyState = 1; // OPEN
|
|
const ev = new Event('open');
|
|
if (typeof this.onopen === 'function') { try { this.onopen(ev); } catch (e) {} }
|
|
try { this.dispatchEvent(ev); } catch (e) {}
|
|
});
|
|
}
|
|
send(data) { /* drop; no real socket */ }
|
|
close(code, reason) {
|
|
if (this.readyState >= 2) return;
|
|
this.readyState = 3; // CLOSED
|
|
const ev = new Event('close');
|
|
ev.code = code || 1000; ev.reason = reason || ''; ev.wasClean = true;
|
|
if (typeof this.onclose === 'function') { try { this.onclose(ev); } catch (e) {} }
|
|
try { this.dispatchEvent(ev); } catch (e) {}
|
|
}
|
|
static CONNECTING = 0; static OPEN = 1; static CLOSING = 2; static CLOSED = 3;
|
|
};
|
|
}
|
|
|
|
if (typeof BroadcastChannel === 'undefined') {
|
|
globalThis.BroadcastChannel = class BroadcastChannel {
|
|
constructor(name) {
|
|
this.name = name; this.onmessage = null; this.onmessageerror = null;
|
|
_makeListenerBox(this);
|
|
}
|
|
postMessage(msg) {}
|
|
close() {}
|
|
};
|
|
}
|
|
|
|
if (typeof MediaQueryList === 'undefined') {
|
|
globalThis.MediaQueryList = class MediaQueryList {
|
|
constructor(q) { this.media = q || ''; this.matches = false; }
|
|
addListener() {} removeListener() {} addEventListener() {} removeEventListener() {}
|
|
};
|
|
}
|
|
|
|
if (typeof ImageData === 'undefined') {
|
|
globalThis.ImageData = class ImageData {
|
|
constructor(w, h) {
|
|
if (w instanceof Uint8ClampedArray) { this.data = w; this.width = h; this.height = w.length / (4 * h); }
|
|
else { this.width = w; this.height = h; this.data = new Uint8ClampedArray(w * h * 4); }
|
|
}
|
|
};
|
|
}
|
|
|
|
if (typeof CanvasRenderingContext2D === 'undefined') {
|
|
globalThis.CanvasRenderingContext2D = class CanvasRenderingContext2D {};
|
|
}
|
|
|
|
if (typeof OffscreenCanvas === 'undefined') {
|
|
globalThis.OffscreenCanvas = class OffscreenCanvas {
|
|
constructor(w, h) { this.width = w; this.height = h; }
|
|
getContext(type) { return globalThis.document?.createElement('canvas')?.getContext(type) || null; }
|
|
convertToBlob() { return Promise.resolve(new Blob([''])); }
|
|
transferToImageBitmap() { return {}; }
|
|
};
|
|
}
|
|
|
|
if (typeof Path2D === 'undefined') {
|
|
globalThis.Path2D = class Path2D { constructor(){} moveTo(){} lineTo(){} arc(){} rect(){} closePath(){} addPath(){} };
|
|
}
|
|
|
|
if (typeof ImageBitmap === 'undefined') {
|
|
globalThis.ImageBitmap = class ImageBitmap { constructor(){this.width=0;this.height=0;} close(){} };
|
|
globalThis.createImageBitmap = function() { return Promise.resolve(new ImageBitmap()); };
|
|
}
|
|
|
|
if (typeof Selection === 'undefined') {
|
|
globalThis.Selection = class Selection {
|
|
constructor(){this.anchorNode=null;this.focusNode=null;this.rangeCount=0;this.isCollapsed=true;this.type='None';}
|
|
getRangeAt(){return null;} collapse(){} extend(){} selectAllChildren(){} deleteFromDocument(){}
|
|
addRange(){} removeRange(){} removeAllRanges(){} toString(){return '';}
|
|
};
|
|
}
|
|
|
|
if (typeof TreeWalker === 'undefined') {
|
|
globalThis.TreeWalker = class TreeWalker {
|
|
constructor(root){this.root=root;this.currentNode=root;this.whatToShow=0xFFFFFFFF;this.filter=null;}
|
|
parentNode(){return this.currentNode?.parentNode||null;}
|
|
firstChild(){return this.currentNode?.firstChild||null;}
|
|
lastChild(){return this.currentNode?.lastChild||null;}
|
|
previousSibling(){return this.currentNode?.previousSibling||null;}
|
|
nextSibling(){return this.currentNode?.nextSibling||null;}
|
|
nextNode(){return null;} previousNode(){return null;}
|
|
};
|
|
}
|
|
|
|
if (typeof Range === 'undefined') {
|
|
globalThis.Range = class Range {
|
|
constructor(){this.startContainer=null;this.startOffset=0;this.endContainer=null;this.endOffset=0;this.collapsed=true;this.commonAncestorContainer=null;}
|
|
setStart(n,o){this.startContainer=n;this.startOffset=o;} setEnd(n,o){this.endContainer=n;this.endOffset=o;}
|
|
collapse(){} selectNode(){} selectNodeContents(){} cloneContents(){return document?.createDocumentFragment();}
|
|
deleteContents(){} insertNode(){} getBoundingClientRect(){return new DOMRect();}
|
|
getClientRects(){return new DOMRectList([]);} cloneRange(){return new Range();} toString(){return '';}
|
|
};
|
|
}
|
|
|
|
if (typeof FontFace === 'undefined') {
|
|
globalThis.FontFace = class FontFace {
|
|
constructor(family, source, descriptors={}) {
|
|
this.family = family;
|
|
this.style = descriptors.style || 'normal';
|
|
this.weight = descriptors.weight || 'normal';
|
|
this.stretch = descriptors.stretch || 'normal';
|
|
this.unicodeRange = descriptors.unicodeRange || 'U+0-10FFFF';
|
|
this.variant = descriptors.variant || 'normal';
|
|
this.featureSettings = descriptors.featureSettings || 'normal';
|
|
this.status = 'unloaded';
|
|
}
|
|
load() { this.status = 'loaded'; return Promise.resolve(this); }
|
|
};
|
|
globalThis.FontFaceSet = class FontFaceSet extends EventTarget {
|
|
constructor() { super(); this.status = 'loaded'; this.ready = Promise.resolve(this); }
|
|
add() { return this; }
|
|
check() { return true; }
|
|
clear() {}
|
|
delete() { return false; }
|
|
load() { return Promise.resolve([]); }
|
|
forEach() {}
|
|
has() { return false; }
|
|
[Symbol.iterator]() { return [][Symbol.iterator](); }
|
|
};
|
|
Object.defineProperty(Document.prototype, 'fonts', {
|
|
get() {
|
|
if (!this._fonts) this._fonts = new FontFaceSet();
|
|
return this._fonts;
|
|
},
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
if (typeof SharedWorker === 'undefined') {
|
|
globalThis.SharedWorker = class SharedWorker {
|
|
constructor() { this.port = { postMessage(){}, onmessage:null, start(){}, close(){}, addEventListener(){}, removeEventListener(){} }; this.onerror = null; }
|
|
};
|
|
}
|
|
if (typeof ServiceWorkerContainer === 'undefined') {
|
|
globalThis.ServiceWorkerContainer = class { register(){return Promise.resolve();} getRegistrations(){return Promise.resolve([]);} };
|
|
}
|
|
|
|
if (typeof URLPattern === 'undefined') {
|
|
globalThis.URLPattern = class URLPattern {
|
|
constructor(pattern){this._pattern=pattern||{};} test(){return false;} exec(){return null;}
|
|
};
|
|
}
|
|
|
|
if (typeof Document !== 'undefined' && !Document.prototype.importNode) {
|
|
Document.prototype.importNode = function(node, deep) { return node?.cloneNode(!!deep) || null; };
|
|
}
|
|
|
|
// Document.adoptNode: standard DOM (HTML living spec). Frameworks that move
|
|
// nodes between documents (portals, iframe hand-off) call it; the missing
|
|
// method throws "adoptNode is not a function". With no second document to
|
|
// transfer ownership from, the node is already ours, so return it as-is,
|
|
// matching the observable effect of adoption into this document.
|
|
if (typeof Document !== 'undefined' && !Document.prototype.adoptNode) {
|
|
Document.prototype.adoptNode = function(node) { return node || null; };
|
|
}
|
|
|
|
// Element.toggleAttribute: standard DOM. Lit/Stencil and several ad SDKs call
|
|
// it; the missing method throws. Spec semantics: no force arg toggles, force
|
|
// true adds, force false removes; returns the new presence.
|
|
if (typeof Element !== 'undefined' && !Element.prototype.toggleAttribute) {
|
|
Element.prototype.toggleAttribute = function(name, force) {
|
|
const n = String(name);
|
|
const present = this.hasAttribute(n);
|
|
const want = arguments.length < 2 ? !present : !!force;
|
|
if (want && !present) { this.setAttribute(n, ''); return true; }
|
|
if (!want && present) { this.removeAttribute(n); return false; }
|
|
return want;
|
|
};
|
|
}
|
|
|
|
// Document.elementFromPoint / elementsFromPoint — no layout engine, so this is a stub:
|
|
// in-viewport coords return <body> (or <html> as fallback), out-of-viewport returns null.
|
|
// Wrong-but-non-throwing beats "undefined", which traps ad/analytics bootstraps in retry loops
|
|
// (see issue #63).
|
|
if (typeof Document !== 'undefined' && !Document.prototype.elementFromPoint) {
|
|
// Real hit testing against the synthetic bboxes from getBoundingClientRect.
|
|
// Flat iteration over every element, NOT a tree walk: our synthetic rects
|
|
// don't form a proper containment hierarchy (a child's rect can lie far
|
|
// outside its parent's), so a tree walk that only descends into ancestors
|
|
// containing (x,y) would never reach a deep <input> inside <label><p>.
|
|
// Returns the deepest matching element (highest nid wins as a proxy for
|
|
// tree depth) so descendants beat ancestors.
|
|
Document.prototype.elementFromPoint = function(x, y) {
|
|
if (typeof x !== 'number' || typeof y !== 'number' || !isFinite(x) || !isFinite(y)) {
|
|
return null;
|
|
}
|
|
var w = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
var h = (typeof window !== 'undefined' && window.innerHeight) || 720;
|
|
if (x < 0 || y < 0 || x > w || y > h) return null;
|
|
var all = this.querySelectorAll('*');
|
|
var best = null;
|
|
var bestNid = -1;
|
|
for (var i = 0; i < all.length; i++) {
|
|
var el = all[i];
|
|
if (!el || !el.getBoundingClientRect) continue;
|
|
// documentElement / body span the viewport; skip them so we pick a
|
|
// real descendant instead of falling back to <html>/<body>.
|
|
if (el === this.documentElement || el === this.body) continue;
|
|
var r = el.getBoundingClientRect();
|
|
if (r.width === 0 || r.height === 0) continue;
|
|
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
|
var nid = el._nid | 0;
|
|
if (nid > bestNid) { best = el; bestNid = nid; }
|
|
}
|
|
}
|
|
return best || this.body || this.documentElement || null;
|
|
};
|
|
Document.prototype.elementsFromPoint = function(x, y) {
|
|
var el = this.elementFromPoint(x, y);
|
|
return el ? [el] : [];
|
|
};
|
|
}
|
|
if (typeof ShadowRoot !== 'undefined' && !ShadowRoot.prototype.elementFromPoint) {
|
|
ShadowRoot.prototype.elementFromPoint = function(x, y) {
|
|
return Document.prototype.elementFromPoint.call(globalThis.document || this, x, y);
|
|
};
|
|
ShadowRoot.prototype.elementsFromPoint = function(x, y) {
|
|
return Document.prototype.elementsFromPoint.call(globalThis.document || this, x, y);
|
|
};
|
|
}
|
|
|
|
globalThis.__obscura_init = function() {
|
|
_fpSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF >>> 0);
|
|
_fpCache = null;
|
|
// A real navigation just completed (this runs after set_url), so drop any
|
|
// URL a location setter previewed synchronously and let document_url drive
|
|
// location.href again, including any redirect target.
|
|
globalThis.__virtualUrl = null;
|
|
_installWasmStreamingFallback();
|
|
|
|
globalThis.document = new Document(+_dom("document_node_id"));
|
|
|
|
const scr = _fp('screen');
|
|
const sw = scr[0], sh = scr[1];
|
|
globalThis.screen = new Screen(sw, sh);
|
|
globalThis.visualViewport = { width:sw, height:sh-80, offsetLeft:0, offsetTop:0, scale:1, addEventListener(){}, removeEventListener(){} };
|
|
globalThis.devicePixelRatio = sw >= 2560 ? 2 : 1;
|
|
globalThis.innerWidth = sw; globalThis.innerHeight = sh - 80;
|
|
globalThis.outerWidth = sw; globalThis.outerHeight = sh - 40;
|
|
|
|
var hwValues = globalThis.__obscura_stealth ? [4, 6, 8, 12, 16] : [2, 4, 6, 8, 12, 16];
|
|
globalThis.__obscura_hw = hwValues[Math.floor(_fpRand(400) * hwValues.length)];
|
|
var memValues = globalThis.__obscura_stealth ? [4, 8] : [0.25, 0.5, 1, 2, 4, 8];
|
|
globalThis.__obscura_mem = memValues[Math.floor(_fpRand(401) * memValues.length)];
|
|
|
|
const t0 = Date.now() + Math.floor(_fpRand(641) * 100) - 50;
|
|
globalThis.performance.timeOrigin = t0;
|
|
globalThis.performance.timing = { navigationStart: t0, domContentLoadedEventEnd: t0, loadEventEnd: t0 };
|
|
var _totalHeap = 15000000 + Math.floor(_fpRand(620) * 85000000);
|
|
globalThis.performance.memory = {
|
|
jsHeapSizeLimit: 4294705152,
|
|
totalJSHeapSize: _totalHeap,
|
|
usedJSHeapSize: Math.floor(_totalHeap * (0.3 + _fpRand(621) * 0.5)),
|
|
};
|
|
globalThis.Notification.permission = "default";
|
|
|
|
// userAgentData brands and getHighEntropyValues now derive the Chrome
|
|
// version from navigator.userAgent and read the platform from the page
|
|
// globals, so every stealth surface agrees without a per-mode override.
|
|
|
|
// Hide internals (_*, obscura, Obscura). The set of keys is static at
|
|
// snapshot-build time, so we precompute it ONCE below (after this
|
|
// function definition) and reuse it on every page init. Was an
|
|
// Object.keys + filter on every navigation, ~5-40ms per page on
|
|
// SPAs that load 1000+ globals.
|
|
const toHide = globalThis.__obscura_hide_list || [];
|
|
for (let i = 0; i < toHide.length; i++) {
|
|
try { Object.defineProperty(globalThis, toHide[i], { enumerable: false }); } catch(e) {}
|
|
}
|
|
delete globalThis.__obscura_init;
|
|
};
|
|
|
|
// Snapshot-time pre-computation of the hide list. Bootstrap.js runs once
|
|
// during the V8 snapshot build (build.rs); this line captures the set of
|
|
// globals defined by bootstrap that we want to hide and stashes them
|
|
// for __obscura_init to consume on every subsequent page. The snapshot
|
|
// preserves the array as a regular global.
|
|
// Use getOwnPropertyNames, not Object.keys: the internal globals declared by
|
|
// _preHideInternals are already non-enumerable, so Object.keys would omit them
|
|
// and leave them out of the hide list (and thus visible to the reflection-API
|
|
// filter and to fingerprinting scripts). getOwnPropertyNames captures them.
|
|
globalThis.__obscura_hide_list = Object.getOwnPropertyNames(globalThis).filter(k =>
|
|
k.startsWith('_') || k.includes('obscura') || k.includes('Obscura')
|
|
);
|
|
|
|
/* ===== WPT conformance shims: batch 2 ===== */
|
|
|
|
// ---- Node namespace lookup methods ----
|
|
|
|
Node.prototype.lookupNamespaceURI = function(prefix) {
|
|
let node = this;
|
|
if (node.nodeType === 9) node = node.documentElement;
|
|
if (!node || node.nodeType !== 1) return null;
|
|
const _ns_builtins = { 'xml': 'http://www.w3.org/XML/1998/namespace', 'xmlns': 'http://www.w3.org/2000/xmlns/' };
|
|
if (prefix && _ns_builtins[prefix]) return _ns_builtins[prefix];
|
|
while (node && node.nodeType === 1) {
|
|
if (prefix) {
|
|
if (node.prefix === prefix && node.namespaceURI) return node.namespaceURI;
|
|
const nsAttr = node.getAttribute('xmlns:' + prefix);
|
|
if (nsAttr !== null) return nsAttr || null;
|
|
} else {
|
|
const defaultNs = node.getAttribute('xmlns');
|
|
if (defaultNs !== null) return defaultNs || null;
|
|
if (node.prefix === null && node.namespaceURI) return node.namespaceURI;
|
|
}
|
|
node = node.parentElement;
|
|
}
|
|
return null;
|
|
};
|
|
_markNative(Node.prototype.lookupNamespaceURI);
|
|
|
|
Node.prototype.lookupPrefix = function(namespace) {
|
|
namespace = namespace || null;
|
|
let node = this;
|
|
if (node.nodeType === 9) node = node.documentElement;
|
|
if (!node || node.nodeType !== 1) return null;
|
|
const _ns_builtins = { 'http://www.w3.org/XML/1998/namespace': 'xml', 'http://www.w3.org/2000/xmlns/': 'xmlns' };
|
|
if (_ns_builtins[namespace]) return _ns_builtins[namespace];
|
|
while (node && node.nodeType === 1) {
|
|
if (node.namespaceURI === namespace) {
|
|
const p = node.prefix;
|
|
if (p) return p;
|
|
}
|
|
const attrs = node.attributes || [];
|
|
for (let i = 0; i < attrs.length; i++) {
|
|
const attr = attrs[i];
|
|
const attrName = attr.name || attr.nodeName || '';
|
|
const attrValue = attr.value || attr.nodeValue || '';
|
|
if (attrName === 'xmlns' && attrValue === namespace) return '';
|
|
if (attrName.startsWith('xmlns:')) {
|
|
const prefix = attrName.substring(6);
|
|
if (attrValue === namespace) return prefix;
|
|
}
|
|
}
|
|
node = node.parentElement;
|
|
}
|
|
return null;
|
|
};
|
|
_markNative(Node.prototype.lookupPrefix);
|
|
|
|
Node.prototype.isDefaultNamespace = function(namespace) {
|
|
return this.lookupNamespaceURI(null) === (namespace || null);
|
|
};
|
|
_markNative(Node.prototype.isDefaultNamespace);
|
|
|
|
|
|
// ---- getElementsByTagNameNS on Element and Document ----
|
|
// getElementsByTagNameNS on Element and Document
|
|
if (!Element.prototype.getElementsByTagNameNS) {
|
|
Element.prototype.getElementsByTagNameNS = function(namespaceURI, localName) {
|
|
const all = this.querySelectorAll('*');
|
|
const filtered = [];
|
|
const nsMatch = namespaceURI === '*';
|
|
const tagMatch = localName === '*';
|
|
for (let i = 0; i < all.length; i++) {
|
|
const el = all[i];
|
|
if (!el) continue;
|
|
const elNs = el.namespaceURI;
|
|
const elTag = el.localName;
|
|
const nsOk = nsMatch || (elNs === (namespaceURI || null));
|
|
const tagOk = tagMatch || (elTag === localName);
|
|
if (nsOk && tagOk) filtered.push(el);
|
|
}
|
|
const result = new HTMLCollection(...filtered);
|
|
result.item = (i) => result[i] != null ? result[i] : null;
|
|
return result;
|
|
};
|
|
_markNative(Element.prototype.getElementsByTagNameNS);
|
|
}
|
|
if (!Document.prototype.getElementsByTagNameNS) {
|
|
Document.prototype.getElementsByTagNameNS = function(namespaceURI, localName) {
|
|
const all = this.querySelectorAll('*');
|
|
const filtered = [];
|
|
const nsMatch = namespaceURI === '*';
|
|
const tagMatch = localName === '*';
|
|
for (let i = 0; i < all.length; i++) {
|
|
const el = all[i];
|
|
if (!el) continue;
|
|
const elNs = el.namespaceURI;
|
|
const elTag = el.localName;
|
|
const nsOk = nsMatch || (elNs === (namespaceURI || null));
|
|
const tagOk = tagMatch || (elTag === localName);
|
|
if (nsOk && tagOk) filtered.push(el);
|
|
}
|
|
const result = new HTMLCollection(...filtered);
|
|
result.item = (i) => result[i] != null ? result[i] : null;
|
|
return result;
|
|
};
|
|
_markNative(Document.prototype.getElementsByTagNameNS);
|
|
}
|
|
|
|
// ---- Attr nodes and createAttribute ----
|
|
// Attr class: represents attribute nodes (nodeType 2)
|
|
if (!globalThis.Attr) {
|
|
globalThis.Attr = class Attr {
|
|
constructor(name, value = '', namespaceURI = null, prefix = null) {
|
|
this.name = name;
|
|
this.localName = name;
|
|
this.value = value;
|
|
this.namespaceURI = namespaceURI;
|
|
this.prefix = prefix;
|
|
this.ownerElement = null;
|
|
this.specified = true;
|
|
}
|
|
get nodeName() { return this.name; }
|
|
get nodeValue() { return this.value; }
|
|
set nodeValue(v) { this.value = v; }
|
|
get nodeType() { return 2; }
|
|
};
|
|
}
|
|
|
|
// XML Name validation helper for attribute/processing instruction names
|
|
const _ns_isValidXmlName = (name) => {
|
|
if (typeof name !== 'string' || !name.length) return false;
|
|
return /^[A-Za-z_:][\w.\-:]*$/.test(name);
|
|
};
|
|
|
|
const _ns_validateQualifiedName = (namespaceURI, qualifiedName) => {
|
|
const parts = qualifiedName.split(':');
|
|
if (parts.length > 2 || parts.some((part) => !_ns_isValidXmlName(part))) {
|
|
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
|
|
}
|
|
const prefix = parts.length === 2 ? parts[0] : null;
|
|
const XML = 'http://www.w3.org/XML/1998/namespace';
|
|
const XMLNS = 'http://www.w3.org/2000/xmlns/';
|
|
if ((prefix && !namespaceURI)
|
|
|| (prefix === 'xml' && namespaceURI !== XML)
|
|
|| ((qualifiedName === 'xmlns' || prefix === 'xmlns') && namespaceURI !== XMLNS)
|
|
|| (namespaceURI === XMLNS && qualifiedName !== 'xmlns' && prefix !== 'xmlns')) {
|
|
throw new DOMException('The namespace is invalid', 'NamespaceError');
|
|
}
|
|
};
|
|
|
|
// Document.prototype.createAttribute: create a detached Attr node
|
|
if (!Document.prototype.createAttribute) {
|
|
Document.prototype.createAttribute = function(localName) {
|
|
const name = String(localName || '');
|
|
if (!_ns_isValidXmlName(name)) {
|
|
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
|
|
}
|
|
return new Attr(name, '', null, null);
|
|
};
|
|
_markNative(Document.prototype.createAttribute);
|
|
}
|
|
|
|
// Document.prototype.createAttributeNS: create a namespaced Attr node
|
|
if (!Document.prototype.createAttributeNS) {
|
|
Document.prototype.createAttributeNS = function(namespaceURI, qualifiedName) {
|
|
const ns = namespaceURI ? String(namespaceURI) : null;
|
|
const qn = String(qualifiedName || '');
|
|
if (!qn.length) {
|
|
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
|
|
}
|
|
let prefix = null;
|
|
let localName = qn;
|
|
const colonIdx = qn.indexOf(':');
|
|
if (colonIdx !== -1) {
|
|
prefix = qn.substring(0, colonIdx);
|
|
localName = qn.substring(colonIdx + 1);
|
|
if (!_ns_isValidXmlName(prefix) || !_ns_isValidXmlName(localName)) {
|
|
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
|
|
}
|
|
} else {
|
|
if (!_ns_isValidXmlName(localName)) {
|
|
throw new DOMException('Invalid attribute name', 'InvalidCharacterError');
|
|
}
|
|
}
|
|
return new Attr(qn, '', ns, prefix);
|
|
};
|
|
_markNative(Document.prototype.createAttributeNS);
|
|
}
|
|
|
|
// Element.prototype.getAttributeNode: return an Attr node or null
|
|
if (!Element.prototype.getAttributeNode) {
|
|
Element.prototype.getAttributeNode = function(name) {
|
|
const val = this.getAttribute(name);
|
|
if (val === null) return null;
|
|
const attr = new Attr(name, val, null, null);
|
|
attr.ownerElement = this;
|
|
return attr;
|
|
};
|
|
_markNative(Element.prototype.getAttributeNode);
|
|
}
|
|
|
|
// Element.prototype.getAttributeNodeNS: return a namespaced Attr node or null
|
|
if (!Element.prototype.getAttributeNodeNS) {
|
|
Element.prototype.getAttributeNodeNS = function(namespaceURI, localName) {
|
|
const val = this.getAttributeNS(namespaceURI, localName);
|
|
if (val === null) return null;
|
|
const name = String(localName || '');
|
|
const attr = new Attr(name, val, namespaceURI ? String(namespaceURI) : null, null);
|
|
attr.ownerElement = this;
|
|
return attr;
|
|
};
|
|
_markNative(Element.prototype.getAttributeNodeNS);
|
|
}
|
|
|
|
// Element.prototype.setAttributeNode: set an Attr and return the previous one
|
|
if (!Element.prototype.setAttributeNode) {
|
|
Element.prototype.setAttributeNode = function(attr) {
|
|
if (!attr || typeof attr.name !== 'string') return null;
|
|
const prevVal = this.getAttribute(attr.name);
|
|
const prevAttr = prevVal !== null ? new Attr(attr.name, prevVal, null, null) : null;
|
|
if (prevAttr) prevAttr.ownerElement = this;
|
|
this.setAttribute(attr.name, attr.value);
|
|
attr.ownerElement = this;
|
|
return prevAttr;
|
|
};
|
|
_markNative(Element.prototype.setAttributeNode);
|
|
}
|
|
|
|
// Element.prototype.setAttributeNodeNS: set a namespaced Attr and return the previous one
|
|
if (!Element.prototype.setAttributeNodeNS) {
|
|
Element.prototype.setAttributeNodeNS = function(attr) {
|
|
if (!attr || typeof attr.name !== 'string') return null;
|
|
const prevVal = this.getAttribute(attr.name);
|
|
const prevAttr = prevVal !== null
|
|
? new Attr(attr.name, prevVal, attr.namespaceURI || null, attr.prefix || null)
|
|
: null;
|
|
if (prevAttr) prevAttr.ownerElement = this;
|
|
this.setAttributeNS(attr.namespaceURI || null, attr.name, attr.value);
|
|
attr.ownerElement = this;
|
|
return prevAttr;
|
|
};
|
|
_markNative(Element.prototype.setAttributeNodeNS);
|
|
}
|
|
|
|
// Element.prototype.removeAttributeNode: remove and return an Attr
|
|
if (!Element.prototype.removeAttributeNode) {
|
|
Element.prototype.removeAttributeNode = function(attr) {
|
|
if (!attr || typeof attr.name !== 'string') return attr;
|
|
const val = this.getAttribute(attr.name);
|
|
if (val !== null) {
|
|
this.removeAttribute(attr.name);
|
|
}
|
|
return attr;
|
|
};
|
|
_markNative(Element.prototype.removeAttributeNode);
|
|
}
|
|
|
|
|
|
// ---- form control validity and text selection ----
|
|
|
|
// ValidityState class for form validation state reporting
|
|
if (typeof ValidityState === 'undefined') {
|
|
globalThis.ValidityState = class ValidityState {
|
|
constructor() {
|
|
this.badInput = false;
|
|
this.customError = false;
|
|
this.patternMismatch = false;
|
|
this.rangeOverflow = false;
|
|
this.rangeUnderflow = false;
|
|
this.stepMismatch = false;
|
|
this.tooLong = false;
|
|
this.tooShort = false;
|
|
this.typeMismatch = false;
|
|
this.valueMissing = false;
|
|
this.valid = true;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Validity and validation message storage on elements
|
|
const _ns_validityCache = new WeakMap();
|
|
const _ns_customValidityMsg = new WeakMap();
|
|
|
|
// Element.prototype.validity - returns cached ValidityState for the element
|
|
if (!Element.prototype.validity) {
|
|
Object.defineProperty(Element.prototype, 'validity', {
|
|
get: function() {
|
|
if (!_ns_validityCache.has(this)) {
|
|
_ns_validityCache.set(this, new ValidityState());
|
|
}
|
|
return _ns_validityCache.get(this);
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Element.prototype.willValidate - whether element is subject to constraint validation
|
|
if (!Element.prototype.willValidate) {
|
|
Object.defineProperty(Element.prototype, 'willValidate', {
|
|
get: function() {
|
|
return true;
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Element.prototype.validationMessage - custom validation message if set
|
|
if (!Element.prototype.validationMessage) {
|
|
Object.defineProperty(Element.prototype, 'validationMessage', {
|
|
get: function() {
|
|
return _ns_customValidityMsg.get(this) || '';
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Element.prototype.checkValidity - stub returns true
|
|
if (!Element.prototype.checkValidity) {
|
|
Element.prototype.checkValidity = function checkValidity() {
|
|
return true;
|
|
};
|
|
_markNative(Element.prototype.checkValidity);
|
|
}
|
|
|
|
// Element.prototype.reportValidity - stub returns true
|
|
if (!Element.prototype.reportValidity) {
|
|
Element.prototype.reportValidity = function reportValidity() {
|
|
return true;
|
|
};
|
|
_markNative(Element.prototype.reportValidity);
|
|
}
|
|
|
|
// Element.prototype.setCustomValidity - set custom validation message
|
|
if (!Element.prototype.setCustomValidity) {
|
|
Element.prototype.setCustomValidity = function setCustomValidity(msg) {
|
|
const validity = this.validity;
|
|
if (msg && msg.length > 0) {
|
|
_ns_customValidityMsg.set(this, msg);
|
|
validity.customError = true;
|
|
validity.valid = false;
|
|
} else {
|
|
_ns_customValidityMsg.delete(this);
|
|
validity.customError = false;
|
|
validity.valid = true;
|
|
}
|
|
};
|
|
_markNative(Element.prototype.setCustomValidity);
|
|
}
|
|
|
|
// Text selection on Element.prototype
|
|
const _ns_selectionStart = new WeakMap();
|
|
const _ns_selectionEnd = new WeakMap();
|
|
const _ns_selectionDir = new WeakMap();
|
|
|
|
// Element.prototype.selectionStart - get/set selection start position
|
|
if (!Element.prototype.selectionStart) {
|
|
Object.defineProperty(Element.prototype, 'selectionStart', {
|
|
get: function() {
|
|
return _ns_selectionStart.get(this) ?? null;
|
|
},
|
|
set: function(v) {
|
|
_ns_selectionStart.set(this, v == null ? null : Math.max(0, parseInt(v, 10) || 0));
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Element.prototype.selectionEnd - get/set selection end position
|
|
if (!Element.prototype.selectionEnd) {
|
|
Object.defineProperty(Element.prototype, 'selectionEnd', {
|
|
get: function() {
|
|
return _ns_selectionEnd.get(this) ?? null;
|
|
},
|
|
set: function(v) {
|
|
_ns_selectionEnd.set(this, v == null ? null : Math.max(0, parseInt(v, 10) || 0));
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Element.prototype.selectionDirection - get/set selection direction
|
|
if (!Element.prototype.selectionDirection) {
|
|
Object.defineProperty(Element.prototype, 'selectionDirection', {
|
|
get: function() {
|
|
return _ns_selectionDir.get(this) ?? 'none';
|
|
},
|
|
set: function(v) {
|
|
_ns_selectionDir.set(this, v === 'forward' || v === 'backward' ? v : 'none');
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Element.prototype.setSelectionRange - set text selection range
|
|
if (!Element.prototype.setSelectionRange) {
|
|
Element.prototype.setSelectionRange = function setSelectionRange(start, end, direction) {
|
|
start = Math.max(0, parseInt(start, 10) || 0);
|
|
end = Math.max(0, parseInt(end, 10) || 0);
|
|
direction = direction === 'forward' || direction === 'backward' ? direction : 'none';
|
|
_ns_selectionStart.set(this, start);
|
|
_ns_selectionEnd.set(this, end);
|
|
_ns_selectionDir.set(this, direction);
|
|
};
|
|
_markNative(Element.prototype.setSelectionRange);
|
|
}
|
|
|
|
// Element.prototype.setRangeText - replace selection with text
|
|
if (!Element.prototype.setRangeText) {
|
|
Element.prototype.setRangeText = function setRangeText(replacement, start, end, selectMode) {
|
|
const val = this.value;
|
|
if (!val) return;
|
|
const strVal = String(val);
|
|
start = start === undefined ? (this.selectionStart ?? 0) : Math.max(0, parseInt(start, 10) || 0);
|
|
end = end === undefined ? (this.selectionEnd ?? 0) : Math.max(0, parseInt(end, 10) || 0);
|
|
const newValue = strVal.slice(0, start) + String(replacement) + strVal.slice(end);
|
|
this.value = newValue;
|
|
selectMode = selectMode || 'preserve';
|
|
if (selectMode === 'select') {
|
|
const replLen = String(replacement).length;
|
|
_ns_selectionStart.set(this, start);
|
|
_ns_selectionEnd.set(this, start + replLen);
|
|
_ns_selectionDir.set(this, 'none');
|
|
} else if (selectMode === 'start') {
|
|
_ns_selectionStart.set(this, start);
|
|
_ns_selectionEnd.set(this, start);
|
|
_ns_selectionDir.set(this, 'none');
|
|
} else if (selectMode === 'end') {
|
|
const replLen = String(replacement).length;
|
|
_ns_selectionStart.set(this, start + replLen);
|
|
_ns_selectionEnd.set(this, start + replLen);
|
|
_ns_selectionDir.set(this, 'none');
|
|
}
|
|
};
|
|
_markNative(Element.prototype.setRangeText);
|
|
}
|
|
|
|
// Element.prototype.select - select all text in the element
|
|
if (!Element.prototype.select) {
|
|
Element.prototype.select = function select() {
|
|
const val = this.value;
|
|
if (val === undefined || val === null) return;
|
|
const len = String(val).length;
|
|
_ns_selectionStart.set(this, 0);
|
|
_ns_selectionEnd.set(this, len);
|
|
_ns_selectionDir.set(this, 'none');
|
|
};
|
|
_markNative(Element.prototype.select);
|
|
}
|
|
|
|
|
|
// ---- Response.blob() on the real fetch path ----
|
|
|
|
if (typeof Response !== 'undefined' && Response.prototype && !Response.prototype.blob) {
|
|
Response.prototype.blob = async function() {
|
|
const bytes = await this.arrayBuffer();
|
|
const contentType = this.headers && typeof this.headers.get === 'function' ? this.headers.get('content-type') : '';
|
|
return new Blob([new Uint8Array(bytes)], { type: contentType || '' });
|
|
};
|
|
_markNative(Response.prototype.blob);
|
|
}
|
|
if (typeof Response !== 'undefined' && Response.prototype && !Response.prototype.text) {
|
|
Response.prototype.text = async function() {
|
|
const buffer = await this.arrayBuffer();
|
|
return new TextDecoder().decode(new Uint8Array(buffer));
|
|
};
|
|
_markNative(Response.prototype.text);
|
|
}
|
|
if (typeof Response !== 'undefined' && Response.prototype && !Response.prototype.json) {
|
|
Response.prototype.json = async function() {
|
|
return JSON.parse(await this.text());
|
|
};
|
|
_markNative(Response.prototype.json);
|
|
}
|
|
// arrayBuffer is the body primitive that blob/text/json derive from; the
|
|
// engine's Response provides it natively, so it is intentionally not shimmed
|
|
// here (a JS fallback could only recurse into itself).
|
|
|
|
// tamperedFunctions: obscura reimplements much of the DOM/Web platform in JS.
|
|
// Real Chrome reports "[native code]" from toString() for every builtin method,
|
|
// accessor, and constructor; any JS-backed member that leaks its source is a
|
|
// detection tell (pixelscan's tamperedFunctions check flags e.g.
|
|
// Element.prototype.nodeType, whose getter returned "get nodeType() {...}").
|
|
// Individual _markNative calls throughout this file cover methods but miss the
|
|
// property accessors and several constructors. Sweep every builtin constructor
|
|
// reachable from the global object and mark its prototype members (methods and
|
|
// accessors) plus the constructor itself native. This runs once at snapshot
|
|
// build time, so it costs nothing per page, and genuinely-native V8 builtins
|
|
// already report native, so only the JS-backed members are affected.
|
|
(function _markBuiltinsNative() {
|
|
var seen = new Set();
|
|
function walk(ctor) {
|
|
if (typeof ctor !== 'function') { return; }
|
|
_markNative(ctor);
|
|
var proto = ctor.prototype;
|
|
if (!proto || seen.has(proto)) { return; }
|
|
seen.add(proto);
|
|
var keys = Object.getOwnPropertyNames(proto);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
var d;
|
|
try { d = Object.getOwnPropertyDescriptor(proto, key); } catch (e) { continue; }
|
|
if (!d) { continue; }
|
|
if (typeof d.value === 'function') { _markNative(d.value); }
|
|
if (typeof d.get === 'function') { _markNativeAs(d.get, 'function get ' + key + '() { [native code] }'); }
|
|
if (typeof d.set === 'function') { _markNativeAs(d.set, 'function set ' + key + '() { [native code] }'); }
|
|
}
|
|
}
|
|
var names = Object.getOwnPropertyNames(globalThis);
|
|
for (var i = 0; i < names.length; i++) {
|
|
var name = names[i];
|
|
if (!/^[A-Z]/.test(name)) { continue; }
|
|
var val;
|
|
try { val = globalThis[name]; } catch (e) { continue; }
|
|
if (typeof val === 'function') { walk(val); }
|
|
}
|
|
})();
|
|
|
|
})();
|