use std::cell::RefCell;
use std::collections::VecDeque;
use std::path::Path;
use std::rc::Rc;
use std::sync::LazyLock;
use blitz_dom::BaseDocument;
use blitz_dom::NodeId;
use blitz_traits::events::{BlitzPointerId, DomEvent, DomEventData, EventState};
use boa_engine::builtins::promise::PromiseState;
use boa_engine::job::SimpleJobExecutor;
use boa_engine::object::builtins::JsPromise;
use boa_engine::object::{JsObject, ObjectInitializer};
use boa_engine::property::Attribute;
use boa_engine::value::JsValue;
use boa_engine::{
Context, JsError, JsNativeError, JsResult, JsString, Module, NativeFunction, Source, js_string,
};
use boa_gc::{Finalize, Trace};
use boa_runtime::Console;
use boa_runtime::console::{ConsoleState, DefaultLogger, Logger};
use url::Url;
use web_time::{Duration, Instant};
use crate::dom::event::{EventRef, create_event, create_event_for_dom_event, set_event_path};
use crate::dom::{define_accessor, dom_ctx, node_wrapper};
use crate::job_budget::{JobBudget, JobSlice};
use crate::module::{BlitzModuleLoader, SharedFetcher};
use crate::state::{DomCtx, Listener};
const DIAGNOSTIC_CAPACITY: usize = 1_000;
#[derive(Debug, Clone)]
#[cfg_attr(not(feature = "debug-control"), allow(dead_code))]
pub(crate) struct DiagnosticEntry {
pub sequence: u64,
pub level: String,
pub message: String,
pub stack: String,
}
#[derive(Debug, Default)]
struct RuntimeDiagnostics {
next_console_sequence: u64,
next_error_sequence: u64,
console: VecDeque<DiagnosticEntry>,
errors: VecDeque<DiagnosticEntry>,
}
impl RuntimeDiagnostics {
fn push_console(&mut self, level: &str, message: String) {
self.next_console_sequence += 1;
push_bounded(
&mut self.console,
DiagnosticEntry {
sequence: self.next_console_sequence,
level: level.into(),
message,
stack: String::new(),
},
);
}
fn push_error(&mut self, what: &str, error: &boa_engine::JsError) {
self.next_error_sequence += 1;
push_bounded(
&mut self.errors,
DiagnosticEntry {
sequence: self.next_error_sequence,
level: "error".into(),
message: format!("Uncaught JS error in {what}: {error}"),
stack: format!("{error:?}"),
},
);
}
}
fn push_bounded(queue: &mut VecDeque<DiagnosticEntry>, entry: DiagnosticEntry) {
if queue.len() == DIAGNOSTIC_CAPACITY {
queue.pop_front();
}
queue.push_back(entry);
}
#[derive(Debug, Trace, Finalize)]
struct CapturingLogger {
#[unsafe_ignore_trace]
diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
}
impl Logger for CapturingLogger {
fn log(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.diagnostics
.borrow_mut()
.push_console("log", msg.clone());
DefaultLogger.log(msg, state, context)
}
fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.diagnostics
.borrow_mut()
.push_console("info", msg.clone());
DefaultLogger.info(msg, state, context)
}
fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.diagnostics
.borrow_mut()
.push_console("warn", msg.clone());
DefaultLogger.warn(msg, state, context)
}
fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.diagnostics
.borrow_mut()
.push_console("error", msg.clone());
DefaultLogger.error(msg, state, context)
}
}
fn report_js_error(
diagnostics: &Rc<RefCell<RuntimeDiagnostics>>,
what: &str,
error: &boa_engine::JsError,
) {
diagnostics.borrow_mut().push_error(what, error);
#[cfg(feature = "tracing")]
tracing::error!("Uncaught JS error in {what}: {error}");
eprintln!("Uncaught JS error in {what}: {error}");
}
#[derive(Clone, Trace, Finalize, boa_engine::JsData)]
struct HandlerErrorReporter {
#[unsafe_ignore_trace]
diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
}
pub(crate) fn report_handler_error(context: &Context, what: &str, error: &JsError) {
if let Some(reporter) = context.get_data::<HandlerErrorReporter>() {
report_js_error(&reporter.diagnostics, what, error);
} else {
eprintln!("Uncaught JS error in {what}: {error}");
}
}
fn listener_error_context(
event_name: &str,
target: &str,
callback: &JsObject,
context: &mut Context,
) -> String {
let source = JsValue::from(callback.clone())
.to_string(context)
.map(|source| source.to_std_string_escaped())
.unwrap_or_else(|_| "<source unavailable>".to_owned());
let source = source.chars().take(500).collect::<String>();
format!("{event_name} event listener on {target}: {source}")
}
pub(crate) struct ScriptRuntime {
pub context: Context,
pub ctx: DomCtx,
diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
module_loader: Rc<BlitzModuleLoader>,
job_executor: Rc<SimpleJobExecutor>,
job_budget: Rc<JobBudget>,
pending_modules: Vec<(JsPromise, String)>,
focussed_text_value: Option<(NodeId, String)>,
domc_polls: Option<[Option<JsObject>; 2]>,
domc_generation: Option<u64>,
}
impl ScriptRuntime {
pub fn new(
doc: Rc<RefCell<BaseDocument>>,
base_url: Option<&Url>,
fetcher: SharedFetcher,
) -> Self {
let module_loader = Rc::new(BlitzModuleLoader::new(fetcher, base_url.cloned()));
let job_executor = Rc::new(SimpleJobExecutor::new());
let mut context = Context::builder()
.module_loader(Rc::clone(&module_loader))
.job_executor(Rc::clone(&job_executor))
.build()
.expect("building the script context should not fail");
let limits = context.runtime_limits_mut();
limits.set_recursion_limit(16384);
limits.set_host_recursion_limit(4096);
limits.set_stack_size_limit(16384 * 32);
let ctx = DomCtx::new(doc);
context.insert_data(ctx.clone());
let diagnostics = Rc::new(RefCell::new(RuntimeDiagnostics::default()));
context.insert_data(HandlerErrorReporter {
diagnostics: Rc::clone(&diagnostics),
});
Console::register_with_logger(
CapturingLogger {
diagnostics: Rc::clone(&diagnostics),
},
&mut context,
)
.expect("failed to register console");
boa_runtime::register_extensions(
(
boa_runtime::extensions::UrlExtension,
boa_runtime::extensions::EncodingExtension,
),
None,
&mut context,
)
.expect("failed to register URL and text-encoding globals");
crate::dom::init_protos(&ctx, &mut context);
let root_id = ctx.doc.borrow().root_node().id;
let document_wrapper = node_wrapper(&ctx, root_id, &mut context);
register_global(&mut context, "document", document_wrapper.into());
let global: JsValue = context.global_object().into();
register_global(&mut context, "window", global.clone());
register_global(&mut context, "self", global);
let global_object = context.global_object().clone();
define_accessor(
&global_object,
"innerWidth",
Some(window_inner_width),
None,
&mut context,
);
define_accessor(
&global_object,
"innerHeight",
Some(window_inner_height),
None,
&mut context,
);
define_accessor(
&global_object,
"outerWidth",
Some(window_inner_width),
None,
&mut context,
);
define_accessor(
&global_object,
"outerHeight",
Some(window_inner_height),
None,
&mut context,
);
define_accessor(
&global_object,
"devicePixelRatio",
Some(window_device_pixel_ratio),
None,
&mut context,
);
let ipc = ObjectInitializer::new(&mut context)
.function(
NativeFunction::from_fn_ptr(ipc_post_message),
js_string!("postMessage"),
1,
)
.build();
register_global(&mut context, "ipc", ipc.into());
let location = build_location(base_url, &mut context);
register_global(&mut context, "location", location);
let computed_style =
NativeFunction::from_fn_ptr(get_computed_style).to_js_function(context.realm());
register_global(&mut context, "getComputedStyle", computed_style.into());
let clipboard = ObjectInitializer::new(&mut context)
.function(
NativeFunction::from_fn_ptr(clipboard_write_text),
js_string!("writeText"),
1,
)
.function(
NativeFunction::from_fn_ptr(clipboard_read_text),
js_string!("readText"),
0,
)
.build();
let navigator = ObjectInitializer::new(&mut context)
.property(
js_string!("userAgent"),
js_string!("Mozilla/5.0 (compatible; Blitz)"),
Attribute::all(),
)
.property(js_string!("clipboard"), clipboard, Attribute::all())
.build();
register_global(&mut context, "navigator", navigator.into());
crate::dom::custom_elements::install(&ctx, &mut context);
let performance = ObjectInitializer::new(&mut context)
.function(
NativeFunction::from_fn_ptr(performance_now),
js_string!("now"),
0,
)
.build();
register_global(&mut context, "performance", performance.into());
register_global_fn(&mut context, "__blitzViewportScroll", 0, viewport_scroll);
register_global_fn(&mut context, "__blitzScrollViewport", 2, scroll_viewport);
register_global_fn(
&mut context,
"__blitzObserveMutations",
1,
observe_mutations,
);
register_global_fn(&mut context, "setTimeout", 2, set_timeout);
register_global_fn(&mut context, "clearTimeout", 1, clear_timer);
register_global_fn(&mut context, "setInterval", 2, set_interval);
register_global_fn(&mut context, "clearInterval", 1, clear_timer);
register_global_fn(
&mut context,
"requestAnimationFrame",
1,
request_animation_frame,
);
register_global_fn(&mut context, "cancelAnimationFrame", 1, clear_timer);
register_global_fn(
&mut context,
"addEventListener",
2,
window_add_event_listener,
);
register_global_fn(
&mut context,
"removeEventListener",
2,
window_remove_event_listener,
);
register_global_fn(&mut context, "dispatchEvent", 1, window_dispatch_event);
register_global_fn(
&mut context,
"__blitzDispatchHistoryEvent",
1,
window_dispatch_history_event,
);
register_global_fn(
&mut context,
"__blitzDispatchMessageEvent",
1,
window_dispatch_history_event,
);
register_global_fn(&mut context, "__blitzRandomU32", 0, random_u32);
let mut runtime = Self {
context,
ctx,
diagnostics,
module_loader,
job_executor,
job_budget: Rc::new(JobBudget::default()),
pending_modules: Vec::new(),
focussed_text_value: None,
domc_polls: None,
domc_generation: None,
};
runtime.eval_internal(
r##"
if (typeof globalThis.queueMicrotask !== "function") {
globalThis.queueMicrotask = function (callback) {
Promise.resolve().then(callback);
};
}
// `MutationObserver`, fed by the document's change log. The log
// records only while a registration exists, and `run_jobs` hands
// it to `__blitzDeliverMutations` at the end of each script turn.
// Records list plain arrays for `addedNodes`/`removedNodes`, which
// is what code iterating them with `for`, `length` or `forEach`
// needs; a live `NodeList` is not modelled.
if (typeof globalThis.MutationObserver !== "function") {
(function () {
const registrations = [];
const syncRecording = function () {
globalThis.__blitzObserveMutations(registrations.length > 0);
};
const normalize = function (options) {
const o = Object.assign({}, options || {});
if ((o.attributeOldValue || o.attributeFilter) && o.attributes === undefined) {
o.attributes = true;
}
if (o.characterDataOldValue && o.characterData === undefined) {
o.characterData = true;
}
if (!o.childList && !o.attributes && !o.characterData) {
throw new TypeError(
"Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true."
);
}
return o;
};
const within = function (ancestor, node) {
try {
return ancestor === node
|| (typeof ancestor.contains === "function" && ancestor.contains(node));
} catch (error) {
return false;
}
};
const MutationObserver = function (callback) {
if (typeof callback !== "function") {
throw new TypeError(
"Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'."
);
}
this._callback = callback;
this._records = [];
};
MutationObserver.prototype.observe = function (target, options) {
if (!target || typeof target !== "object") {
throw new TypeError(
"Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'."
);
}
const normalized = normalize(options);
const existing = registrations.find(
(r) => r.observer === this && r.target === target
);
if (existing) {
existing.options = normalized;
} else {
registrations.push({ observer: this, target: target, options: normalized });
}
syncRecording();
};
MutationObserver.prototype.disconnect = function () {
for (let i = registrations.length - 1; i >= 0; i--) {
if (registrations[i].observer === this) registrations.splice(i, 1);
}
this._records = [];
syncRecording();
};
MutationObserver.prototype.takeRecords = function () {
const records = this._records;
this._records = [];
return records;
};
globalThis.__blitzDeliverMutations = function (raw) {
const notified = [];
for (const entry of raw) {
const type = entry[0];
const target = entry[1];
const name = entry[6];
// One record per observer per change, however many
// of its registrations match.
const seen = [];
for (const r of registrations) {
if (seen.indexOf(r.observer) !== -1) continue;
const o = r.options;
if (r.target !== target && !(o.subtree && within(r.target, target))) continue;
if (type === "childList" && !o.childList) continue;
if (type === "attributes") {
if (!o.attributes) continue;
if (o.attributeFilter && o.attributeFilter.indexOf(name) === -1) continue;
}
if (type === "characterData" && !o.characterData) continue;
const keepOld = type === "attributes"
? o.attributeOldValue
: type === "characterData" && o.characterDataOldValue;
seen.push(r.observer);
r.observer._records.push({
type: type,
target: target,
addedNodes: entry[2],
removedNodes: entry[3],
previousSibling: entry[4],
nextSibling: entry[5],
attributeName: type === "attributes" ? name : null,
attributeNamespace: null,
oldValue: keepOld ? entry[7] : null
});
if (notified.indexOf(r.observer) === -1) notified.push(r.observer);
}
}
for (const observer of notified) {
const records = observer.takeRecords();
if (!records.length) continue;
try {
observer._callback.call(observer, records, observer);
} catch (error) {
setTimeout(function () { throw error; }, 0);
}
}
};
globalThis.MutationObserver = MutationObserver;
})();
}
if (typeof globalThis.structuredClone !== "function") {
const Exception = globalThis.DOMException;
const exceptionName = Object.getOwnPropertyDescriptor(Exception.prototype, "name").get;
const exceptionMessage = Object.getOwnPropertyDescriptor(Exception.prototype, "message").get;
globalThis.structuredClone = function (input, options) {
if (options && options.transfer && options.transfer.length) {
throw new Exception("structuredClone transfer is not supported", "DataCloneError");
}
const seen = new Map();
const clone = function (value) {
if (value === null || typeof value !== "object") {
if (typeof value === "function" || typeof value === "symbol") {
throw new Exception("value cannot be structured-cloned", "DataCloneError");
}
return value;
}
if (value === globalThis || typeof value.nodeType === "number") {
throw new Exception("value cannot be structured-cloned", "DataCloneError");
}
if (seen.has(value)) return seen.get(value);
let copy;
if (value instanceof Exception) {
copy = new Exception(
exceptionMessage.call(value),
exceptionName.call(value),
);
seen.set(value, copy);
return copy;
} else if (Array.isArray(value)) {
copy = [];
} else if (value instanceof Date) {
return new Date(value.getTime());
} else if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
} else if (value instanceof Map) {
copy = new Map();
seen.set(value, copy);
for (const [key, entry] of value) copy.set(clone(key), clone(entry));
return copy;
} else if (value instanceof Set) {
copy = new Set();
seen.set(value, copy);
for (const entry of value) copy.add(clone(entry));
return copy;
} else if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) {
return value.slice(0);
} else if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) {
const buffer = clone(value.buffer);
if (typeof DataView !== "undefined" && value instanceof DataView) {
return new DataView(buffer, value.byteOffset, value.byteLength);
}
return new value.constructor(buffer, value.byteOffset, value.length);
} else if (value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise ||
(typeof MessagePort === "function" && value instanceof MessagePort)) {
throw new Exception("value cannot be structured-cloned", "DataCloneError");
} else {
copy = Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype);
}
seen.set(value, copy);
for (const key of Object.keys(value)) copy[key] = clone(value[key]);
return copy;
};
return clone(input);
};
}
if (typeof globalThis.crypto !== "object" || globalThis.crypto === null) {
Object.defineProperty(globalThis, "crypto", {
value: {},
writable: false,
enumerable: true,
configurable: true,
});
}
if (typeof globalThis.crypto.getRandomValues !== "function") {
const randomU32 = globalThis.__blitzRandomU32;
Object.defineProperty(globalThis.crypto, "getRandomValues", {
value: function (array) {
if (typeof ArrayBuffer === "undefined" || !ArrayBuffer.isView(array)) {
throw new TypeError("getRandomValues requires an integer TypedArray");
}
const supported = [
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
];
if (!supported.some((Type) => array instanceof Type)) {
throw new TypeError("getRandomValues requires an integer TypedArray");
}
if (array.byteLength > 65536) {
throw new DOMException("getRandomValues quota exceeded", "QuotaExceededError");
}
for (let index = 0; index < array.length; index += 1) {
array[index] = randomU32();
}
return array;
},
writable: true,
enumerable: true,
configurable: true,
});
}
if (typeof globalThis.history !== "object" || globalThis.history === null) {
const entries = [{ state: null, url: globalThis.location.href }];
let index = 0;
const dispatchHistoryEvent = globalThis.__blitzDispatchHistoryEvent;
let applyingUrl = false;
const resolveHistoryUrl = function (url) {
if (url === undefined || url === null) return globalThis.location.href;
let resolved;
try {
resolved = new URL(String(url), globalThis.location.href);
} catch (error) {
throw new DOMException("Invalid history URL", "SecurityError");
}
const current = new URL(globalThis.location.href);
if (resolved.origin !== current.origin) {
throw new DOMException("History URL must have the document's origin", "SecurityError");
}
return resolved.href;
};
const applyUrl = function (url) {
if (url === undefined || url === null) return;
const value = String(url);
let pathname = globalThis.location.pathname;
let search = globalThis.location.search;
let hash = globalThis.location.hash;
if (value.startsWith("#")) {
hash = value;
} else if (value.startsWith("?")) {
const hashAt = value.indexOf("#");
search = hashAt < 0 ? value : value.slice(0, hashAt);
hash = hashAt < 0 ? "" : value.slice(hashAt);
} else {
let relative = value;
const schemeAt = relative.indexOf("://");
if (schemeAt >= 0) {
const pathAt = relative.indexOf("/", schemeAt + 3);
relative = pathAt < 0 ? "/" : relative.slice(pathAt);
}
if (!relative.startsWith("/")) {
const slashAt = pathname.lastIndexOf("/");
relative = pathname.slice(0, slashAt + 1) + relative;
}
const hashAt = relative.indexOf("#");
hash = hashAt < 0 ? "" : relative.slice(hashAt);
const withoutHash = hashAt < 0 ? relative : relative.slice(0, hashAt);
const searchAt = withoutHash.indexOf("?");
search = searchAt < 0 ? "" : withoutHash.slice(searchAt);
pathname = searchAt < 0 ? withoutHash : withoutHash.slice(0, searchAt);
}
globalThis.location.pathname = pathname || "/";
globalThis.location.search = search;
globalThis.location.hash = hash;
applyingUrl = true;
try {
globalThis.location.href = globalThis.location.protocol + "//" +
globalThis.location.host + globalThis.location.pathname + search + hash;
} finally {
applyingUrl = false;
}
};
const history = {
scrollRestoration: "auto",
get length() { return entries.length; },
get state() { return entries[index].state; },
pushState(state, _unused, url) {
const entry = { state: structuredClone(state), url: resolveHistoryUrl(url) };
entries.splice(index + 1, entries.length, entry);
index += 1;
applyUrl(entry.url);
},
replaceState(state, _unused, url) {
const entry = { state: structuredClone(state), url: resolveHistoryUrl(url) };
entries[index] = entry;
applyUrl(entry.url);
},
go(delta) {
const number = Number(delta === undefined ? 0 : delta);
const integer = Number.isFinite(number) ? Math.trunc(number) : 0;
const distance = (integer >>> 0) | 0;
if (distance === 0) {
globalThis.location.reload();
return;
}
setTimeout(function () {
const next = index + distance;
if (next < 0 || next >= entries.length) return;
const oldURL = globalThis.location.href;
const oldHash = globalThis.location.hash;
index = next;
applyUrl(entries[index].url);
const newURL = globalThis.location.href;
dispatchHistoryEvent(new PopStateEvent("popstate", {
state: entries[index].state,
}));
if (oldHash !== globalThis.location.hash) {
dispatchHistoryEvent(new HashChangeEvent("hashchange", {
oldURL: oldURL,
newURL: newURL,
}));
}
}, 0);
},
back() { this.go(-1); },
forward() { this.go(1); },
};
Object.defineProperty(globalThis, "history", {
value: history,
writable: false,
enumerable: true,
configurable: true,
});
// `location.href = url` navigates. A plain data property only
// stored the string, so pages that redirect by assignment never
// left. A change of fragment alone stays in this document, as a
// history entry with a hashchange; applyUrl keeps the value in sync.
let currentHref = globalThis.location.href;
Object.defineProperty(globalThis.location, "href", {
get() { return currentHref; },
set(value) {
if (applyingUrl) {
currentHref = String(value);
return;
}
const target = new URL(String(value), currentHref);
const current = new URL(currentHref);
const withoutHash = function (href) {
const hashAt = href.indexOf("#");
return hashAt < 0 ? href : href.slice(0, hashAt);
};
if (target.hash && withoutHash(target.href) === withoutHash(current.href)) {
const oldURL = currentHref;
history.pushState(null, "", target.href);
if (current.hash !== target.hash) {
dispatchHistoryEvent(new HashChangeEvent("hashchange", {
oldURL: oldURL,
newURL: currentHref,
}));
}
return;
}
globalThis.location.assign(target.href);
},
enumerable: true,
configurable: true,
});
}
const htmlConstructor = globalThis.__blitzHTMLConstructor;
const domPrototype = globalThis.__blitzDOMPrototype;
const sequenceFrom = Array.from;
const sequenceString = String;
globalThis.__blitzCEInitialize(function (value) {
if (value === null || value === undefined) {
throw new TypeError("cannot convert 'null' or 'undefined' to object");
}
const iterator = value[Symbol.iterator];
if (typeof iterator !== "function") {
throw new TypeError("not a callable function");
}
return sequenceFrom(value, sequenceString);
});
delete globalThis.__blitzCEInitialize;
const defineDomInterface = function (name, matches) {
if (typeof globalThis[name] === "function") return;
const html = name === "HTMLElement"
|| (name.startsWith("HTML") && name.endsWith("Element"));
const Interface = html ? function () {
return htmlConstructor(new.target, name);
} : function () {
throw new TypeError("Illegal constructor");
};
Object.defineProperty(Interface, "name", { value: name, configurable: true });
Interface.prototype = domPrototype(name);
Object.defineProperty(Interface.prototype, "constructor", {
value: Interface,
writable: true,
configurable: true,
});
if (html) {
Object.setPrototypeOf(Interface,
name === "HTMLElement" ? globalThis.Element : globalThis.HTMLElement);
}
Object.defineProperty(Interface, Symbol.hasInstance, {
value: html ? Function.prototype[Symbol.hasInstance] : matches,
configurable: true,
});
Object.defineProperty(globalThis, name, {
value: Interface,
writable: true,
enumerable: false,
configurable: true,
});
};
const isNode = function (value) {
return value !== null && typeof value === "object" && typeof value.nodeType === "number";
};
const isElement = function (value) { return isNode(value) && value.nodeType === 1; };
const isTag = function (tagName) {
return function (value) { return isElement(value) && value.tagName === tagName; };
};
defineDomInterface("Node", isNode);
Object.defineProperties(globalThis.Node, {
ELEMENT_NODE: { value: 1 },
ATTRIBUTE_NODE: { value: 2 },
TEXT_NODE: { value: 3 },
CDATA_SECTION_NODE: { value: 4 },
PROCESSING_INSTRUCTION_NODE: { value: 7 },
COMMENT_NODE: { value: 8 },
DOCUMENT_NODE: { value: 9 },
DOCUMENT_TYPE_NODE: { value: 10 },
DOCUMENT_FRAGMENT_NODE: { value: 11 },
DOCUMENT_POSITION_DISCONNECTED: { value: 0x01 },
DOCUMENT_POSITION_PRECEDING: { value: 0x02 },
DOCUMENT_POSITION_FOLLOWING: { value: 0x04 },
DOCUMENT_POSITION_CONTAINS: { value: 0x08 },
DOCUMENT_POSITION_CONTAINED_BY: { value: 0x10 },
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: { value: 0x20 },
});
defineDomInterface("Element", isElement);
defineDomInterface("HTMLElement", isElement);
defineDomInterface("Document", function (value) { return isNode(value) && value.nodeType === 9; });
defineDomInterface("HTMLDocument", function (value) { return isNode(value) && value.nodeType === 9; });
defineDomInterface("HTMLHeadElement", isTag("HEAD"));
defineDomInterface("HTMLBodyElement", isTag("BODY"));
defineDomInterface("HTMLAnchorElement", isTag("A"));
defineDomInterface("HTMLButtonElement", isTag("BUTTON"));
defineDomInterface("HTMLFormElement", isTag("FORM"));
defineDomInterface("HTMLImageElement", isTag("IMG"));
defineDomInterface("HTMLInputElement", isTag("INPUT"));
defineDomInterface("HTMLOptionElement", isTag("OPTION"));
defineDomInterface("HTMLScriptElement", isTag("SCRIPT"));
defineDomInterface("HTMLSelectElement", isTag("SELECT"));
defineDomInterface("HTMLStyleElement", isTag("STYLE"));
defineDomInterface("HTMLTemplateElement", isTag("TEMPLATE"));
defineDomInterface("HTMLTextAreaElement", isTag("TEXTAREA"));
delete globalThis.__blitzDispatchHistoryEvent;
defineDomInterface("HTMLDivElement", isTag("DIV"));
defineDomInterface("HTMLSpanElement", isTag("SPAN"));
defineDomInterface("HTMLParagraphElement", isTag("P"));
defineDomInterface("HTMLLIElement", isTag("LI"));
defineDomInterface("HTMLUListElement", isTag("UL"));
defineDomInterface("HTMLOListElement", isTag("OL"));
delete globalThis.__blitzHTMLConstructor;
delete globalThis.__blitzDOMPrototype;
delete globalThis.__blitzRandomU32;
"##,
"<blitz-bootstrap>",
);
crate::domc::install(&mut runtime.context);
runtime.eval_internal(include_str!("domc.js"), "<blitz-domc>");
runtime.eval_internal(include_str!("domexc.js"), "<blitz-domexc>");
let loader = Rc::clone(&runtime.module_loader);
crate::docwrite::install(&mut runtime.context, loader);
crate::fonts::install(&mut runtime.context);
runtime.eval_internal(include_str!("docwrite.js"), "<blitz-docwrite>");
runtime
}
pub fn eval(&mut self, code: &str, description: &str) {
self.eval_at(code, None, description);
}
pub fn eval_at(&mut self, code: &str, url: Option<&Url>, description: &str) {
let path = url.map(|url| url.as_str().to_owned());
let source = Source::from_bytes(code.as_bytes());
let source = match &path {
Some(path) => source.with_path(Path::new(path)),
None => source,
};
if let Err(error) = self.context.eval(source) {
report_js_error(&self.diagnostics, description, &error);
}
crate::domc::set_current_script(&self.context, None);
self.run_jobs(description);
}
pub fn set_import_map(&self, json: &str, base_url: Option<&Url>) {
self.module_loader
.set_import_map(crate::module::ImportMap::parse(json, base_url));
}
pub fn eval_module(&mut self, code: &str, url: Option<&Url>, description: &str) {
let path = url.map(|url| url.as_str().to_owned());
let source = Source::from_bytes(code.as_bytes());
let source = match &path {
Some(path) => source.with_path(Path::new(path)),
None => source,
};
let module = match Module::parse(source, None, &mut self.context) {
Ok(module) => module,
Err(error) => {
report_js_error(&self.diagnostics, description, &error);
return;
}
};
if let Some(url) = url {
self.module_loader.register(url, module.clone());
}
let promise = module.load_link_evaluate(&mut self.context);
self.run_jobs(description);
match promise.state() {
PromiseState::Fulfilled(_) => {}
PromiseState::Rejected(error) => {
report_js_error(&self.diagnostics, description, &JsError::from_opaque(error));
}
PromiseState::Pending => self.pending_modules.push((promise, description.to_owned())),
}
}
pub fn poll_module_evaluations(&mut self) {
if self.pending_modules.is_empty() {
return;
}
let mut still_pending = Vec::new();
for (promise, description) in std::mem::take(&mut self.pending_modules) {
match promise.state() {
PromiseState::Pending => still_pending.push((promise, description)),
PromiseState::Fulfilled(_) => {}
PromiseState::Rejected(error) => {
report_js_error(
&self.diagnostics,
&description,
&JsError::from_opaque(error),
);
}
}
}
self.pending_modules = still_pending;
}
pub fn eval_json(
&mut self,
code: &str,
description: &str,
) -> Result<serde_json::Value, String> {
match self.context.eval(Source::from_bytes(code)) {
Ok(value) => {
self.run_jobs(description);
value
.to_json(&mut self.context)
.map(|value| value.unwrap_or(serde_json::Value::Null))
.map_err(|error| error.to_string())
}
Err(error) => {
report_js_error(&self.diagnostics, description, &error);
Err(error.to_string())
}
}
}
#[cfg(feature = "debug-control")]
pub fn console_entries_after(&self, sequence: u64) -> Vec<DiagnosticEntry> {
self.diagnostics
.borrow()
.console
.iter()
.filter(|entry| entry.sequence > sequence)
.cloned()
.collect()
}
#[cfg(feature = "debug-control")]
pub fn runtime_errors_after(&self, sequence: u64) -> Vec<DiagnosticEntry> {
self.diagnostics
.borrow()
.errors
.iter()
.filter(|entry| entry.sequence > sequence)
.cloned()
.collect()
}
pub(crate) fn eval_internal(&mut self, code: &str, description: &str) {
let source = Source::from_bytes(code).with_path(Path::new(description));
if let Err(error) = self.context.eval(source) {
report_js_error(&self.diagnostics, description, &error);
}
}
pub(crate) fn begin_job_slice(&self) -> JobSlice {
self.job_budget.begin()
}
pub(crate) fn set_document_ready_state(&mut self, next: &'static str) -> bool {
if !crate::domc::set_ready_state(&self.context, next) {
return false;
}
let root_id = self.ctx.doc.borrow().root_node().id;
self.dispatch_node_event(root_id, "readystatechange");
true
}
pub(crate) fn has_pending_module_evaluations(&self) -> bool {
self.pending_modules
.iter()
.any(|(promise, _)| matches!(promise.state(), PromiseState::Pending))
}
pub(crate) fn has_pending_module_loads(&self) -> bool {
self.module_loader.has_pending_loads()
}
pub(crate) fn poll_domc(&mut self) -> bool {
let polls = match &self.domc_polls {
Some(polls) => polls.clone(),
None => {
let global = self.context.global_object().clone();
let polls = ["__blitzDOMCPoll", "__blitzDocwritePoll"].map(|name| {
global
.get(boa_engine::JsString::from(name), &mut self.context)
.ok()
.and_then(|value| value.as_callable())
});
self.domc_polls = Some(polls.clone());
polls
}
};
let generation = self.ctx.doc.borrow().viewport_generation();
let device_changed = self.domc_generation != Some(generation);
self.domc_generation = Some(generation);
let mut changed = false;
for (index, function) in polls.into_iter().enumerate() {
let Some(function) = function else {
continue;
};
if index == 0 && !device_changed {
continue;
}
let result = function.call(&JsValue::undefined(), &[], &mut self.context);
match result {
Ok(value) => changed |= value.to_boolean(),
Err(error) => {
report_js_error(&self.diagnostics, "document platform changes", &error);
}
}
}
changed
}
pub(crate) fn job_budget(&self) -> Duration {
self.job_budget.duration()
}
pub(crate) fn set_job_budget(&self, duration: Duration) {
self.job_budget.set_duration(duration);
}
pub(crate) fn pending_task_count(&self) -> u64 {
let due = self.ctx.state.borrow().timers.due_count(Instant::now()) as u64;
due + u64::from(self.jobs_remain())
}
pub(crate) fn jobs_remain(&self) -> bool {
self.job_executor.has_pending_jobs(&self.context)
}
pub fn run_jobs(&mut self, description: &str) {
self.run_jobs_with_progress(description);
}
pub(crate) fn run_jobs_with_progress(&mut self, description: &str) -> bool {
let _slice = self.begin_job_slice();
let before = self.job_executor.executed_job_count();
const MUTATION_DELIVERY_ROUNDS: usize = 32;
let mut delivered = false;
for _ in 0..MUTATION_DELIVERY_ROUNDS {
crate::dom::inline_handlers::sync(&self.ctx, &mut self.context);
crate::dom::custom_elements::checkpoint(&self.ctx, &mut self.context);
if let Err(error) = Rc::clone(&self.job_executor)
.run_jobs_with_budget(&mut self.context, self.job_budget.remaining())
{
report_js_error(&self.diagnostics, description, &error);
}
let mutations_delivered = self.deliver_mutations();
let toggles_delivered =
crate::dom::element::deliver_details_toggles(&self.ctx, &mut self.context);
#[cfg(feature = "shadow-dom")]
let slots_delivered =
crate::dom::shadow::deliver_slot_changes(&self.ctx, &mut self.context);
#[cfg(not(feature = "shadow-dom"))]
let slots_delivered = false;
if !mutations_delivered && !slots_delivered && !toggles_delivered {
return delivered
|| self.jobs_remain()
|| self.job_executor.executed_job_count() != before;
}
delivered = true;
if self.job_budget.exhausted() {
break;
}
}
delivered || self.jobs_remain() || self.job_executor.executed_job_count() != before
}
fn deliver_mutations(&mut self) -> bool {
let records = {
let mut doc = self.ctx.doc.borrow_mut();
if !doc.is_recording_mutations() {
return false;
}
doc.take_mutations()
};
if records.is_empty() {
return false;
}
let deliver = match self
.context
.global_object()
.get(js_string!("__blitzDeliverMutations"), &mut self.context)
{
Ok(value) => value,
Err(_) => return false,
};
let Some(deliver) = deliver.as_callable() else {
return false;
};
let values: Vec<JsValue> = records
.into_iter()
.filter_map(|record| mutation_record_value(&self.ctx, record, &mut self.context))
.collect();
let list = boa_engine::object::builtins::JsArray::from_iter(values, &mut self.context);
if let Err(error) = deliver.call(&JsValue::undefined(), &[list.into()], &mut self.context) {
report_js_error(&self.diagnostics, "MutationObserver delivery", &error);
}
true
}
pub fn next_timer_deadline(&self) -> Option<Instant> {
let dom = self.ctx.state.borrow().timers.next_deadline();
let boa = self
.job_executor
.next_job_timeout(&self.context)
.map(|delay| Instant::now() + delay);
match (dom, boa) {
(Some(dom), Some(boa)) => Some(dom.min(boa)),
(dom, boa) => dom.or(boa),
}
}
pub fn sweep_detached_nodes(&mut self) {
crate::dom::sweep_detached_nodes(&self.ctx);
}
pub fn run_due_timers(&mut self, profiling: bool) -> bool {
let timers_started = profiling.then(std::time::Instant::now);
let due = self.ctx.state.borrow_mut().timers.take_due(Instant::now());
if due.is_empty() {
return false;
}
let frame_args = [JsValue::new(TIME_ORIGIN.elapsed().as_secs_f64() * 1000.0)];
for timer in due {
let args = if timer.animation_frame {
if !self
.ctx
.state
.borrow_mut()
.timers
.begin_animation_frame_callback(timer.id)
{
continue;
}
&frame_args[..]
} else {
&timer.args[..]
};
if let Err(error) = timer
.callback
.call(&JsValue::undefined(), args, &mut self.context)
{
report_js_error(&self.diagnostics, "timer callback", &error);
}
}
self.run_jobs("timer microtasks");
if let Some(started) = timers_started {
crate::script_stats::record_work("timers", started.elapsed());
}
true
}
pub fn dispatch_dom_event(
&mut self,
chain: &[NodeId],
event: &DomEvent,
event_state: &mut EventState,
profiling: bool,
) -> bool {
let dispatch_started = profiling.then(std::time::Instant::now);
let ran = self.dispatch_dom_event_timed(chain, event, event_state);
if let Some(started) = dispatch_started {
crate::script_stats::record_work(
&format!("event:{}", event.data.name()),
started.elapsed(),
);
}
ran
}
fn dispatch_dom_event_timed(
&mut self,
chain: &[NodeId],
event: &DomEvent,
event_state: &mut EventState,
) -> bool {
let pointer_id = match &event.data {
DomEventData::PointerMove(pointer)
| DomEventData::PointerDown(pointer)
| DomEventData::PointerUp(pointer)
| DomEventData::PointerCancel(pointer) => Some(match pointer.id {
BlitzPointerId::Mouse => 1,
BlitzPointerId::Pen => 2,
BlitzPointerId::Finger(id) => id.saturating_add(3),
}),
_ => None,
};
let captured_node =
pointer_id.and_then(|id| self.ctx.state.borrow().pointer_capture.get(&id).copied());
let captured_chain = captured_node.map(|target| {
let doc = self.ctx.doc.borrow();
let mut chain = Vec::new();
let mut current = Some(target);
while let Some(node_id) = current {
chain.push(node_id);
current = doc.get_node(node_id).and_then(|node| node.parent);
}
chain
});
let chain = captured_chain.as_deref().unwrap_or(chain);
if chain.is_empty() {
return false;
}
let name = event.name().to_string();
let target: JsValue = node_wrapper(&self.ctx, chain[0], &mut self.context).into();
let event_object = create_event_for_dom_event(
&self.ctx,
&event.data,
event.bubbles,
event.cancelable,
&target,
&mut self.context,
);
let mut any_called = self.dispatch_event_inner(
chain,
&name,
event.bubbles,
|_, _, _| event_object,
event_state,
);
let synthesise_change = match &event.data {
DomEventData::Input(_) => {
self.target_is_checkbox_or_radio(event.target)
|| self.target_is_select(event.target)
}
DomEventData::Focus(_) => {
self.focussed_text_value = self
.text_control_value(event.target)
.map(|value| (event.target, value));
false
}
DomEventData::Blur(_) => match self.focussed_text_value.take() {
Some((node_id, focussed_value)) if node_id == event.target => self
.text_control_value(event.target)
.is_some_and(|current| current != focussed_value),
_ => false,
},
_ => false,
};
if synthesise_change {
let mut change_state = EventState::default();
any_called |= self.dispatch_event_inner(
chain,
"change",
true,
|ctx, target, context| create_event(ctx, "change", true, false, target, context),
&mut change_state,
);
if change_state.redraw_is_requested() {
event_state.request_redraw();
}
}
if any_called {
self.run_jobs("event microtasks");
}
if matches!(
event.data,
DomEventData::PointerUp(_) | DomEventData::PointerCancel(_)
) && let Some(pointer_id) = pointer_id
{
self.ctx
.state
.borrow_mut()
.pointer_capture
.remove(&pointer_id);
}
any_called
}
fn text_control_value(&self, node_id: NodeId) -> Option<String> {
let doc = self.ctx.doc.borrow();
doc.get_node(node_id)?
.element_data()?
.text_input_data()
.map(|input| input.editor.text().to_string())
}
fn target_is_select(&self, node_id: NodeId) -> bool {
let doc = self.ctx.doc.borrow();
doc.get_node(node_id).is_some_and(|node| {
node.data
.is_element_with_tag_name(&blitz_dom::local_name!("select"))
})
}
fn target_is_checkbox_or_radio(&self, node_id: NodeId) -> bool {
let doc = self.ctx.doc.borrow();
doc.get_node(node_id)
.and_then(|node| node.element_data())
.is_some_and(|element| {
element.name.local == blitz_dom::local_name!("input")
&& matches!(
element.attr(blitz_dom::local_name!("type")),
Some("checkbox") | Some("radio")
)
})
}
fn dispatch_event_inner(
&mut self,
chain: &[NodeId],
name: &str,
bubbles: bool,
make_event: impl FnOnce(&DomCtx, &JsValue, &mut Context) -> JsObject,
event_state: &mut EventState,
) -> bool {
let ctx = self.ctx.clone();
let context = &mut self.context;
crate::dom::inline_handlers::sync(&ctx, context);
let on_name = JsString::from(format!("on{name}"));
#[cfg(feature = "shadow-dom")]
if ctx.doc.borrow().has_shadow_roots() {
let Some(&target_id) = chain.first() else {
return false;
};
let target: JsValue = node_wrapper(&ctx, target_id, context).into();
let event_obj = make_event(&ctx, &target, context);
let composed = name.starts_with("pointer")
|| name.starts_with("mouse")
|| name.starts_with("touch")
|| name.starts_with("key")
|| matches!(
name,
"click"
| "dblclick"
| "contextmenu"
| "wheel"
| "input"
| "focus"
| "blur"
| "focusin"
| "focusout"
);
crate::dom::define_value(&event_obj, "composed", JsValue::from(composed), context);
match crate::dom::shadow_event::dispatch(&ctx, target_id, &event_obj, false, context) {
Ok(result) => {
if result.prevented {
event_state.prevent_default();
}
if result.stopped {
event_state.stop_propagation();
}
if result.called {
event_state.request_redraw();
}
return result.called;
}
Err(error) => {
report_js_error(&self.diagnostics, name, &error);
return false;
}
}
}
let window_handler = bubbles
&& context
.global_object()
.get(on_name, context)
.is_ok_and(|handler| handler.is_callable());
let may_have_listeners = {
let state = ctx.state.borrow();
let registry_hit = chain.iter().any(|node_id| {
state
.node_listeners
.get(node_id)
.and_then(|map| map.get(name))
.is_some_and(|listeners| !listeners.is_empty())
}) || state
.window_listeners
.get(name)
.is_some_and(|listeners| !listeners.is_empty());
let wrapper_hit = chain
.iter()
.any(|node_id| state.node_wrappers.contains_key(node_id));
registry_hit || wrapper_hit || window_handler
};
if !may_have_listeners {
return false;
}
let target: JsValue = node_wrapper(&ctx, chain[0], context).into();
let event_obj = make_event(&ctx, &target, context);
let _dispatch = crate::dom::event::begin_dispatch(&event_obj, false, context)
.expect("fresh native event");
crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(2), context);
let mut event_path: Vec<JsObject> = chain
.iter()
.map(|&node_id| node_wrapper(&ctx, node_id, context))
.collect();
event_path.push(context.global_object().clone());
set_event_path(&event_obj, event_path);
let event_ref = |event_obj: &JsObject, f: &dyn Fn(&EventRef) -> bool| -> bool {
event_obj
.downcast_ref::<EventRef>()
.map(|event| f(&event))
.unwrap_or(false)
};
let mut any_called = false;
'chain: for &node_id in chain {
let mut callbacks: Vec<JsObject> = Vec::new();
{
let mut state = ctx.state.borrow_mut();
if let Some(listeners) = state
.node_listeners
.get_mut(&node_id)
.and_then(|map| map.get_mut(name))
{
callbacks.extend(
listeners
.iter()
.filter_map(|listener| listener.callback.upgrade()),
);
listeners.retain(|l| !l.once);
}
}
crate::dom::node::sync_node_listener_callbacks(&ctx, node_id, context);
let current_target: JsValue = node_wrapper(&ctx, node_id, context).into();
let phase = if node_id == chain[0] { 2 } else { 3 };
crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(phase), context);
crate::dom::define_value(&event_obj, "currentTarget", current_target.clone(), context);
for callback in callbacks {
any_called = true;
if let Err(error) =
callback.call(¤t_target, &[event_obj.clone().into()], context)
{
let what = listener_error_context(
name,
&format!("node {node_id}"),
&callback,
context,
);
report_js_error(&self.diagnostics, &what, &error);
}
if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
break 'chain;
}
}
let wrapper = node_wrapper(&ctx, node_id, context);
any_called |=
crate::dom::inline_handlers::invoke(&ctx, &wrapper, &event_obj, name, context);
if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
break 'chain;
}
if !bubbles || event_ref(&event_obj, &|event| event.stopped.get()) {
break;
}
}
if bubbles && !event_ref(&event_obj, &|event| event.stopped.get()) {
let listeners: Vec<Listener> = {
let mut state = ctx.state.borrow_mut();
match state.window_listeners.get_mut(name) {
Some(listeners) => {
let cloned = listeners.clone();
listeners.retain(|l| !l.once);
cloned
}
None => Vec::new(),
}
};
let window = context.global_object().clone();
let global: JsValue = window.clone().into();
crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(3), context);
crate::dom::define_value(&event_obj, "currentTarget", global.clone(), context);
for listener in listeners {
any_called = true;
if let Err(error) =
listener
.callback
.call(&global, &[event_obj.clone().into()], context)
{
let what = listener_error_context(name, "window", &listener.callback, context);
report_js_error(&self.diagnostics, &what, &error);
}
if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
break;
}
}
if !event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
any_called |=
crate::dom::inline_handlers::invoke(&ctx, &window, &event_obj, name, context);
}
}
crate::dom::define_value(&event_obj, "currentTarget", JsValue::null(), context);
if event_ref(&event_obj, &|event| event.prevented.get()) {
event_state.prevent_default();
}
if event_ref(&event_obj, &|event| event.stopped.get()) {
event_state.stop_propagation();
}
if any_called {
event_state.request_redraw();
}
any_called
}
pub fn dispatch_document_event(&mut self, name: &str) -> bool {
let root_id = self.ctx.doc.borrow().root_node().id;
let mut event_state = EventState::default();
let ran = self.dispatch_event_inner(
&[root_id],
name,
true,
|ctx, target, context| create_event(ctx, name, true, false, target, context),
&mut event_state,
);
if ran {
self.run_jobs("event microtasks");
}
ran
}
pub fn dispatch_node_event(&mut self, node_id: NodeId, name: &str) -> bool {
let mut event_state = EventState::default();
let ran = self.dispatch_event_inner(
&[node_id],
name,
false,
|ctx, target, context| create_event(ctx, name, false, false, target, context),
&mut event_state,
);
if ran {
self.run_jobs("event microtasks");
}
ran
}
pub fn dispatch_window_event(&mut self, name: &str) -> bool {
let ctx = self.ctx.clone();
let context = &mut self.context;
let listeners: Vec<Listener> = {
let mut state = ctx.state.borrow_mut();
match state.window_listeners.get_mut(name) {
Some(listeners) => {
let cloned = listeners.clone();
listeners.retain(|l| !l.once);
cloned
}
None => Vec::new(),
}
};
let global: JsValue = context.global_object().into();
let event_obj = create_event(&ctx, name, false, false, &global, context);
crate::dom::define_value(&event_obj, "currentTarget", global.clone(), context);
let mut any_called = false;
for listener in listeners {
any_called = true;
if let Err(error) =
listener
.callback
.call(&global, &[event_obj.clone().into()], context)
{
let what = listener_error_context(name, "window", &listener.callback, context);
report_js_error(&self.diagnostics, &what, &error);
}
}
if !event_obj
.downcast_ref::<EventRef>()
.is_some_and(|event| event.stopped_immediate.get())
{
let window = context.global_object().clone();
any_called |=
crate::dom::inline_handlers::invoke(&ctx, &window, &event_obj, name, context);
}
crate::dom::define_value(&event_obj, "currentTarget", JsValue::null(), context);
if any_called {
self.run_jobs("event microtasks");
}
any_called
}
}
fn get_computed_style(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
crate::domc::get_computed_style(args, context)
}
fn observe_mutations(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let on = args.first().is_some_and(JsValue::to_boolean);
dom_ctx(context)?
.doc
.borrow_mut()
.set_recording_mutations(on);
Ok(JsValue::undefined())
}
fn mutation_record_value(
ctx: &DomCtx,
record: blitz_dom::DomMutation,
context: &mut Context,
) -> Option<JsValue> {
use blitz_dom::DomMutation;
use boa_engine::object::builtins::JsArray;
let exists = |id: NodeId| ctx.doc.borrow().get_node(id).is_some();
let node = |id: NodeId, context: &mut Context| -> JsValue {
if exists(id) {
node_wrapper(ctx, id, context).into()
} else {
JsValue::null()
}
};
let nodes = |ids: Vec<NodeId>, context: &mut Context| -> JsValue {
let values: Vec<JsValue> = ids
.into_iter()
.filter(|id| exists(*id))
.map(|id| node_wrapper(ctx, id, context).into())
.collect();
JsArray::from_iter(values, context).into()
};
let text = |value: Option<String>| -> JsValue {
value.map_or(JsValue::null(), |value| {
JsString::from(value.as_str()).into()
})
};
let fields: Vec<JsValue> = match record {
DomMutation::ChildList {
target,
added,
removed,
previous_sibling,
next_sibling,
} => {
if !exists(target) {
return None;
}
vec![
js_string!("childList").into(),
node(target, context),
nodes(added, context),
nodes(removed, context),
previous_sibling.map_or(JsValue::null(), |id| node(id, context)),
next_sibling.map_or(JsValue::null(), |id| node(id, context)),
JsValue::null(),
JsValue::null(),
]
}
DomMutation::Attributes {
target,
name,
old_value,
} => {
if !exists(target) {
return None;
}
vec![
js_string!("attributes").into(),
node(target, context),
nodes(Vec::new(), context),
nodes(Vec::new(), context),
JsValue::null(),
JsValue::null(),
text(Some(name)),
text(old_value),
]
}
DomMutation::CharacterData { target, old_value } => {
if !exists(target) {
return None;
}
vec![
js_string!("characterData").into(),
node(target, context),
nodes(Vec::new(), context),
nodes(Vec::new(), context),
JsValue::null(),
JsValue::null(),
JsValue::null(),
text(Some(old_value)),
]
}
};
Some(JsArray::from_iter(fields, context).into())
}
fn ipc_post_message(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let body = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(context)?
.to_std_string_lossy();
let handler = dom_ctx(context)?.state.borrow().ipc_handler.clone();
if let Some(handler) = handler {
handler(body);
}
Ok(JsValue::undefined())
}
fn clipboard_write_text(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let text = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(context)?
.to_std_string_lossy();
let wrote = dom_ctx(context)?
.doc
.borrow()
.shell_provider
.set_clipboard_text(text)
.is_ok();
let promise = if wrote {
JsPromise::resolve(JsValue::undefined(), context)
} else {
let error =
crate::dom_exception::error("NotAllowedError", "the clipboard is unavailable", context);
JsPromise::reject(error, context)
};
Ok(JsValue::from(promise?))
}
fn clipboard_read_text(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let read = dom_ctx(context)?
.doc
.borrow()
.shell_provider
.get_clipboard_text();
let promise = match read {
Ok(text) => JsPromise::resolve(JsValue::from(JsString::from(text)), context),
Err(_) => {
let error = crate::dom_exception::error(
"NotAllowedError",
"the clipboard is unavailable",
context,
);
JsPromise::reject(error, context)
}
};
Ok(JsValue::from(promise?))
}
static TIME_ORIGIN: LazyLock<Instant> = LazyLock::new(Instant::now);
fn performance_now(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::new(TIME_ORIGIN.elapsed().as_secs_f64() * 1000.0))
}
fn viewport_scroll(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let scroll = ctx.doc.borrow().viewport_scroll();
Ok(boa_engine::object::builtins::JsArray::from_iter(
[JsValue::new(scroll.x), JsValue::new(scroll.y)],
context,
)
.into())
}
fn scroll_viewport(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let x = args
.first()
.unwrap_or(&JsValue::undefined())
.to_number(context)?;
let y = args
.get(1)
.unwrap_or(&JsValue::undefined())
.to_number(context)?;
let x = if x.is_finite() { x } else { 0.0 };
let y = if y.is_finite() { y } else { 0.0 };
ctx.flush_layout();
let mut document = ctx.doc.borrow_mut();
let current = document.viewport_scroll();
let changed = document.scroll_viewport_by_has_changed(current.x - x, current.y - y);
if changed {
document.shell_provider.request_redraw();
}
Ok(JsValue::new(changed))
}
fn random_u32(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
getrandom::u32().map(JsValue::new).map_err(|error| {
crate::dom_exception::error(
"OperationError",
&format!("secure random generation failed: {error}"),
context,
)
})
}
fn register_global(context: &mut Context, name: &str, value: JsValue) {
context
.register_global_property(
JsString::from(name),
value,
Attribute::WRITABLE.union(Attribute::CONFIGURABLE),
)
.expect("failed to register global");
}
fn register_global_fn(
context: &mut Context,
name: &str,
length: usize,
body: fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>,
) {
context
.register_global_callable(
JsString::from(name),
length,
NativeFunction::from_fn_ptr(body),
)
.expect("failed to register global function");
}
fn build_location(base_url: Option<&Url>, context: &mut Context) -> JsValue {
let (href, protocol, host, pathname, search, hash) = match base_url {
Some(url) => (
url.to_string(),
format!("{}:", url.scheme()),
url.host_str().unwrap_or_default().to_string(),
url.path().to_string(),
url.query().map(|q| format!("?{q}")).unwrap_or_default(),
url.fragment().map(|f| format!("#{f}")).unwrap_or_default(),
),
None => (
"about:blank".to_string(),
"about:".to_string(),
String::new(),
"blank".to_string(),
String::new(),
String::new(),
),
};
ObjectInitializer::new(context)
.property(js_string!("href"), JsString::from(href), Attribute::all())
.property(
js_string!("protocol"),
JsString::from(protocol),
Attribute::all(),
)
.property(
js_string!("host"),
JsString::from(host.clone()),
Attribute::all(),
)
.property(
js_string!("hostname"),
JsString::from(host),
Attribute::all(),
)
.property(
js_string!("pathname"),
JsString::from(pathname),
Attribute::all(),
)
.property(
js_string!("search"),
JsString::from(search),
Attribute::all(),
)
.property(js_string!("hash"), JsString::from(hash), Attribute::all())
.function(
NativeFunction::from_fn_ptr(location_assign),
js_string!("assign"),
1,
)
.function(
NativeFunction::from_fn_ptr(location_assign),
js_string!("replace"),
1,
)
.function(
NativeFunction::from_fn_ptr(location_reload),
js_string!("reload"),
0,
)
.function(
NativeFunction::from_fn_ptr(location_to_string),
js_string!("toString"),
0,
)
.build()
.into()
}
fn location_assign(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let url = crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
if !ctx.doc.borrow().navigate_to_url(&url) {
return Err(crate::dom_exception::error(
"SyntaxError",
&format!("{url} is not a valid URL"),
context,
));
}
Ok(JsValue::undefined())
}
fn location_reload(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let doc = ctx.doc.borrow();
let current = doc.current_url();
doc.navigate_to_url(¤t);
Ok(JsValue::undefined())
}
fn location_to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
match this.as_object() {
Some(object) => object.get(js_string!("href"), context),
None => Ok(js_string!("").into()),
}
}
fn window_inner_width(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let viewport = ctx.doc.borrow().get_viewport();
Ok(JsValue::from(
viewport.window_size.0 as f64 / viewport.scale_f64(),
))
}
fn window_inner_height(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let viewport = ctx.doc.borrow().get_viewport();
Ok(JsValue::from(
viewport.window_size.1 as f64 / viewport.scale_f64(),
))
}
fn window_device_pixel_ratio(
_: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
Ok(JsValue::from(ctx.doc.borrow().get_viewport().scale_f64()))
}
fn timer_args(
args: &[JsValue],
context: &mut Context,
) -> JsResult<Option<(JsObject, Duration, Vec<JsValue>)>> {
let Some(callback) = args
.first()
.and_then(|value| value.as_object())
.filter(|obj| obj.is_callable())
else {
return Ok(None);
};
let delay_ms = match args.get(1) {
Some(value) => value.to_number(context)?,
None => 0.0,
};
let delay_ms = if delay_ms.is_finite() && delay_ms > 0.0 {
delay_ms
} else {
0.0
};
let rest: Vec<JsValue> = args.iter().skip(2).cloned().collect();
Ok(Some((
callback,
Duration::from_secs_f64(delay_ms / 1000.0),
rest,
)))
}
fn set_timeout(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let Some((callback, delay, rest)) = timer_args(args, context)? else {
return Ok(JsValue::from(0));
};
let id = ctx
.state
.borrow_mut()
.timers
.add(delay, None, callback, rest);
Ok(JsValue::from(id as f64))
}
fn set_interval(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let Some((callback, delay, rest)) = timer_args(args, context)? else {
return Ok(JsValue::from(0));
};
let id = ctx
.state
.borrow_mut()
.timers
.add(delay, Some(delay), callback, rest);
Ok(JsValue::from(id as f64))
}
fn request_animation_frame(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let Some(callback) = args
.first()
.and_then(|value| value.as_object())
.filter(|obj| obj.is_callable())
else {
return Ok(JsValue::from(0));
};
let id = ctx.state.borrow_mut().timers.add_animation_frame(callback);
Ok(JsValue::from(id as f64))
}
fn clear_timer(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let id = match args.first() {
Some(value) => value.to_number(context)?,
None => return Ok(JsValue::undefined()),
};
if id.is_finite() && id >= 0.0 {
ctx.state.borrow_mut().timers.remove(id as u64);
}
Ok(JsValue::undefined())
}
pub(crate) fn window_dispatch_event(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
dispatch_window_supplied_event(args, true, context)
}
fn window_dispatch_history_event(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let event = args
.first()
.and_then(JsValue::as_object)
.filter(|event| event.downcast_ref::<EventRef>().is_some())
.ok_or_else(|| JsNativeError::typ().with_message("History dispatch requires an Event"))?;
crate::dom::define_value(&event, "isTrusted", JsValue::from(true), context);
dispatch_window_supplied_event(args, false, context)
}
fn dispatch_window_supplied_event(
args: &[JsValue],
scripted: bool,
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let event = args
.first()
.and_then(JsValue::as_object)
.filter(|event| event.downcast_ref::<EventRef>().is_some())
.ok_or_else(|| JsNativeError::typ().with_message("dispatchEvent requires an Event"))?;
let _dispatch = crate::dom::event::begin_dispatch(&event, scripted, context)?;
let event_type = crate::dom::to_rust_string(&event.get(js_string!("type"), context)?, context)?;
set_event_path(&event, vec![context.global_object().clone()]);
let global: JsValue = context.global_object().into();
crate::dom::define_value(&event, "target", global.clone(), context);
crate::dom::define_value(&event, "srcElement", global.clone(), context);
crate::dom::define_value(&event, "currentTarget", global.clone(), context);
crate::dom::define_value(&event, "eventPhase", JsValue::from(2), context);
let listeners: Vec<Listener> = {
let mut state = ctx.state.borrow_mut();
match state.window_listeners.get_mut(&event_type) {
Some(listeners) => {
let cloned = listeners.clone();
listeners.retain(|listener| !listener.once);
cloned
}
None => Vec::new(),
}
};
for listener in listeners {
listener
.callback
.call(&global, &[event.clone().into()], context)?;
if event
.downcast_ref::<EventRef>()
.is_some_and(|event| event.stopped_immediate.get())
{
break;
}
}
if !event
.downcast_ref::<EventRef>()
.is_some_and(|event| event.stopped_immediate.get())
{
let window = context.global_object().clone();
crate::dom::inline_handlers::invoke(&ctx, &window, &event, &event_type, context);
}
crate::dom::define_value(&event, "currentTarget", JsValue::null(), context);
let prevented = event
.downcast_ref::<EventRef>()
.is_some_and(|event| event.prevented.get());
Ok(JsValue::from(!prevented))
}
pub(crate) fn window_add_event_listener(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let event_type =
crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let Some(callback) = args
.get(1)
.and_then(|value| value.as_object())
.filter(|obj| obj.is_callable())
else {
return Ok(JsValue::undefined());
};
let mut state = ctx.state.borrow_mut();
let listeners = state.window_listeners.entry(event_type).or_default();
if !listeners
.iter()
.any(|l| JsObject::equals(&l.callback, &callback))
{
listeners.push(Listener {
callback,
once: false,
});
}
Ok(JsValue::undefined())
}
pub(crate) fn window_remove_event_listener(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let event_type =
crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let Some(callback) = args.get(1).and_then(|value| value.as_object()) else {
return Ok(JsValue::undefined());
};
let mut state = ctx.state.borrow_mut();
if let Some(listeners) = state.window_listeners.get_mut(&event_type) {
listeners.retain(|l| !JsObject::equals(&l.callback, &callback));
}
Ok(JsValue::undefined())
}