1use std::cell::RefCell;
5use std::collections::VecDeque;
6use std::path::Path;
7use std::rc::Rc;
8use std::sync::LazyLock;
9
10use blitz_dom::BaseDocument;
11use blitz_dom::NodeId;
12use blitz_traits::events::{BlitzPointerId, DomEvent, DomEventData, EventState};
13use boa_engine::builtins::promise::PromiseState;
14use boa_engine::job::SimpleJobExecutor;
15use boa_engine::object::builtins::JsPromise;
16use boa_engine::object::{JsObject, ObjectInitializer};
17use boa_engine::property::Attribute;
18use boa_engine::value::JsValue;
19use boa_engine::{
20 Context, JsError, JsNativeError, JsResult, JsString, Module, NativeFunction, Source, js_string,
21};
22use boa_gc::{Finalize, Trace};
23use boa_runtime::Console;
24use boa_runtime::console::{ConsoleState, DefaultLogger, Logger};
25use url::Url;
26use web_time::{Duration, Instant};
27
28use crate::dom::event::{EventRef, create_event, create_event_for_dom_event, set_event_path};
29use crate::dom::{define_accessor, dom_ctx, node_wrapper};
30use crate::job_budget::{JobBudget, JobSlice};
31use crate::module::{BlitzModuleLoader, SharedFetcher};
32use crate::state::{DomCtx, Listener};
33
34const DIAGNOSTIC_CAPACITY: usize = 1_000;
35
36#[derive(Debug, Clone)]
37#[cfg_attr(not(feature = "debug-control"), allow(dead_code))]
38pub struct DiagnosticEntry {
39 pub sequence: u64,
40 pub order: u64,
41 pub level: String,
42 pub message: String,
43 pub stack: String,
44}
45
46#[derive(Debug, Clone)]
47pub struct DiagnosticStreamBatch {
48 pub entries: Vec<DiagnosticEntry>,
49 pub lost: u64,
50 pub next_sequence: u64,
51}
52
53#[derive(Debug, Clone)]
54pub struct DiagnosticBatch {
55 pub console: DiagnosticStreamBatch,
56 pub errors: DiagnosticStreamBatch,
57}
58
59#[cfg(feature = "debug-control")]
60fn diagnostic_stream(
61 queue: &VecDeque<DiagnosticEntry>,
62 after: u64,
63 limit: usize,
64) -> DiagnosticStreamBatch {
65 let lost = queue.front().map_or(0, |entry| {
66 entry.sequence.saturating_sub(after.saturating_add(1))
67 });
68 let entries: Vec<_> = queue
69 .iter()
70 .filter(|entry| entry.sequence > after)
71 .take(limit.clamp(1, DIAGNOSTIC_CAPACITY))
72 .cloned()
73 .collect();
74 let next_sequence = entries.last().map_or(after, |entry| entry.sequence);
75 DiagnosticStreamBatch {
76 entries,
77 lost,
78 next_sequence,
79 }
80}
81
82#[derive(Debug, Default)]
83struct RuntimeDiagnostics {
84 next_order: u64,
85 next_console_sequence: u64,
86 next_error_sequence: u64,
87 console: VecDeque<DiagnosticEntry>,
88 errors: VecDeque<DiagnosticEntry>,
89}
90
91impl RuntimeDiagnostics {
92 fn push_console(&mut self, level: &str, message: String) {
93 self.next_console_sequence += 1;
94 self.next_order += 1;
95 push_bounded(
96 &mut self.console,
97 DiagnosticEntry {
98 sequence: self.next_console_sequence,
99 order: self.next_order,
100 level: level.into(),
101 message,
102 stack: String::new(),
103 },
104 );
105 }
106
107 fn push_error(&mut self, what: &str, error: &boa_engine::JsError) {
108 self.next_error_sequence += 1;
109 self.next_order += 1;
110 push_bounded(
111 &mut self.errors,
112 DiagnosticEntry {
113 sequence: self.next_error_sequence,
114 order: self.next_order,
115 level: "error".into(),
116 message: format!("Uncaught JS error in {what}: {error}"),
117 stack: format!("{error:?}"),
118 },
119 );
120 }
121}
122
123fn push_bounded(queue: &mut VecDeque<DiagnosticEntry>, entry: DiagnosticEntry) {
124 if queue.len() == DIAGNOSTIC_CAPACITY {
125 queue.pop_front();
126 }
127 queue.push_back(entry);
128}
129
130#[derive(Debug, Trace, Finalize)]
131struct CapturingLogger {
132 #[unsafe_ignore_trace]
133 diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
134}
135
136impl Logger for CapturingLogger {
137 fn log(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
138 self.diagnostics
139 .borrow_mut()
140 .push_console("log", msg.clone());
141 DefaultLogger.log(msg, state, context)
142 }
143
144 fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
145 self.diagnostics
146 .borrow_mut()
147 .push_console("info", msg.clone());
148 DefaultLogger.info(msg, state, context)
149 }
150
151 fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
152 self.diagnostics
153 .borrow_mut()
154 .push_console("warn", msg.clone());
155 DefaultLogger.warn(msg, state, context)
156 }
157
158 fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
159 self.diagnostics
160 .borrow_mut()
161 .push_console("error", msg.clone());
162 DefaultLogger.error(msg, state, context)
163 }
164}
165
166fn report_js_error(
168 diagnostics: &Rc<RefCell<RuntimeDiagnostics>>,
169 what: &str,
170 error: &boa_engine::JsError,
171) {
172 diagnostics.borrow_mut().push_error(what, error);
173 #[cfg(feature = "tracing")]
174 tracing::error!("Uncaught JS error in {what}: {error}");
175 eprintln!("Uncaught JS error in {what}: {error}");
176}
177
178#[derive(Clone, Trace, Finalize, boa_engine::JsData)]
179struct HandlerErrorReporter {
180 #[unsafe_ignore_trace]
181 diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
182}
183
184pub(crate) fn report_handler_error(context: &Context, what: &str, error: &JsError) {
185 if let Some(reporter) = context.get_data::<HandlerErrorReporter>() {
186 report_js_error(&reporter.diagnostics, what, error);
187 } else {
188 eprintln!("Uncaught JS error in {what}: {error}");
189 }
190}
191
192fn listener_error_context(
193 event_name: &str,
194 target: &str,
195 callback: &JsObject,
196 context: &mut Context,
197) -> String {
198 let source = JsValue::from(callback.clone())
199 .to_string(context)
200 .map(|source| source.to_std_string_escaped())
201 .unwrap_or_else(|_| "<source unavailable>".to_owned());
202 let source = source.chars().take(500).collect::<String>();
203 format!("{event_name} event listener on {target}: {source}")
204}
205
206pub(crate) struct ScriptRuntime {
207 pub context: Context,
208 pub ctx: DomCtx,
209 diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
210 module_loader: Rc<BlitzModuleLoader>,
211 job_executor: Rc<SimpleJobExecutor>,
212 job_budget: Rc<JobBudget>,
213 pending_modules: Vec<(JsPromise, String)>,
221 focussed_text_value: Option<(NodeId, String)>,
225 domc_polls: Option<[Option<JsObject>; 2]>,
227 domc_generation: Option<u64>,
229}
230
231impl ScriptRuntime {
232 pub fn new(
233 doc: Rc<RefCell<BaseDocument>>,
234 base_url: Option<&Url>,
235 fetcher: SharedFetcher,
236 ) -> Self {
237 let module_loader = Rc::new(BlitzModuleLoader::new(fetcher, base_url.cloned()));
243 let job_executor = Rc::new(SimpleJobExecutor::new());
244 let mut context = Context::builder()
245 .module_loader(Rc::clone(&module_loader))
246 .job_executor(Rc::clone(&job_executor))
247 .build()
248 .expect("building the script context should not fail");
249 let limits = context.runtime_limits_mut();
284 limits.set_recursion_limit(16384);
285 limits.set_host_recursion_limit(4096);
286 limits.set_stack_size_limit(16384 * 32);
287
288 let ctx = DomCtx::new(doc);
289 context.insert_data(ctx.clone());
290 let diagnostics = Rc::new(RefCell::new(RuntimeDiagnostics::default()));
291 context.insert_data(HandlerErrorReporter {
292 diagnostics: Rc::clone(&diagnostics),
293 });
294
295 Console::register_with_logger(
296 CapturingLogger {
297 diagnostics: Rc::clone(&diagnostics),
298 },
299 &mut context,
300 )
301 .expect("failed to register console");
302
303 boa_runtime::register_extensions(
315 (
316 boa_runtime::extensions::UrlExtension,
317 boa_runtime::extensions::EncodingExtension,
318 ),
319 None,
320 &mut context,
321 )
322 .expect("failed to register URL and text-encoding globals");
323
324 crate::dom::init_protos(&ctx, &mut context);
325
326 let root_id = ctx.doc.borrow().root_node().id;
328 let document_wrapper = node_wrapper(&ctx, root_id, &mut context);
329 register_global(&mut context, "document", document_wrapper.into());
330
331 let global: JsValue = context.global_object().into();
333 register_global(&mut context, "window", global.clone());
334 register_global(&mut context, "self", global);
335 let global_object = context.global_object().clone();
336 define_accessor(
337 &global_object,
338 "innerWidth",
339 Some(window_inner_width),
340 None,
341 &mut context,
342 );
343 define_accessor(
344 &global_object,
345 "innerHeight",
346 Some(window_inner_height),
347 None,
348 &mut context,
349 );
350 define_accessor(
351 &global_object,
352 "outerWidth",
353 Some(window_inner_width),
354 None,
355 &mut context,
356 );
357 define_accessor(
358 &global_object,
359 "outerHeight",
360 Some(window_inner_height),
361 None,
362 &mut context,
363 );
364 define_accessor(
365 &global_object,
366 "devicePixelRatio",
367 Some(window_device_pixel_ratio),
368 None,
369 &mut context,
370 );
371
372 let ipc = ObjectInitializer::new(&mut context)
375 .function(
376 NativeFunction::from_fn_ptr(ipc_post_message),
377 js_string!("postMessage"),
378 1,
379 )
380 .build();
381 register_global(&mut context, "ipc", ipc.into());
382
383 let location = build_location(base_url, &mut context);
385 register_global(&mut context, "location", location);
386
387 let computed_style =
390 NativeFunction::from_fn_ptr(get_computed_style).to_js_function(context.realm());
391 register_global(&mut context, "getComputedStyle", computed_style.into());
392
393 let clipboard = ObjectInitializer::new(&mut context)
402 .function(
403 NativeFunction::from_fn_ptr(clipboard_write_text),
404 js_string!("writeText"),
405 1,
406 )
407 .function(
408 NativeFunction::from_fn_ptr(clipboard_read_text),
409 js_string!("readText"),
410 0,
411 )
412 .build();
413 let navigator = ObjectInitializer::new(&mut context)
414 .property(
415 js_string!("userAgent"),
416 js_string!("Mozilla/5.0 (compatible; Blitz)"),
417 Attribute::all(),
418 )
419 .property(js_string!("clipboard"), clipboard, Attribute::all())
420 .build();
421 register_global(&mut context, "navigator", navigator.into());
422
423 crate::dom::custom_elements::install(&ctx, &mut context);
424
425 let performance = ObjectInitializer::new(&mut context)
427 .function(
428 NativeFunction::from_fn_ptr(performance_now),
429 js_string!("now"),
430 0,
431 )
432 .build();
433 register_global(&mut context, "performance", performance.into());
434 register_global_fn(&mut context, "__blitzViewportScroll", 0, viewport_scroll);
435 register_global_fn(&mut context, "__blitzScrollViewport", 2, scroll_viewport);
436
437 register_global_fn(
440 &mut context,
441 "__blitzObserveMutations",
442 1,
443 observe_mutations,
444 );
445
446 register_global_fn(&mut context, "setTimeout", 2, set_timeout);
448 register_global_fn(&mut context, "clearTimeout", 1, clear_timer);
449 register_global_fn(&mut context, "setInterval", 2, set_interval);
450 register_global_fn(&mut context, "clearInterval", 1, clear_timer);
451 register_global_fn(
452 &mut context,
453 "requestAnimationFrame",
454 1,
455 request_animation_frame,
456 );
457 register_global_fn(&mut context, "cancelAnimationFrame", 1, clear_timer);
458 register_global_fn(
459 &mut context,
460 "addEventListener",
461 2,
462 window_add_event_listener,
463 );
464 register_global_fn(
465 &mut context,
466 "removeEventListener",
467 2,
468 window_remove_event_listener,
469 );
470 register_global_fn(&mut context, "dispatchEvent", 1, window_dispatch_event);
471 register_global_fn(
472 &mut context,
473 "__blitzDispatchHistoryEvent",
474 1,
475 window_dispatch_history_event,
476 );
477 register_global_fn(
478 &mut context,
479 "__blitzDispatchMessageEvent",
480 1,
481 window_dispatch_history_event,
482 );
483 register_global_fn(&mut context, "__blitzRandomU32", 0, random_u32);
484
485 let mut runtime = Self {
486 context,
487 ctx,
488 diagnostics,
489 module_loader,
490 job_executor,
491 job_budget: Rc::new(JobBudget::default()),
492 pending_modules: Vec::new(),
493 focussed_text_value: None,
494 domc_polls: None,
495 domc_generation: None,
496 };
497
498 runtime.eval_internal(
500 r##"
501 if (typeof globalThis.queueMicrotask !== "function") {
502 globalThis.queueMicrotask = function (callback) {
503 Promise.resolve().then(callback);
504 };
505 }
506 // `MutationObserver`, fed by the document's change log. The log
507 // records only while a registration exists, and `run_jobs` hands
508 // it to `__blitzDeliverMutations` at the end of each script turn.
509 // Records list plain arrays for `addedNodes`/`removedNodes`, which
510 // is what code iterating them with `for`, `length` or `forEach`
511 // needs; a live `NodeList` is not modelled.
512 if (typeof globalThis.MutationObserver !== "function") {
513 (function () {
514 const registrations = [];
515 const syncRecording = function () {
516 globalThis.__blitzObserveMutations(registrations.length > 0);
517 };
518 const normalize = function (options) {
519 const o = Object.assign({}, options || {});
520 if ((o.attributeOldValue || o.attributeFilter) && o.attributes === undefined) {
521 o.attributes = true;
522 }
523 if (o.characterDataOldValue && o.characterData === undefined) {
524 o.characterData = true;
525 }
526 if (!o.childList && !o.attributes && !o.characterData) {
527 throw new TypeError(
528 "Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true."
529 );
530 }
531 return o;
532 };
533 const within = function (ancestor, node) {
534 try {
535 return ancestor === node
536 || (typeof ancestor.contains === "function" && ancestor.contains(node));
537 } catch (error) {
538 return false;
539 }
540 };
541 const MutationObserver = function (callback) {
542 if (typeof callback !== "function") {
543 throw new TypeError(
544 "Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'."
545 );
546 }
547 this._callback = callback;
548 this._records = [];
549 };
550 MutationObserver.prototype.observe = function (target, options) {
551 if (!target || typeof target !== "object") {
552 throw new TypeError(
553 "Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'."
554 );
555 }
556 const normalized = normalize(options);
557 const existing = registrations.find(
558 (r) => r.observer === this && r.target === target
559 );
560 if (existing) {
561 existing.options = normalized;
562 } else {
563 registrations.push({ observer: this, target: target, options: normalized });
564 }
565 syncRecording();
566 };
567 MutationObserver.prototype.disconnect = function () {
568 for (let i = registrations.length - 1; i >= 0; i--) {
569 if (registrations[i].observer === this) registrations.splice(i, 1);
570 }
571 this._records = [];
572 syncRecording();
573 };
574 MutationObserver.prototype.takeRecords = function () {
575 const records = this._records;
576 this._records = [];
577 return records;
578 };
579 globalThis.__blitzDeliverMutations = function (raw) {
580 const notified = [];
581 for (const entry of raw) {
582 const type = entry[0];
583 const target = entry[1];
584 const name = entry[6];
585 // One record per observer per change, however many
586 // of its registrations match.
587 const seen = [];
588 for (const r of registrations) {
589 if (seen.indexOf(r.observer) !== -1) continue;
590 const o = r.options;
591 if (r.target !== target && !(o.subtree && within(r.target, target))) continue;
592 if (type === "childList" && !o.childList) continue;
593 if (type === "attributes") {
594 if (!o.attributes) continue;
595 if (o.attributeFilter && o.attributeFilter.indexOf(name) === -1) continue;
596 }
597 if (type === "characterData" && !o.characterData) continue;
598 const keepOld = type === "attributes"
599 ? o.attributeOldValue
600 : type === "characterData" && o.characterDataOldValue;
601 seen.push(r.observer);
602 r.observer._records.push({
603 type: type,
604 target: target,
605 addedNodes: entry[2],
606 removedNodes: entry[3],
607 previousSibling: entry[4],
608 nextSibling: entry[5],
609 attributeName: type === "attributes" ? name : null,
610 attributeNamespace: null,
611 oldValue: keepOld ? entry[7] : null
612 });
613 if (notified.indexOf(r.observer) === -1) notified.push(r.observer);
614 }
615 }
616 for (const observer of notified) {
617 const records = observer.takeRecords();
618 if (!records.length) continue;
619 try {
620 observer._callback.call(observer, records, observer);
621 } catch (error) {
622 setTimeout(function () { throw error; }, 0);
623 }
624 }
625 };
626 globalThis.MutationObserver = MutationObserver;
627 })();
628 }
629 if (typeof globalThis.structuredClone !== "function") {
630 const Exception = globalThis.DOMException;
631 const exceptionName = Object.getOwnPropertyDescriptor(Exception.prototype, "name").get;
632 const exceptionMessage = Object.getOwnPropertyDescriptor(Exception.prototype, "message").get;
633 globalThis.structuredClone = function (input, options) {
634 if (options && options.transfer && options.transfer.length) {
635 throw new Exception("structuredClone transfer is not supported", "DataCloneError");
636 }
637
638 const seen = new Map();
639 const clone = function (value) {
640 if (value === null || typeof value !== "object") {
641 if (typeof value === "function" || typeof value === "symbol") {
642 throw new Exception("value cannot be structured-cloned", "DataCloneError");
643 }
644 return value;
645 }
646 if (value === globalThis || typeof value.nodeType === "number") {
647 throw new Exception("value cannot be structured-cloned", "DataCloneError");
648 }
649 if (seen.has(value)) return seen.get(value);
650
651 let copy;
652 if (value instanceof Exception) {
653 copy = new Exception(
654 exceptionMessage.call(value),
655 exceptionName.call(value),
656 );
657 seen.set(value, copy);
658 return copy;
659 } else if (Array.isArray(value)) {
660 copy = [];
661 } else if (value instanceof Date) {
662 return new Date(value.getTime());
663 } else if (value instanceof RegExp) {
664 return new RegExp(value.source, value.flags);
665 } else if (value instanceof Map) {
666 copy = new Map();
667 seen.set(value, copy);
668 for (const [key, entry] of value) copy.set(clone(key), clone(entry));
669 return copy;
670 } else if (value instanceof Set) {
671 copy = new Set();
672 seen.set(value, copy);
673 for (const entry of value) copy.add(clone(entry));
674 return copy;
675 } else if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) {
676 return value.slice(0);
677 } else if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) {
678 const buffer = clone(value.buffer);
679 if (typeof DataView !== "undefined" && value instanceof DataView) {
680 return new DataView(buffer, value.byteOffset, value.byteLength);
681 }
682 return new value.constructor(buffer, value.byteOffset, value.length);
683 } else if (value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise ||
684 (typeof MessagePort === "function" && value instanceof MessagePort)) {
685 throw new Exception("value cannot be structured-cloned", "DataCloneError");
686 } else {
687 copy = Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype);
688 }
689
690 seen.set(value, copy);
691 for (const key of Object.keys(value)) copy[key] = clone(value[key]);
692 return copy;
693 };
694 return clone(input);
695 };
696 }
697 if (typeof globalThis.crypto !== "object" || globalThis.crypto === null) {
698 Object.defineProperty(globalThis, "crypto", {
699 value: {},
700 writable: false,
701 enumerable: true,
702 configurable: true,
703 });
704 }
705 if (typeof globalThis.crypto.getRandomValues !== "function") {
706 const randomU32 = globalThis.__blitzRandomU32;
707 Object.defineProperty(globalThis.crypto, "getRandomValues", {
708 value: function (array) {
709 if (typeof ArrayBuffer === "undefined" || !ArrayBuffer.isView(array)) {
710 throw new TypeError("getRandomValues requires an integer TypedArray");
711 }
712 const supported = [
713 Int8Array,
714 Uint8Array,
715 Uint8ClampedArray,
716 Int16Array,
717 Uint16Array,
718 Int32Array,
719 Uint32Array,
720 ];
721 if (!supported.some((Type) => array instanceof Type)) {
722 throw new TypeError("getRandomValues requires an integer TypedArray");
723 }
724 if (array.byteLength > 65536) {
725 throw new DOMException("getRandomValues quota exceeded", "QuotaExceededError");
726 }
727 for (let index = 0; index < array.length; index += 1) {
728 array[index] = randomU32();
729 }
730 return array;
731 },
732 writable: true,
733 enumerable: true,
734 configurable: true,
735 });
736 }
737 if (typeof globalThis.history !== "object" || globalThis.history === null) {
738 const entries = [{ state: null, url: globalThis.location.href }];
739 let index = 0;
740 const dispatchHistoryEvent = globalThis.__blitzDispatchHistoryEvent;
741 let applyingUrl = false;
742 const resolveHistoryUrl = function (url) {
743 if (url === undefined || url === null) return globalThis.location.href;
744 let resolved;
745 try {
746 resolved = new URL(String(url), globalThis.location.href);
747 } catch (error) {
748 throw new DOMException("Invalid history URL", "SecurityError");
749 }
750 const current = new URL(globalThis.location.href);
751 if (resolved.origin !== current.origin) {
752 throw new DOMException("History URL must have the document's origin", "SecurityError");
753 }
754 return resolved.href;
755 };
756 const applyUrl = function (url) {
757 if (url === undefined || url === null) return;
758 const value = String(url);
759 let pathname = globalThis.location.pathname;
760 let search = globalThis.location.search;
761 let hash = globalThis.location.hash;
762
763 if (value.startsWith("#")) {
764 hash = value;
765 } else if (value.startsWith("?")) {
766 const hashAt = value.indexOf("#");
767 search = hashAt < 0 ? value : value.slice(0, hashAt);
768 hash = hashAt < 0 ? "" : value.slice(hashAt);
769 } else {
770 let relative = value;
771 const schemeAt = relative.indexOf("://");
772 if (schemeAt >= 0) {
773 const pathAt = relative.indexOf("/", schemeAt + 3);
774 relative = pathAt < 0 ? "/" : relative.slice(pathAt);
775 }
776 if (!relative.startsWith("/")) {
777 const slashAt = pathname.lastIndexOf("/");
778 relative = pathname.slice(0, slashAt + 1) + relative;
779 }
780 const hashAt = relative.indexOf("#");
781 hash = hashAt < 0 ? "" : relative.slice(hashAt);
782 const withoutHash = hashAt < 0 ? relative : relative.slice(0, hashAt);
783 const searchAt = withoutHash.indexOf("?");
784 search = searchAt < 0 ? "" : withoutHash.slice(searchAt);
785 pathname = searchAt < 0 ? withoutHash : withoutHash.slice(0, searchAt);
786 }
787
788 globalThis.location.pathname = pathname || "/";
789 globalThis.location.search = search;
790 globalThis.location.hash = hash;
791 applyingUrl = true;
792 try {
793 globalThis.location.href = globalThis.location.protocol + "//" +
794 globalThis.location.host + globalThis.location.pathname + search + hash;
795 } finally {
796 applyingUrl = false;
797 }
798 };
799 const history = {
800 scrollRestoration: "auto",
801 get length() { return entries.length; },
802 get state() { return entries[index].state; },
803 pushState(state, _unused, url) {
804 const entry = { state: structuredClone(state), url: resolveHistoryUrl(url) };
805 entries.splice(index + 1, entries.length, entry);
806 index += 1;
807 applyUrl(entry.url);
808 },
809 replaceState(state, _unused, url) {
810 const entry = { state: structuredClone(state), url: resolveHistoryUrl(url) };
811 entries[index] = entry;
812 applyUrl(entry.url);
813 },
814 go(delta) {
815 const number = Number(delta === undefined ? 0 : delta);
816 const integer = Number.isFinite(number) ? Math.trunc(number) : 0;
817 const distance = (integer >>> 0) | 0;
818 if (distance === 0) {
819 globalThis.location.reload();
820 return;
821 }
822 setTimeout(function () {
823 const next = index + distance;
824 if (next < 0 || next >= entries.length) return;
825 const oldURL = globalThis.location.href;
826 const oldHash = globalThis.location.hash;
827 index = next;
828 applyUrl(entries[index].url);
829 const newURL = globalThis.location.href;
830 dispatchHistoryEvent(new PopStateEvent("popstate", {
831 state: entries[index].state,
832 }));
833 if (oldHash !== globalThis.location.hash) {
834 dispatchHistoryEvent(new HashChangeEvent("hashchange", {
835 oldURL: oldURL,
836 newURL: newURL,
837 }));
838 }
839 }, 0);
840 },
841 back() { this.go(-1); },
842 forward() { this.go(1); },
843 };
844 Object.defineProperty(globalThis, "history", {
845 value: history,
846 writable: false,
847 enumerable: true,
848 configurable: true,
849 });
850 // `location.href = url` navigates. A plain data property only
851 // stored the string, so pages that redirect by assignment never
852 // left. A change of fragment alone stays in this document, as a
853 // history entry with a hashchange; applyUrl keeps the value in sync.
854 let currentHref = globalThis.location.href;
855 Object.defineProperty(globalThis.location, "href", {
856 get() { return currentHref; },
857 set(value) {
858 if (applyingUrl) {
859 currentHref = String(value);
860 return;
861 }
862 const target = new URL(String(value), currentHref);
863 const current = new URL(currentHref);
864 const withoutHash = function (href) {
865 const hashAt = href.indexOf("#");
866 return hashAt < 0 ? href : href.slice(0, hashAt);
867 };
868 if (target.hash && withoutHash(target.href) === withoutHash(current.href)) {
869 const oldURL = currentHref;
870 history.pushState(null, "", target.href);
871 if (current.hash !== target.hash) {
872 dispatchHistoryEvent(new HashChangeEvent("hashchange", {
873 oldURL: oldURL,
874 newURL: currentHref,
875 }));
876 }
877 return;
878 }
879 globalThis.location.assign(target.href);
880 },
881 enumerable: true,
882 configurable: true,
883 });
884 }
885 const htmlConstructor = globalThis.__blitzHTMLConstructor;
886 const domPrototype = globalThis.__blitzDOMPrototype;
887 const sequenceFrom = Array.from;
888 const sequenceString = String;
889 globalThis.__blitzCEInitialize(function (value) {
890 if (value === null || value === undefined) {
891 throw new TypeError("cannot convert 'null' or 'undefined' to object");
892 }
893 const iterator = value[Symbol.iterator];
894 if (typeof iterator !== "function") {
895 throw new TypeError("not a callable function");
896 }
897 return sequenceFrom(value, sequenceString);
898 });
899 delete globalThis.__blitzCEInitialize;
900 const defineDomInterface = function (name, matches) {
901 if (typeof globalThis[name] === "function") return;
902 const html = name === "HTMLElement"
903 || (name.startsWith("HTML") && name.endsWith("Element"));
904 const Interface = html ? function () {
905 return htmlConstructor(new.target, name);
906 } : function () {
907 throw new TypeError("Illegal constructor");
908 };
909 Object.defineProperty(Interface, "name", { value: name, configurable: true });
910 Interface.prototype = domPrototype(name);
911 Object.defineProperty(Interface.prototype, "constructor", {
912 value: Interface,
913 writable: true,
914 configurable: true,
915 });
916 if (html) {
917 Object.setPrototypeOf(Interface,
918 name === "HTMLElement" ? globalThis.Element : globalThis.HTMLElement);
919 }
920 Object.defineProperty(Interface, Symbol.hasInstance, {
921 value: html ? Function.prototype[Symbol.hasInstance] : matches,
922 configurable: true,
923 });
924 Object.defineProperty(globalThis, name, {
925 value: Interface,
926 writable: true,
927 enumerable: false,
928 configurable: true,
929 });
930 };
931 const isNode = function (value) {
932 return value !== null && typeof value === "object" && typeof value.nodeType === "number";
933 };
934 const isElement = function (value) { return isNode(value) && value.nodeType === 1; };
935 const isTag = function (tagName) {
936 return function (value) { return isElement(value) && value.tagName === tagName; };
937 };
938 defineDomInterface("Node", isNode);
939 Object.defineProperties(globalThis.Node, {
940 ELEMENT_NODE: { value: 1 },
941 ATTRIBUTE_NODE: { value: 2 },
942 TEXT_NODE: { value: 3 },
943 CDATA_SECTION_NODE: { value: 4 },
944 PROCESSING_INSTRUCTION_NODE: { value: 7 },
945 COMMENT_NODE: { value: 8 },
946 DOCUMENT_NODE: { value: 9 },
947 DOCUMENT_TYPE_NODE: { value: 10 },
948 DOCUMENT_FRAGMENT_NODE: { value: 11 },
949 DOCUMENT_POSITION_DISCONNECTED: { value: 0x01 },
950 DOCUMENT_POSITION_PRECEDING: { value: 0x02 },
951 DOCUMENT_POSITION_FOLLOWING: { value: 0x04 },
952 DOCUMENT_POSITION_CONTAINS: { value: 0x08 },
953 DOCUMENT_POSITION_CONTAINED_BY: { value: 0x10 },
954 DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: { value: 0x20 },
955 });
956 defineDomInterface("Element", isElement);
957 defineDomInterface("HTMLElement", isElement);
958 defineDomInterface("Document", function (value) { return isNode(value) && value.nodeType === 9; });
959 defineDomInterface("HTMLDocument", function (value) { return isNode(value) && value.nodeType === 9; });
960 defineDomInterface("HTMLHeadElement", isTag("HEAD"));
961 defineDomInterface("HTMLBodyElement", isTag("BODY"));
962 defineDomInterface("HTMLAnchorElement", isTag("A"));
963 defineDomInterface("HTMLButtonElement", isTag("BUTTON"));
964 defineDomInterface("HTMLFormElement", isTag("FORM"));
965 defineDomInterface("HTMLImageElement", isTag("IMG"));
966 defineDomInterface("HTMLInputElement", isTag("INPUT"));
967 defineDomInterface("HTMLOptionElement", isTag("OPTION"));
968 defineDomInterface("HTMLScriptElement", isTag("SCRIPT"));
969 defineDomInterface("HTMLSelectElement", isTag("SELECT"));
970 defineDomInterface("HTMLStyleElement", isTag("STYLE"));
971 defineDomInterface("HTMLTemplateElement", isTag("TEMPLATE"));
972 defineDomInterface("HTMLTextAreaElement", isTag("TEXTAREA"));
973 delete globalThis.__blitzDispatchHistoryEvent;
974 defineDomInterface("HTMLDivElement", isTag("DIV"));
975 defineDomInterface("HTMLSpanElement", isTag("SPAN"));
976 defineDomInterface("HTMLParagraphElement", isTag("P"));
977 defineDomInterface("HTMLLIElement", isTag("LI"));
978 defineDomInterface("HTMLUListElement", isTag("UL"));
979 defineDomInterface("HTMLOListElement", isTag("OL"));
980 delete globalThis.__blitzHTMLConstructor;
981 delete globalThis.__blitzDOMPrototype;
982 delete globalThis.__blitzRandomU32;
983 "##,
984 "<blitz-bootstrap>",
985 );
986
987 crate::domc::install(&mut runtime.context);
988 runtime.eval_internal(include_str!("domc.js"), "<blitz-domc>");
989 runtime.eval_internal(include_str!("domexc.js"), "<blitz-domexc>");
990 let loader = Rc::clone(&runtime.module_loader);
991 crate::docwrite::install(&mut runtime.context, loader);
992 crate::fonts::install(&mut runtime.context);
993 runtime.eval_internal(include_str!("docwrite.js"), "<blitz-docwrite>");
994
995 runtime
996 }
997
998 pub fn eval(&mut self, code: &str, description: &str) {
1001 self.eval_at(code, None, description);
1002 }
1003
1004 pub fn eval_at(&mut self, code: &str, url: Option<&Url>, description: &str) {
1011 let path = url.map(|url| url.as_str().to_owned());
1012 let source = Source::from_bytes(code.as_bytes());
1013 let source = match &path {
1014 Some(path) => source.with_path(Path::new(path)),
1015 None => source,
1016 };
1017
1018 if let Err(error) = self.context.eval(source) {
1019 report_js_error(&self.diagnostics, description, &error);
1020 }
1021 crate::domc::set_current_script(&self.context, None);
1022 self.run_jobs(description);
1023 }
1024
1025 pub fn set_import_map(&self, json: &str, base_url: Option<&Url>) {
1031 self.module_loader
1032 .set_import_map(crate::module::ImportMap::parse(json, base_url));
1033 }
1034
1035 pub fn eval_module(&mut self, code: &str, url: Option<&Url>, description: &str) {
1042 let path = url.map(|url| url.as_str().to_owned());
1043 let source = Source::from_bytes(code.as_bytes());
1044 let source = match &path {
1045 Some(path) => source.with_path(Path::new(path)),
1046 None => source,
1047 };
1048
1049 let module = match Module::parse(source, None, &mut self.context) {
1050 Ok(module) => module,
1051 Err(error) => {
1052 report_js_error(&self.diagnostics, description, &error);
1053 return;
1054 }
1055 };
1056
1057 if let Some(url) = url {
1061 self.module_loader.register(url, module.clone());
1062 }
1063
1064 let promise = module.load_link_evaluate(&mut self.context);
1069 self.run_jobs(description);
1070
1071 match promise.state() {
1072 PromiseState::Fulfilled(_) => {}
1073 PromiseState::Rejected(error) => {
1074 report_js_error(&self.diagnostics, description, &JsError::from_opaque(error));
1075 }
1076 PromiseState::Pending => self.pending_modules.push((promise, description.to_owned())),
1081 }
1082 }
1083
1084 pub fn poll_module_evaluations(&mut self) {
1090 if self.pending_modules.is_empty() {
1091 return;
1092 }
1093
1094 let mut still_pending = Vec::new();
1095 for (promise, description) in std::mem::take(&mut self.pending_modules) {
1096 match promise.state() {
1097 PromiseState::Pending => still_pending.push((promise, description)),
1098 PromiseState::Fulfilled(_) => {}
1099 PromiseState::Rejected(error) => {
1100 report_js_error(
1101 &self.diagnostics,
1102 &description,
1103 &JsError::from_opaque(error),
1104 );
1105 }
1106 }
1107 }
1108 self.pending_modules = still_pending;
1109 }
1110
1111 pub fn eval_json(
1112 &mut self,
1113 code: &str,
1114 description: &str,
1115 ) -> Result<serde_json::Value, String> {
1116 match self.context.eval(Source::from_bytes(code)) {
1117 Ok(value) => {
1118 self.run_jobs(description);
1119 value
1120 .to_json(&mut self.context)
1121 .map(|value| value.unwrap_or(serde_json::Value::Null))
1122 .map_err(|error| error.to_string())
1123 }
1124 Err(error) => {
1125 report_js_error(&self.diagnostics, description, &error);
1126 Err(error.to_string())
1127 }
1128 }
1129 }
1130
1131 #[cfg(feature = "debug-control")]
1132 pub fn console_entries_after(&self, sequence: u64) -> Vec<DiagnosticEntry> {
1133 self.diagnostics
1134 .borrow()
1135 .console
1136 .iter()
1137 .filter(|entry| entry.sequence > sequence)
1138 .cloned()
1139 .collect()
1140 }
1141
1142 #[cfg(feature = "debug-control")]
1143 pub fn runtime_errors_after(&self, sequence: u64) -> Vec<DiagnosticEntry> {
1144 self.diagnostics
1145 .borrow()
1146 .errors
1147 .iter()
1148 .filter(|entry| entry.sequence > sequence)
1149 .cloned()
1150 .collect()
1151 }
1152
1153 #[cfg(feature = "debug-control")]
1154 pub fn diagnostic_batch(
1155 &self,
1156 console_after: u64,
1157 errors_after: u64,
1158 limit: usize,
1159 ) -> DiagnosticBatch {
1160 let diagnostics = self.diagnostics.borrow();
1161 DiagnosticBatch {
1162 console: diagnostic_stream(&diagnostics.console, console_after, limit),
1163 errors: diagnostic_stream(&diagnostics.errors, errors_after, limit),
1164 }
1165 }
1166
1167 pub(crate) fn eval_internal(&mut self, code: &str, description: &str) {
1168 let source = Source::from_bytes(code).with_path(Path::new(description));
1171 if let Err(error) = self.context.eval(source) {
1172 report_js_error(&self.diagnostics, description, &error);
1173 }
1174 }
1175
1176 pub(crate) fn begin_job_slice(&self) -> JobSlice {
1177 self.job_budget.begin()
1178 }
1179
1180 pub(crate) fn set_document_ready_state(&mut self, next: &'static str) -> bool {
1181 if !crate::domc::set_ready_state(&self.context, next) {
1182 return false;
1183 }
1184 let root_id = self.ctx.doc.borrow().root_node().id;
1185 self.dispatch_node_event(root_id, "readystatechange");
1186 true
1187 }
1188
1189 pub(crate) fn has_pending_module_evaluations(&self) -> bool {
1190 self.pending_modules
1191 .iter()
1192 .any(|(promise, _)| matches!(promise.state(), PromiseState::Pending))
1193 }
1194
1195 pub(crate) fn has_pending_module_loads(&self) -> bool {
1196 self.module_loader.has_pending_loads()
1197 }
1198
1199 pub(crate) fn poll_domc(&mut self) -> bool {
1200 let polls = match &self.domc_polls {
1201 Some(polls) => polls.clone(),
1202 None => {
1203 let global = self.context.global_object().clone();
1204 let polls = ["__blitzDOMCPoll", "__blitzDocwritePoll"].map(|name| {
1205 global
1206 .get(boa_engine::JsString::from(name), &mut self.context)
1207 .ok()
1208 .and_then(|value| value.as_callable())
1209 });
1210 self.domc_polls = Some(polls.clone());
1211 polls
1212 }
1213 };
1214 let generation = self.ctx.doc.borrow().viewport_generation();
1218 let device_changed = self.domc_generation != Some(generation);
1219 self.domc_generation = Some(generation);
1220 let mut changed = false;
1221 for (index, function) in polls.into_iter().enumerate() {
1222 let Some(function) = function else {
1223 continue;
1224 };
1225 if index == 0 && !device_changed {
1226 continue;
1227 }
1228 let result = function.call(&JsValue::undefined(), &[], &mut self.context);
1229 match result {
1230 Ok(value) => changed |= value.to_boolean(),
1231 Err(error) => {
1232 report_js_error(&self.diagnostics, "document platform changes", &error);
1233 }
1234 }
1235 }
1236 changed
1237 }
1238
1239 pub(crate) fn job_budget(&self) -> Duration {
1240 self.job_budget.duration()
1241 }
1242
1243 pub(crate) fn set_job_budget(&self, duration: Duration) {
1244 self.job_budget.set_duration(duration);
1245 }
1246
1247 pub(crate) fn pending_task_count(&self) -> u64 {
1251 let due = self.ctx.state.borrow().timers.due_count(Instant::now()) as u64;
1252 due + u64::from(self.jobs_remain())
1253 }
1254
1255 pub(crate) fn jobs_remain(&self) -> bool {
1256 self.job_executor.has_pending_jobs(&self.context)
1257 }
1258
1259 pub fn run_jobs(&mut self, description: &str) {
1261 self.run_jobs_with_progress(description);
1262 }
1263
1264 pub(crate) fn run_jobs_with_progress(&mut self, description: &str) -> bool {
1271 let _slice = self.begin_job_slice();
1272 let before = self.job_executor.executed_job_count();
1273 const MUTATION_DELIVERY_ROUNDS: usize = 32;
1274 let mut delivered = false;
1275 for _ in 0..MUTATION_DELIVERY_ROUNDS {
1276 crate::dom::inline_handlers::sync(&self.ctx, &mut self.context);
1277 crate::dom::custom_elements::checkpoint(&self.ctx, &mut self.context);
1278 if let Err(error) = Rc::clone(&self.job_executor)
1279 .run_jobs_with_budget(&mut self.context, self.job_budget.remaining())
1280 {
1281 report_js_error(&self.diagnostics, description, &error);
1282 }
1283 let mutations_delivered = self.deliver_mutations();
1284 let toggles_delivered =
1285 crate::dom::element::deliver_details_toggles(&self.ctx, &mut self.context);
1286 #[cfg(feature = "shadow-dom")]
1287 let slots_delivered =
1288 crate::dom::shadow::deliver_slot_changes(&self.ctx, &mut self.context);
1289 #[cfg(not(feature = "shadow-dom"))]
1290 let slots_delivered = false;
1291 if !mutations_delivered && !slots_delivered && !toggles_delivered {
1292 return delivered
1293 || self.jobs_remain()
1294 || self.job_executor.executed_job_count() != before;
1295 }
1296 delivered = true;
1297 if self.job_budget.exhausted() {
1298 break;
1299 }
1300 }
1301 delivered || self.jobs_remain() || self.job_executor.executed_job_count() != before
1302 }
1303
1304 fn deliver_mutations(&mut self) -> bool {
1307 let records = {
1308 let mut doc = self.ctx.doc.borrow_mut();
1309 if !doc.is_recording_mutations() {
1310 return false;
1311 }
1312 doc.take_mutations()
1313 };
1314 if records.is_empty() {
1315 return false;
1316 }
1317 let deliver = match self
1318 .context
1319 .global_object()
1320 .get(js_string!("__blitzDeliverMutations"), &mut self.context)
1321 {
1322 Ok(value) => value,
1323 Err(_) => return false,
1324 };
1325 let Some(deliver) = deliver.as_callable() else {
1326 return false;
1327 };
1328 let values: Vec<JsValue> = records
1329 .into_iter()
1330 .filter_map(|record| mutation_record_value(&self.ctx, record, &mut self.context))
1331 .collect();
1332 let list = boa_engine::object::builtins::JsArray::from_iter(values, &mut self.context);
1333 if let Err(error) = deliver.call(&JsValue::undefined(), &[list.into()], &mut self.context) {
1334 report_js_error(&self.diagnostics, "MutationObserver delivery", &error);
1335 }
1336 true
1337 }
1338
1339 pub fn next_timer_deadline(&self) -> Option<Instant> {
1341 let dom = self.ctx.state.borrow().timers.next_deadline();
1342 let boa = self
1343 .job_executor
1344 .next_job_timeout(&self.context)
1345 .map(|delay| Instant::now() + delay);
1346 match (dom, boa) {
1347 (Some(dom), Some(boa)) => Some(dom.min(boa)),
1348 (dom, boa) => dom.or(boa),
1349 }
1350 }
1351
1352 pub fn sweep_detached_nodes(&mut self) {
1359 crate::dom::sweep_detached_nodes(&self.ctx);
1360 }
1361
1362 pub fn run_due_timers(&mut self, profiling: bool) -> bool {
1364 let timers_started = profiling.then(std::time::Instant::now);
1365 let due = self.ctx.state.borrow_mut().timers.take_due(Instant::now());
1366 if due.is_empty() {
1367 return false;
1368 }
1369 let frame_args = [JsValue::new(TIME_ORIGIN.elapsed().as_secs_f64() * 1000.0)];
1372 for timer in due {
1373 let args = if timer.animation_frame {
1374 if !self
1375 .ctx
1376 .state
1377 .borrow_mut()
1378 .timers
1379 .begin_animation_frame_callback(timer.id)
1380 {
1381 continue;
1382 }
1383 &frame_args[..]
1384 } else {
1385 &timer.args[..]
1386 };
1387 if let Err(error) = timer
1388 .callback
1389 .call(&JsValue::undefined(), args, &mut self.context)
1390 {
1391 report_js_error(&self.diagnostics, "timer callback", &error);
1392 }
1393 }
1394 self.run_jobs("timer microtasks");
1395 if let Some(started) = timers_started {
1396 crate::script_stats::record_work("timers", started.elapsed());
1397 }
1398 true
1399 }
1400
1401 pub fn dispatch_dom_event(
1406 &mut self,
1407 chain: &[NodeId],
1408 event: &DomEvent,
1409 event_state: &mut EventState,
1410 profiling: bool,
1411 ) -> bool {
1412 let dispatch_started = profiling.then(std::time::Instant::now);
1415 let ran = self.dispatch_dom_event_timed(chain, event, event_state);
1416 if let Some(started) = dispatch_started {
1417 crate::script_stats::record_work(
1418 &format!("event:{}", event.data.name()),
1419 started.elapsed(),
1420 );
1421 }
1422 ran
1423 }
1424
1425 fn dispatch_dom_event_timed(
1426 &mut self,
1427 chain: &[NodeId],
1428 event: &DomEvent,
1429 event_state: &mut EventState,
1430 ) -> bool {
1431 let pointer_id = match &event.data {
1432 DomEventData::PointerMove(pointer)
1433 | DomEventData::PointerDown(pointer)
1434 | DomEventData::PointerUp(pointer)
1435 | DomEventData::PointerCancel(pointer) => Some(match pointer.id {
1436 BlitzPointerId::Mouse => 1,
1437 BlitzPointerId::Pen => 2,
1438 BlitzPointerId::Finger(id) => id.saturating_add(3),
1439 }),
1440 _ => None,
1441 };
1442 let captured_node =
1443 pointer_id.and_then(|id| self.ctx.state.borrow().pointer_capture.get(&id).copied());
1444 let captured_chain = captured_node.map(|target| {
1445 let doc = self.ctx.doc.borrow();
1446 let mut chain = Vec::new();
1447 let mut current = Some(target);
1448 while let Some(node_id) = current {
1449 chain.push(node_id);
1450 current = doc.get_node(node_id).and_then(|node| node.parent);
1451 }
1452 chain
1453 });
1454 let chain = captured_chain.as_deref().unwrap_or(chain);
1455
1456 if chain.is_empty() {
1457 return false;
1458 }
1459 let name = event.name().to_string();
1460 let target: JsValue = node_wrapper(&self.ctx, chain[0], &mut self.context).into();
1462 let event_object = create_event_for_dom_event(
1463 &self.ctx,
1464 &event.data,
1465 event.bubbles,
1466 event.cancelable,
1467 &target,
1468 &mut self.context,
1469 );
1470 let mut any_called = self.dispatch_event_inner(
1471 chain,
1472 &name,
1473 event.bubbles,
1474 |_, _, _| event_object,
1475 event_state,
1476 );
1477
1478 let synthesise_change = match &event.data {
1487 DomEventData::Input(_) => {
1488 self.target_is_checkbox_or_radio(event.target)
1491 || self.target_is_select(event.target)
1492 }
1493 DomEventData::Focus(_) => {
1494 self.focussed_text_value = self
1495 .text_control_value(event.target)
1496 .map(|value| (event.target, value));
1497 false
1498 }
1499 DomEventData::Blur(_) => match self.focussed_text_value.take() {
1500 Some((node_id, focussed_value)) if node_id == event.target => self
1501 .text_control_value(event.target)
1502 .is_some_and(|current| current != focussed_value),
1503 _ => false,
1504 },
1505 _ => false,
1506 };
1507 if synthesise_change {
1508 let mut change_state = EventState::default();
1509 any_called |= self.dispatch_event_inner(
1510 chain,
1511 "change",
1512 true,
1513 |ctx, target, context| create_event(ctx, "change", true, false, target, context),
1514 &mut change_state,
1515 );
1516 if change_state.redraw_is_requested() {
1517 event_state.request_redraw();
1518 }
1519 }
1520
1521 if any_called {
1522 self.run_jobs("event microtasks");
1523 }
1524
1525 if matches!(
1526 event.data,
1527 DomEventData::PointerUp(_) | DomEventData::PointerCancel(_)
1528 ) && let Some(pointer_id) = pointer_id
1529 {
1530 self.ctx
1531 .state
1532 .borrow_mut()
1533 .pointer_capture
1534 .remove(&pointer_id);
1535 }
1536
1537 any_called
1538 }
1539
1540 fn text_control_value(&self, node_id: NodeId) -> Option<String> {
1543 let doc = self.ctx.doc.borrow();
1544 doc.get_node(node_id)?
1545 .element_data()?
1546 .text_input_data()
1547 .map(|input| input.editor.text().to_string())
1548 }
1549
1550 fn target_is_select(&self, node_id: NodeId) -> bool {
1551 let doc = self.ctx.doc.borrow();
1552 doc.get_node(node_id).is_some_and(|node| {
1553 node.data
1554 .is_element_with_tag_name(&blitz_dom::local_name!("select"))
1555 })
1556 }
1557
1558 fn target_is_checkbox_or_radio(&self, node_id: NodeId) -> bool {
1559 let doc = self.ctx.doc.borrow();
1560 doc.get_node(node_id)
1561 .and_then(|node| node.element_data())
1562 .is_some_and(|element| {
1563 element.name.local == blitz_dom::local_name!("input")
1564 && matches!(
1565 element.attr(blitz_dom::local_name!("type")),
1566 Some("checkbox") | Some("radio")
1567 )
1568 })
1569 }
1570
1571 fn dispatch_event_inner(
1574 &mut self,
1575 chain: &[NodeId],
1576 name: &str,
1577 bubbles: bool,
1578 make_event: impl FnOnce(&DomCtx, &JsValue, &mut Context) -> JsObject,
1579 event_state: &mut EventState,
1580 ) -> bool {
1581 let ctx = self.ctx.clone();
1582 let context = &mut self.context;
1583 crate::dom::inline_handlers::sync(&ctx, context);
1584 let on_name = JsString::from(format!("on{name}"));
1585
1586 #[cfg(feature = "shadow-dom")]
1587 if ctx.doc.borrow().has_shadow_roots() {
1588 let Some(&target_id) = chain.first() else {
1589 return false;
1590 };
1591 let target: JsValue = node_wrapper(&ctx, target_id, context).into();
1592 let event_obj = make_event(&ctx, &target, context);
1593 let composed = name.starts_with("pointer")
1594 || name.starts_with("mouse")
1595 || name.starts_with("touch")
1596 || name.starts_with("key")
1597 || matches!(
1598 name,
1599 "click"
1600 | "dblclick"
1601 | "contextmenu"
1602 | "wheel"
1603 | "input"
1604 | "focus"
1605 | "blur"
1606 | "focusin"
1607 | "focusout"
1608 );
1609 crate::dom::define_value(&event_obj, "composed", JsValue::from(composed), context);
1610 match crate::dom::shadow_event::dispatch(&ctx, target_id, &event_obj, false, context) {
1611 Ok(result) => {
1612 if result.prevented {
1613 event_state.prevent_default();
1614 }
1615 if result.stopped {
1616 event_state.stop_propagation();
1617 }
1618 if result.called {
1619 event_state.request_redraw();
1620 }
1621 return result.called;
1622 }
1623 Err(error) => {
1624 report_js_error(&self.diagnostics, name, &error);
1625 return false;
1626 }
1627 }
1628 }
1629
1630 let window_handler = bubbles
1632 && context
1633 .global_object()
1634 .get(on_name, context)
1635 .is_ok_and(|handler| handler.is_callable());
1636 let may_have_listeners = {
1637 let state = ctx.state.borrow();
1638 let registry_hit = chain.iter().any(|node_id| {
1639 state
1640 .node_listeners
1641 .get(node_id)
1642 .and_then(|map| map.get(name))
1643 .is_some_and(|listeners| !listeners.is_empty())
1644 }) || state
1645 .window_listeners
1646 .get(name)
1647 .is_some_and(|listeners| !listeners.is_empty());
1648 let wrapper_hit = chain
1650 .iter()
1651 .any(|node_id| state.node_wrappers.contains_key(node_id));
1652 registry_hit || wrapper_hit || window_handler
1653 };
1654 if !may_have_listeners {
1655 return false;
1656 }
1657
1658 let target: JsValue = node_wrapper(&ctx, chain[0], context).into();
1659 let event_obj = make_event(&ctx, &target, context);
1660 let _dispatch = crate::dom::event::begin_dispatch(&event_obj, false, context)
1661 .expect("fresh native event");
1662 crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(2), context);
1663 let mut event_path: Vec<JsObject> = chain
1664 .iter()
1665 .map(|&node_id| node_wrapper(&ctx, node_id, context))
1666 .collect();
1667 event_path.push(context.global_object().clone());
1668 set_event_path(&event_obj, event_path);
1669 let event_ref = |event_obj: &JsObject, f: &dyn Fn(&EventRef) -> bool| -> bool {
1670 event_obj
1671 .downcast_ref::<EventRef>()
1672 .map(|event| f(&event))
1673 .unwrap_or(false)
1674 };
1675
1676 let mut any_called = false;
1677
1678 'chain: for &node_id in chain {
1679 let mut callbacks: Vec<(JsObject, bool)> = Vec::new();
1682 {
1683 let mut state = ctx.state.borrow_mut();
1684 if let Some(listeners) = state
1685 .node_listeners
1686 .get_mut(&node_id)
1687 .and_then(|map| map.get_mut(name))
1688 {
1689 callbacks.extend(listeners.iter().filter_map(|listener| {
1690 listener
1691 .callback
1692 .upgrade()
1693 .map(|callback| (callback, listener.passive))
1694 }));
1695 listeners.retain(|l| !l.once);
1697 }
1698 }
1699 crate::dom::node::sync_node_listener_callbacks(&ctx, node_id, context);
1700
1701 let current_target: JsValue = node_wrapper(&ctx, node_id, context).into();
1702 let phase = if node_id == chain[0] { 2 } else { 3 };
1703 crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(phase), context);
1704 crate::dom::define_value(&event_obj, "currentTarget", current_target.clone(), context);
1705
1706 for (callback, passive) in callbacks {
1707 any_called = true;
1708 if let Err(error) = crate::dom::passive::invoke(
1709 &callback,
1710 ¤t_target,
1711 &event_obj,
1712 passive,
1713 context,
1714 ) {
1715 let what = listener_error_context(
1716 name,
1717 &format!("node {node_id}"),
1718 &callback,
1719 context,
1720 );
1721 report_js_error(&self.diagnostics, &what, &error);
1722 }
1723 if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1724 break 'chain;
1725 }
1726 }
1727
1728 let wrapper = node_wrapper(&ctx, node_id, context);
1729 any_called |=
1730 crate::dom::inline_handlers::invoke(&ctx, &wrapper, &event_obj, name, context);
1731 if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1732 break 'chain;
1733 }
1734 if !bubbles || event_ref(&event_obj, &|event| event.stopped.get()) {
1735 break;
1736 }
1737 }
1738
1739 if bubbles && !event_ref(&event_obj, &|event| event.stopped.get()) {
1741 let listeners: Vec<Listener> = {
1742 let mut state = ctx.state.borrow_mut();
1743 match state.window_listeners.get_mut(name) {
1744 Some(listeners) => {
1745 let cloned = listeners.clone();
1746 listeners.retain(|l| !l.once);
1747 cloned
1748 }
1749 None => Vec::new(),
1750 }
1751 };
1752 let window = context.global_object().clone();
1753 let global: JsValue = window.clone().into();
1754 crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(3), context);
1755 crate::dom::define_value(&event_obj, "currentTarget", global.clone(), context);
1756 for listener in listeners {
1757 any_called = true;
1758 if let Err(error) = crate::dom::passive::invoke(
1759 &listener.callback,
1760 &global,
1761 &event_obj,
1762 listener.passive,
1763 context,
1764 ) {
1765 let what = listener_error_context(name, "window", &listener.callback, context);
1766 report_js_error(&self.diagnostics, &what, &error);
1767 }
1768 if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1769 break;
1770 }
1771 }
1772 if !event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1773 any_called |=
1774 crate::dom::inline_handlers::invoke(&ctx, &window, &event_obj, name, context);
1775 }
1776 }
1777
1778 crate::dom::define_value(&event_obj, "currentTarget", JsValue::null(), context);
1779
1780 if event_ref(&event_obj, &|event| event.prevented.get()) {
1782 event_state.prevent_default();
1783 }
1784 if event_ref(&event_obj, &|event| event.stopped.get()) {
1785 event_state.stop_propagation();
1786 }
1787 if any_called {
1788 event_state.request_redraw();
1789 }
1790
1791 any_called
1792 }
1793
1794 pub fn dispatch_document_event(&mut self, name: &str) -> bool {
1796 let root_id = self.ctx.doc.borrow().root_node().id;
1797 let mut event_state = EventState::default();
1798 let ran = self.dispatch_event_inner(
1799 &[root_id],
1800 name,
1801 true,
1802 |ctx, target, context| create_event(ctx, name, true, false, target, context),
1803 &mut event_state,
1804 );
1805 if ran {
1806 self.run_jobs("event microtasks");
1807 }
1808 ran
1809 }
1810
1811 pub fn dispatch_node_event(&mut self, node_id: NodeId, name: &str) -> bool {
1818 let mut event_state = EventState::default();
1819 let ran = self.dispatch_event_inner(
1820 &[node_id],
1821 name,
1822 false,
1823 |ctx, target, context| create_event(ctx, name, false, false, target, context),
1824 &mut event_state,
1825 );
1826 if ran {
1827 self.run_jobs("event microtasks");
1828 }
1829 ran
1830 }
1831
1832 pub fn dispatch_window_event(&mut self, name: &str) -> bool {
1834 let ctx = self.ctx.clone();
1835 let context = &mut self.context;
1836
1837 let listeners: Vec<Listener> = {
1838 let mut state = ctx.state.borrow_mut();
1839 match state.window_listeners.get_mut(name) {
1840 Some(listeners) => {
1841 let cloned = listeners.clone();
1842 listeners.retain(|l| !l.once);
1843 cloned
1844 }
1845 None => Vec::new(),
1846 }
1847 };
1848
1849 let global: JsValue = context.global_object().into();
1850 let event_obj = create_event(&ctx, name, false, false, &global, context);
1851 crate::dom::define_value(&event_obj, "currentTarget", global.clone(), context);
1852
1853 let mut any_called = false;
1854 for listener in listeners {
1855 any_called = true;
1856 if let Err(error) = crate::dom::passive::invoke(
1857 &listener.callback,
1858 &global,
1859 &event_obj,
1860 listener.passive,
1861 context,
1862 ) {
1863 let what = listener_error_context(name, "window", &listener.callback, context);
1864 report_js_error(&self.diagnostics, &what, &error);
1865 }
1866 }
1867
1868 if !event_obj
1869 .downcast_ref::<EventRef>()
1870 .is_some_and(|event| event.stopped_immediate.get())
1871 {
1872 let window = context.global_object().clone();
1873 any_called |=
1874 crate::dom::inline_handlers::invoke(&ctx, &window, &event_obj, name, context);
1875 }
1876 crate::dom::define_value(&event_obj, "currentTarget", JsValue::null(), context);
1877
1878 if any_called {
1879 self.run_jobs("event microtasks");
1880 }
1881 any_called
1882 }
1883}
1884
1885fn get_computed_style(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
1897 crate::domc::get_computed_style(args, context)
1898}
1899
1900fn observe_mutations(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
1904 let on = args.first().is_some_and(JsValue::to_boolean);
1905 dom_ctx(context)?
1906 .doc
1907 .borrow_mut()
1908 .set_recording_mutations(on);
1909 Ok(JsValue::undefined())
1910}
1911
1912fn mutation_record_value(
1916 ctx: &DomCtx,
1917 record: blitz_dom::DomMutation,
1918 context: &mut Context,
1919) -> Option<JsValue> {
1920 use blitz_dom::DomMutation;
1921 use boa_engine::object::builtins::JsArray;
1922
1923 let exists = |id: NodeId| ctx.doc.borrow().get_node(id).is_some();
1924 let node = |id: NodeId, context: &mut Context| -> JsValue {
1925 if exists(id) {
1926 node_wrapper(ctx, id, context).into()
1927 } else {
1928 JsValue::null()
1929 }
1930 };
1931 let nodes = |ids: Vec<NodeId>, context: &mut Context| -> JsValue {
1932 let values: Vec<JsValue> = ids
1933 .into_iter()
1934 .filter(|id| exists(*id))
1935 .map(|id| node_wrapper(ctx, id, context).into())
1936 .collect();
1937 JsArray::from_iter(values, context).into()
1938 };
1939 let text = |value: Option<String>| -> JsValue {
1940 value.map_or(JsValue::null(), |value| {
1941 JsString::from(value.as_str()).into()
1942 })
1943 };
1944
1945 let fields: Vec<JsValue> = match record {
1946 DomMutation::ChildList {
1947 target,
1948 added,
1949 removed,
1950 previous_sibling,
1951 next_sibling,
1952 } => {
1953 if !exists(target) {
1954 return None;
1955 }
1956 vec![
1957 js_string!("childList").into(),
1958 node(target, context),
1959 nodes(added, context),
1960 nodes(removed, context),
1961 previous_sibling.map_or(JsValue::null(), |id| node(id, context)),
1962 next_sibling.map_or(JsValue::null(), |id| node(id, context)),
1963 JsValue::null(),
1964 JsValue::null(),
1965 ]
1966 }
1967 DomMutation::Attributes {
1968 target,
1969 name,
1970 old_value,
1971 } => {
1972 if !exists(target) {
1973 return None;
1974 }
1975 vec![
1976 js_string!("attributes").into(),
1977 node(target, context),
1978 nodes(Vec::new(), context),
1979 nodes(Vec::new(), context),
1980 JsValue::null(),
1981 JsValue::null(),
1982 text(Some(name)),
1983 text(old_value),
1984 ]
1985 }
1986 DomMutation::CharacterData { target, old_value } => {
1987 if !exists(target) {
1988 return None;
1989 }
1990 vec![
1991 js_string!("characterData").into(),
1992 node(target, context),
1993 nodes(Vec::new(), context),
1994 nodes(Vec::new(), context),
1995 JsValue::null(),
1996 JsValue::null(),
1997 JsValue::null(),
1998 text(Some(old_value)),
1999 ]
2000 }
2001 };
2002 Some(JsArray::from_iter(fields, context).into())
2003}
2004
2005fn ipc_post_message(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2006 let body = args
2007 .first()
2008 .unwrap_or(&JsValue::undefined())
2009 .to_string(context)?
2010 .to_std_string_lossy();
2011 let handler = dom_ctx(context)?.state.borrow().ipc_handler.clone();
2012 if let Some(handler) = handler {
2013 handler(body);
2014 }
2015 Ok(JsValue::undefined())
2016}
2017
2018fn clipboard_write_text(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2025 let text = args
2026 .first()
2027 .unwrap_or(&JsValue::undefined())
2028 .to_string(context)?
2029 .to_std_string_lossy();
2030 let wrote = dom_ctx(context)?
2031 .doc
2032 .borrow()
2033 .shell_provider
2034 .set_clipboard_text(text)
2035 .is_ok();
2036 let promise = if wrote {
2037 JsPromise::resolve(JsValue::undefined(), context)
2038 } else {
2039 let error =
2040 crate::dom_exception::error("NotAllowedError", "the clipboard is unavailable", context);
2041 JsPromise::reject(error, context)
2042 };
2043 Ok(JsValue::from(promise?))
2044}
2045
2046fn clipboard_read_text(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2050 let read = dom_ctx(context)?
2051 .doc
2052 .borrow()
2053 .shell_provider
2054 .get_clipboard_text();
2055 let promise = match read {
2056 Ok(text) => JsPromise::resolve(JsValue::from(JsString::from(text)), context),
2057 Err(_) => {
2058 let error = crate::dom_exception::error(
2059 "NotAllowedError",
2060 "the clipboard is unavailable",
2061 context,
2062 );
2063 JsPromise::reject(error, context)
2064 }
2065 };
2066 Ok(JsValue::from(promise?))
2067}
2068
2069static TIME_ORIGIN: LazyLock<Instant> = LazyLock::new(Instant::now);
2073
2074fn performance_now(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
2075 Ok(JsValue::new(TIME_ORIGIN.elapsed().as_secs_f64() * 1000.0))
2076}
2077
2078fn viewport_scroll(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2079 let ctx = dom_ctx(context)?;
2080 let scroll = ctx.doc.borrow().viewport_scroll();
2081 Ok(boa_engine::object::builtins::JsArray::from_iter(
2082 [JsValue::new(scroll.x), JsValue::new(scroll.y)],
2083 context,
2084 )
2085 .into())
2086}
2087
2088fn scroll_viewport(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2089 let ctx = dom_ctx(context)?;
2090 let x = args
2091 .first()
2092 .unwrap_or(&JsValue::undefined())
2093 .to_number(context)?;
2094 let y = args
2095 .get(1)
2096 .unwrap_or(&JsValue::undefined())
2097 .to_number(context)?;
2098 let x = if x.is_finite() { x } else { 0.0 };
2099 let y = if y.is_finite() { y } else { 0.0 };
2100 ctx.flush_layout();
2101 let mut document = ctx.doc.borrow_mut();
2102 let current = document.viewport_scroll();
2103 let changed = document.scroll_viewport_by_has_changed(current.x - x, current.y - y);
2104 if changed {
2105 document.shell_provider.request_redraw();
2106 }
2107 Ok(JsValue::new(changed))
2108}
2109
2110fn random_u32(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2111 getrandom::u32().map(JsValue::new).map_err(|error| {
2112 crate::dom_exception::error(
2113 "OperationError",
2114 &format!("secure random generation failed: {error}"),
2115 context,
2116 )
2117 })
2118}
2119
2120fn register_global(context: &mut Context, name: &str, value: JsValue) {
2121 context
2122 .register_global_property(
2123 JsString::from(name),
2124 value,
2125 Attribute::WRITABLE.union(Attribute::CONFIGURABLE),
2126 )
2127 .expect("failed to register global");
2128}
2129
2130fn register_global_fn(
2131 context: &mut Context,
2132 name: &str,
2133 length: usize,
2134 body: fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>,
2135) {
2136 context
2137 .register_global_callable(
2138 JsString::from(name),
2139 length,
2140 NativeFunction::from_fn_ptr(body),
2141 )
2142 .expect("failed to register global function");
2143}
2144
2145fn build_location(base_url: Option<&Url>, context: &mut Context) -> JsValue {
2146 let (href, protocol, host, pathname, search, hash) = match base_url {
2147 Some(url) => (
2148 url.to_string(),
2149 format!("{}:", url.scheme()),
2150 url.host_str().unwrap_or_default().to_string(),
2151 url.path().to_string(),
2152 url.query().map(|q| format!("?{q}")).unwrap_or_default(),
2153 url.fragment().map(|f| format!("#{f}")).unwrap_or_default(),
2154 ),
2155 None => (
2156 "about:blank".to_string(),
2157 "about:".to_string(),
2158 String::new(),
2159 "blank".to_string(),
2160 String::new(),
2161 String::new(),
2162 ),
2163 };
2164 ObjectInitializer::new(context)
2165 .property(js_string!("href"), JsString::from(href), Attribute::all())
2166 .property(
2167 js_string!("protocol"),
2168 JsString::from(protocol),
2169 Attribute::all(),
2170 )
2171 .property(
2172 js_string!("host"),
2173 JsString::from(host.clone()),
2174 Attribute::all(),
2175 )
2176 .property(
2177 js_string!("hostname"),
2178 JsString::from(host),
2179 Attribute::all(),
2180 )
2181 .property(
2182 js_string!("pathname"),
2183 JsString::from(pathname),
2184 Attribute::all(),
2185 )
2186 .property(
2187 js_string!("search"),
2188 JsString::from(search),
2189 Attribute::all(),
2190 )
2191 .property(js_string!("hash"), JsString::from(hash), Attribute::all())
2192 .function(
2197 NativeFunction::from_fn_ptr(location_assign),
2198 js_string!("assign"),
2199 1,
2200 )
2201 .function(
2202 NativeFunction::from_fn_ptr(location_assign),
2203 js_string!("replace"),
2204 1,
2205 )
2206 .function(
2207 NativeFunction::from_fn_ptr(location_reload),
2208 js_string!("reload"),
2209 0,
2210 )
2211 .function(
2212 NativeFunction::from_fn_ptr(location_to_string),
2213 js_string!("toString"),
2214 0,
2215 )
2216 .build()
2217 .into()
2218}
2219
2220fn location_assign(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2224 let ctx = dom_ctx(context)?;
2225 let url = crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
2226 if !ctx.doc.borrow().navigate_to_url(&url) {
2227 return Err(crate::dom_exception::error(
2228 "SyntaxError",
2229 &format!("{url} is not a valid URL"),
2230 context,
2231 ));
2232 }
2233 Ok(JsValue::undefined())
2234}
2235
2236fn location_reload(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2237 let ctx = dom_ctx(context)?;
2238 let doc = ctx.doc.borrow();
2239 let current = doc.current_url();
2240 doc.navigate_to_url(¤t);
2241 Ok(JsValue::undefined())
2242}
2243
2244fn location_to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2249 match this.as_object() {
2250 Some(object) => object.get(js_string!("href"), context),
2251 None => Ok(js_string!("").into()),
2252 }
2253}
2254
2255fn window_inner_width(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2256 let ctx = dom_ctx(context)?;
2257 let viewport = ctx.doc.borrow().get_viewport();
2258 Ok(JsValue::from(
2259 viewport.window_size.0 as f64 / viewport.scale_f64(),
2260 ))
2261}
2262
2263fn window_inner_height(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2264 let ctx = dom_ctx(context)?;
2265 let viewport = ctx.doc.borrow().get_viewport();
2266 Ok(JsValue::from(
2267 viewport.window_size.1 as f64 / viewport.scale_f64(),
2268 ))
2269}
2270
2271fn window_device_pixel_ratio(
2272 _: &JsValue,
2273 _: &[JsValue],
2274 context: &mut Context,
2275) -> JsResult<JsValue> {
2276 let ctx = dom_ctx(context)?;
2277 Ok(JsValue::from(ctx.doc.borrow().get_viewport().scale_f64()))
2278}
2279
2280fn timer_args(
2283 args: &[JsValue],
2284 context: &mut Context,
2285) -> JsResult<Option<(JsObject, Duration, Vec<JsValue>)>> {
2286 let Some(callback) = args
2287 .first()
2288 .and_then(|value| value.as_object())
2289 .filter(|obj| obj.is_callable())
2290 else {
2291 return Ok(None);
2292 };
2293 let delay_ms = match args.get(1) {
2294 Some(value) => value.to_number(context)?,
2295 None => 0.0,
2296 };
2297 let delay_ms = if delay_ms.is_finite() && delay_ms > 0.0 {
2298 delay_ms
2299 } else {
2300 0.0
2301 };
2302 let rest: Vec<JsValue> = args.iter().skip(2).cloned().collect();
2303 Ok(Some((
2304 callback,
2305 Duration::from_secs_f64(delay_ms / 1000.0),
2306 rest,
2307 )))
2308}
2309
2310fn set_timeout(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2311 let ctx = dom_ctx(context)?;
2312 let Some((callback, delay, rest)) = timer_args(args, context)? else {
2313 return Ok(JsValue::from(0));
2314 };
2315 let id = ctx
2316 .state
2317 .borrow_mut()
2318 .timers
2319 .add(delay, None, callback, rest);
2320 Ok(JsValue::from(id as f64))
2321}
2322
2323fn set_interval(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2324 let ctx = dom_ctx(context)?;
2325 let Some((callback, delay, rest)) = timer_args(args, context)? else {
2326 return Ok(JsValue::from(0));
2327 };
2328 let id = ctx
2329 .state
2330 .borrow_mut()
2331 .timers
2332 .add(delay, Some(delay), callback, rest);
2333 Ok(JsValue::from(id as f64))
2334}
2335
2336fn request_animation_frame(
2337 _: &JsValue,
2338 args: &[JsValue],
2339 context: &mut Context,
2340) -> JsResult<JsValue> {
2341 let ctx = dom_ctx(context)?;
2342 let Some(callback) = args
2343 .first()
2344 .and_then(|value| value.as_object())
2345 .filter(|obj| obj.is_callable())
2346 else {
2347 return Ok(JsValue::from(0));
2348 };
2349 let id = ctx.state.borrow_mut().timers.add_animation_frame(callback);
2350 Ok(JsValue::from(id as f64))
2351}
2352
2353fn clear_timer(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2354 let ctx = dom_ctx(context)?;
2355 let id = match args.first() {
2356 Some(value) => value.to_number(context)?,
2357 None => return Ok(JsValue::undefined()),
2358 };
2359 if id.is_finite() && id >= 0.0 {
2360 ctx.state.borrow_mut().timers.remove(id as u64);
2361 }
2362 Ok(JsValue::undefined())
2363}
2364
2365pub(crate) fn window_dispatch_event(
2371 _: &JsValue,
2372 args: &[JsValue],
2373 context: &mut Context,
2374) -> JsResult<JsValue> {
2375 dispatch_window_supplied_event(args, true, context)
2376}
2377
2378fn window_dispatch_history_event(
2379 _: &JsValue,
2380 args: &[JsValue],
2381 context: &mut Context,
2382) -> JsResult<JsValue> {
2383 let event = args
2384 .first()
2385 .and_then(JsValue::as_object)
2386 .filter(|event| event.downcast_ref::<EventRef>().is_some())
2387 .ok_or_else(|| JsNativeError::typ().with_message("History dispatch requires an Event"))?;
2388 crate::dom::define_value(&event, "isTrusted", JsValue::from(true), context);
2389 dispatch_window_supplied_event(args, false, context)
2390}
2391
2392fn dispatch_window_supplied_event(
2393 args: &[JsValue],
2394 scripted: bool,
2395 context: &mut Context,
2396) -> JsResult<JsValue> {
2397 let ctx = dom_ctx(context)?;
2398 let event = args
2399 .first()
2400 .and_then(JsValue::as_object)
2401 .filter(|event| event.downcast_ref::<EventRef>().is_some())
2402 .ok_or_else(|| JsNativeError::typ().with_message("dispatchEvent requires an Event"))?;
2403 let _dispatch = crate::dom::event::begin_dispatch(&event, scripted, context)?;
2404 let event_type = crate::dom::to_rust_string(&event.get(js_string!("type"), context)?, context)?;
2405
2406 set_event_path(&event, vec![context.global_object().clone()]);
2407 let global: JsValue = context.global_object().into();
2408 crate::dom::define_value(&event, "target", global.clone(), context);
2409 crate::dom::define_value(&event, "srcElement", global.clone(), context);
2410 crate::dom::define_value(&event, "currentTarget", global.clone(), context);
2411 crate::dom::define_value(&event, "eventPhase", JsValue::from(2), context);
2412
2413 let listeners: Vec<Listener> = {
2414 let mut state = ctx.state.borrow_mut();
2415 match state.window_listeners.get_mut(&event_type) {
2416 Some(listeners) => {
2417 let cloned = listeners.clone();
2418 listeners.retain(|listener| !listener.once);
2419 cloned
2420 }
2421 None => Vec::new(),
2422 }
2423 };
2424 for listener in listeners {
2425 crate::dom::passive::invoke(
2426 &listener.callback,
2427 &global,
2428 &event,
2429 listener.passive,
2430 context,
2431 )?;
2432 if event
2433 .downcast_ref::<EventRef>()
2434 .is_some_and(|event| event.stopped_immediate.get())
2435 {
2436 break;
2437 }
2438 }
2439
2440 if !event
2442 .downcast_ref::<EventRef>()
2443 .is_some_and(|event| event.stopped_immediate.get())
2444 {
2445 let window = context.global_object().clone();
2446 crate::dom::inline_handlers::invoke(&ctx, &window, &event, &event_type, context);
2447 }
2448
2449 crate::dom::define_value(&event, "currentTarget", JsValue::null(), context);
2450 let prevented = event
2451 .downcast_ref::<EventRef>()
2452 .is_some_and(|event| event.prevented.get());
2453 Ok(JsValue::from(!prevented))
2454}
2455
2456pub(crate) fn window_add_event_listener(
2457 _: &JsValue,
2458 args: &[JsValue],
2459 context: &mut Context,
2460) -> JsResult<JsValue> {
2461 let ctx = dom_ctx(context)?;
2462 let event_type =
2463 crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
2464 let Some(callback) = args
2465 .get(1)
2466 .and_then(|value| value.as_object())
2467 .filter(|obj| obj.is_callable())
2468 else {
2469 return Ok(JsValue::undefined());
2470 };
2471
2472 let window: JsValue = context.global_object().into();
2473 let passive = crate::dom::passive::resolve(&window, args, &event_type, context)?;
2474 let mut state = ctx.state.borrow_mut();
2475 let listeners = state.window_listeners.entry(event_type).or_default();
2476 if !listeners
2477 .iter()
2478 .any(|l| JsObject::equals(&l.callback, &callback))
2479 {
2480 listeners.push(Listener {
2481 callback,
2482 once: false,
2483 passive,
2484 });
2485 }
2486 Ok(JsValue::undefined())
2487}
2488
2489pub(crate) fn window_remove_event_listener(
2490 _: &JsValue,
2491 args: &[JsValue],
2492 context: &mut Context,
2493) -> JsResult<JsValue> {
2494 let ctx = dom_ctx(context)?;
2495 let event_type =
2496 crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
2497 let Some(callback) = args.get(1).and_then(|value| value.as_object()) else {
2498 return Ok(JsValue::undefined());
2499 };
2500
2501 let mut state = ctx.state.borrow_mut();
2502 if let Some(listeners) = state.window_listeners.get_mut(&event_type) {
2503 listeners.retain(|l| !JsObject::equals(&l.callback, &callback));
2504 }
2505 Ok(JsValue::undefined())
2506}