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