1use std::cell::RefCell;
4use std::rc::Rc;
5use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
6use std::sync::{Arc, Mutex};
7use std::task::{Context as TaskContext, Waker};
8
9use blitz_dom::{
10 BaseDocument, DEFAULT_CSS, DocGuard, DocGuardMut, Document, DocumentConfig, EventDriver, NodeId,
11};
12use blitz_html::stream::StreamingParser;
13use blitz_html::{DocumentHtmlParser, HtmlProvider};
14use blitz_traits::events::{BlitzPointerEvent, DomEvent, UiEvent};
15use url::Url;
16use web_time::{Duration, Instant};
17
18use crate::event_handler::ScriptEventHandler;
19use crate::fetch::{DefaultScriptFetcher, ScriptFetcher};
20use crate::module::SharedFetcher;
21use crate::runtime::ScriptRuntime;
22
23type PollHook =
24 Box<dyn for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static>;
25
26#[derive(Clone, Copy, PartialEq, Eq)]
28enum ScriptKind {
29 Classic,
30 Module,
33 ImportMap,
36}
37
38struct PendingScript {
40 node_id: NodeId,
41 src: Option<String>,
42 inline_text: String,
43 kind: ScriptKind,
44 deferred: bool,
50}
51
52pub struct ScriptDocument {
61 inner: Rc<RefCell<BaseDocument>>,
62 runtime: ScriptRuntime,
63 base_url: Option<Url>,
64 fetcher: SharedFetcher,
71 scripts_executed: bool,
72 startup_events_pending: bool,
73 load_event_pending: bool,
74 executed_scripts: std::collections::HashSet<NodeId>,
81 poll_hook: Option<PollHook>,
82
83 waker: Arc<Mutex<Option<Waker>>>,
86 timer_thread: Option<Sender<Instant>>,
87}
88
89impl ScriptDocument {
90 pub fn from_html(html: &str, mut config: DocumentConfig) -> Self {
99 if let Some(ss) = &mut config.ua_stylesheets {
100 if !ss.iter().any(|s| s == DEFAULT_CSS) {
101 ss.push(String::from(DEFAULT_CSS));
102 }
103 } else {
104 config.ua_stylesheets = Some(vec![String::from(DEFAULT_CSS)]);
105 }
106 if config.html_parser_provider.is_none() {
107 config.html_parser_provider = Some(Arc::new(HtmlProvider));
108 }
109
110 let base_url = config
111 .base_url
112 .as_deref()
113 .and_then(|url| Url::parse(url).ok());
114
115 let is_xml = DocumentHtmlParser::is_xhtml_document(html);
116 let mut doc = BaseDocument::new(config);
117 if is_xml {
118 let mut mutr = doc.mutate();
119 DocumentHtmlParser::parse_into_mutator(&mut mutr, html);
120 drop(mutr);
121 }
122
123 let inner = Rc::new(RefCell::new(doc));
124 let parser = if is_xml {
125 None
126 } else {
127 let mut parser = StreamingParser::new(Rc::clone(&inner), html);
128 if parser.next_script().is_some() {
129 Some(parser)
130 } else {
131 parser.finish();
132 None
133 }
134 };
135 let fetcher: SharedFetcher = Rc::new(RefCell::new(Rc::new(DefaultScriptFetcher)));
136 let runtime = ScriptRuntime::new(Rc::clone(&inner), base_url.as_ref(), Rc::clone(&fetcher));
137 crate::docwrite::prepare(&runtime.context, html, Rc::clone(&fetcher), parser);
138
139 Self {
140 inner,
141 runtime,
142 base_url,
143 fetcher,
144 scripts_executed: false,
145 startup_events_pending: false,
146 load_event_pending: false,
147 executed_scripts: std::collections::HashSet::new(),
148 poll_hook: None,
149 waker: Arc::new(Mutex::new(None)),
150 timer_thread: None,
151 }
152 }
153
154 pub fn set_job_budget(&mut self, duration: Duration) {
159 self.runtime.set_job_budget(duration);
160 }
161
162 pub fn job_budget(&self) -> Duration {
164 self.runtime.job_budget()
165 }
166
167 pub fn has_pending_jobs(&self) -> bool {
171 self.runtime.jobs_remain()
172 }
173
174 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
178 self.runtime.next_timer_deadline()
179 }
180
181 pub fn with_fetcher(self, fetcher: impl ScriptFetcher) -> Self {
184 *self.fetcher.borrow_mut() = Rc::new(fetcher);
185 self
186 }
187
188 pub fn set_ipc_handler(&mut self, handler: impl Fn(String) + 'static) {
193 self.runtime.ctx.state.borrow_mut().ipc_handler = Some(Rc::new(handler));
194 }
195
196 pub fn set_poll_hook(
200 &mut self,
201 hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
202 ) {
203 self.poll_hook = Some(Box::new(hook));
204 }
205
206 pub fn add_poll_hook(
209 &mut self,
210 mut hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
211 ) {
212 let Some(mut existing) = self.poll_hook.take() else {
213 self.poll_hook = Some(Box::new(hook));
214 return;
215 };
216 self.poll_hook = Some(Box::new(move |document, task_context| {
217 let ran_existing = existing(document, task_context);
218 let ran_added = hook(document, task_context);
219 ran_existing | ran_added
220 }));
221 }
222
223 pub fn execute_scripts(&mut self) {
228 let _profiling = self.runtime.ctx.enter_profiling_boundary();
229 let _slice = self.runtime.begin_job_slice();
230 if self.scripts_executed {
231 return;
232 }
233 self.scripts_executed = true;
234 self.startup_events_pending = true;
235
236 crate::docwrite::begin(&mut self.runtime.context);
237 loop {
238 let progressed = self.run_pending_scripts();
239 if !progressed
240 || self.runtime.jobs_remain()
241 || !crate::docwrite::active(&self.runtime.context)
242 {
243 break;
244 }
245 }
246 self.finish_startup_events();
247
248 self.request_redraw();
249 self.arm_timer_thread();
250 }
251
252 pub fn external_script_urls(&self) -> Vec<Url> {
260 if let Some(original) = crate::docwrite::original(&self.runtime.context) {
261 return blitz_html::scan_script_tags(&original)
264 .into_iter()
265 .filter(|script| script.is_javascript())
266 .filter_map(|script| script.src)
267 .filter_map(|src| self.resolve_script_url(src.as_ref()))
268 .collect();
269 }
270 self.collect_scripts()
271 .iter()
272 .filter(|script| script.kind != ScriptKind::ImportMap)
275 .filter_map(|script| script.src.as_deref())
276 .filter_map(|src| self.resolve_script_url(src))
277 .collect()
278 }
279
280 fn resolve_script_url(&self, src: &str) -> Option<Url> {
282 match &self.base_url {
283 Some(base) => base.join(src).ok(),
284 None => Url::parse(src).ok(),
285 }
286 }
287
288 pub fn with_js_context<R>(
294 &mut self,
295 callback: impl FnOnce(&mut boa_engine::Context) -> R,
296 ) -> R {
297 callback(&mut self.runtime.context)
298 }
299
300 pub fn eval(&mut self, code: &str) {
302 let _profiling = self.runtime.ctx.enter_profiling_boundary();
303 self.runtime.eval(code, "<eval>");
304 self.request_redraw();
305 self.arm_timer_thread();
306 }
307
308 pub fn eval_named(&mut self, code: &str, name: &str) {
311 let _profiling = self.runtime.ctx.enter_profiling_boundary();
312 self.runtime.eval_internal(code, name);
313 self.runtime.run_jobs(name);
314 self.request_redraw();
315 self.arm_timer_thread();
316 }
317
318 pub fn eval_json(&mut self, code: &str) -> Result<serde_json::Value, String> {
324 let _profiling = self.runtime.ctx.enter_profiling_boundary();
325 let result = self.runtime.eval_json(code, "<eval with result>");
326 self.request_redraw();
327 self.arm_timer_thread();
328 result
329 }
330
331 #[cfg(feature = "debug-control")]
332 pub(crate) fn console_entries_after(
333 &self,
334 sequence: u64,
335 ) -> Vec<crate::runtime::DiagnosticEntry> {
336 self.runtime.console_entries_after(sequence)
337 }
338
339 #[cfg(feature = "debug-control")]
340 pub(crate) fn runtime_errors_after(
341 &self,
342 sequence: u64,
343 ) -> Vec<crate::runtime::DiagnosticEntry> {
344 self.runtime.runtime_errors_after(sequence)
345 }
346
347 pub fn current_url(&self) -> Option<&Url> {
349 self.base_url.as_ref()
350 }
351
352 pub fn page_source(&self) -> String {
354 self.inner.borrow().root_element().outer_html()
355 }
356
357 pub fn dispatch_dom_event(&mut self, event: DomEvent) {
362 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
363 let profiling = profiling_boundary.enabled();
364 let handler = ScriptEventHandler {
365 runtime: &mut self.runtime,
366 profiling,
367 };
368 let mut driver = EventDriver::new(&mut self.inner, handler);
369 driver.handle_dom_event(event);
370
371 self.request_redraw();
372 self.arm_timer_thread();
373 }
374
375 fn run_pending_scripts(&mut self) -> bool {
388 let _slice = self.runtime.begin_job_slice();
389 if self.runtime.jobs_remain() {
390 return false;
391 }
392 if crate::docwrite::take_restart(&self.runtime.context) {
393 self.executed_scripts.clear();
394 self.startup_events_pending = true;
395 self.load_event_pending = false;
396 }
397 let parsed = crate::docwrite::advance(&mut self.runtime.context);
398 let generation = crate::docwrite::generation(&self.runtime.context);
399 if parsed {
400 self.runtime.ctx.mark_layout_dirty();
401 }
402 let pending: Vec<PendingScript> = self
405 .collect_scripts()
406 .into_iter()
407 .filter(|script| !self.executed_scripts.contains(&script.node_id))
408 .filter(|script| {
409 !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
410 && (!script.deferred || !crate::docwrite::active(&self.runtime.context))
411 })
412 .collect();
413 let ran = parsed || !pending.is_empty();
414
415 for script in &pending {
420 if script.kind == ScriptKind::ImportMap && !script.inline_text.trim().is_empty() {
421 self.runtime
422 .set_import_map(&script.inline_text, self.base_url.as_ref());
423 }
424 }
425
426 let (deferred, immediate): (Vec<_>, Vec<_>) =
437 pending.into_iter().partition(|script| script.deferred);
438
439 for script in immediate.into_iter().chain(deferred) {
440 if generation != crate::docwrite::generation(&self.runtime.context) {
441 break;
442 }
443 if self.startup_events_pending && script.deferred {
444 self.runtime.set_document_ready_state("interactive");
445 if self.runtime.jobs_remain() {
446 break;
447 }
448 }
449 self.executed_scripts.insert(script.node_id);
453 if script.kind == ScriptKind::ImportMap {
454 continue;
455 }
456 match script.src {
457 Some(src) => {
458 let Some(url) = self.resolve_script_url(&src) else {
459 eprintln!("blitz-script: could not resolve script URL {src:?}");
460 self.runtime.dispatch_node_event(script.node_id, "error");
461 if self.runtime.jobs_remain() {
462 break;
463 }
464 continue;
465 };
466 let fetcher = Rc::clone(&self.fetcher.borrow());
467 match fetcher.fetch(&url) {
468 Ok(code) => {
469 if script.kind == ScriptKind::Module {
470 self.runtime.eval_module(&code, Some(&url), url.as_str());
471 } else {
472 crate::domc::set_current_script(
473 &self.runtime.context,
474 Some(script.node_id),
475 );
476 self.runtime.eval_at(&code, Some(&url), url.as_str());
477 }
478 self.runtime.dispatch_node_event(script.node_id, "load");
479 }
480 Err(error) => {
481 eprintln!("blitz-script: failed to fetch script {url}: {error}");
482 self.runtime.dispatch_node_event(script.node_id, "error");
483 }
484 }
485 }
486 None => {
487 if !script.inline_text.trim().is_empty() {
488 let base_url = self.base_url.clone();
492 if script.kind == ScriptKind::Module {
493 self.runtime.eval_module(
494 &script.inline_text,
495 base_url.as_ref(),
496 "<inline module>",
497 );
498 } else {
499 crate::domc::set_current_script(
500 &self.runtime.context,
501 Some(script.node_id),
502 );
503 self.runtime.eval_at(
504 &script.inline_text,
505 base_url.as_ref(),
506 "<inline script>",
507 );
508 }
509 }
510 }
511 }
512 if self.runtime.jobs_remain() {
515 break;
516 }
517 }
518 ran
519 }
520
521 fn finish_startup_events(&mut self) -> bool {
522 if !(self.startup_events_pending || self.load_event_pending)
523 || self.runtime.jobs_remain()
524 || crate::docwrite::active(&self.runtime.context)
525 {
526 return false;
527 }
528 if self.startup_events_pending
529 && self.collect_scripts().iter().any(|script| {
530 !self.executed_scripts.contains(&script.node_id)
531 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
532 })
533 {
534 return false;
535 }
536
537 let mut ran = false;
538 if self.startup_events_pending {
539 ran |= self.runtime.set_document_ready_state("interactive");
540 if self.runtime.jobs_remain() {
541 return ran;
542 }
543 self.startup_events_pending = false;
544 self.load_event_pending = true;
545 self.runtime.dispatch_document_event("DOMContentLoaded");
546 ran = true;
547 }
548 let resources_pending = {
549 let doc = self.inner.borrow();
550 doc.has_pending_critical_resources() || doc.pending_image_count() != 0
551 };
552 if self.load_event_pending
553 && !self.runtime.jobs_remain()
554 && !resources_pending
555 && !self.runtime.has_pending_module_evaluations()
556 {
557 ran |= self.runtime.set_document_ready_state("complete");
558 if !self.runtime.jobs_remain() {
559 self.load_event_pending = false;
560 self.runtime.dispatch_window_event("load");
561 ran = true;
562 }
563 }
564 ran
565 }
566
567 fn collect_scripts(&self) -> Vec<PendingScript> {
569 let doc = self.inner.borrow();
570 let mut scripts = Vec::new();
571 let mut stack = vec![doc.root_node().id];
572
573 while let Some(node_id) = stack.pop() {
574 let Some(node) = doc.get_node(node_id) else {
575 continue;
576 };
577
578 if let Some(element) = node.element_data() {
579 if element.name.local == blitz_dom::local_name!("script") {
580 if node
581 .flags
582 .contains(blitz_dom::node::NodeFlags::IS_PARSER_INERT_SCRIPT)
583 {
584 continue;
585 }
586 let script_type = element
588 .attr(blitz_dom::local_name!("type"))
589 .unwrap_or("")
590 .trim()
591 .to_ascii_lowercase();
592 let kind = match script_type.as_str() {
593 "module" => Some(ScriptKind::Module),
594 "importmap" => Some(ScriptKind::ImportMap),
595 "" | "text/javascript" | "application/javascript" => {
596 if element.attr(blitz_dom::local_name!("nomodule")).is_some() {
601 None
602 } else {
603 Some(ScriptKind::Classic)
604 }
605 }
606 _ => None,
607 };
608
609 if let Some(kind) = kind {
610 let src = element
611 .attr(blitz_dom::local_name!("src"))
612 .map(str::to_string);
613 let is_async = element.attr(blitz_dom::local_name!("async")).is_some();
614
615 let deferred = !is_async
619 && match kind {
620 ScriptKind::Module => true,
621 ScriptKind::Classic => {
622 src.is_some()
623 && element.attr(blitz_dom::local_name!("defer")).is_some()
624 }
625 ScriptKind::ImportMap => false,
626 };
627
628 scripts.push(PendingScript {
629 node_id,
630 src,
631 inline_text: node.text_content(),
632 kind,
633 deferred,
634 });
635 }
636 continue;
637 }
638 }
639
640 stack.extend(node.children.iter().rev().copied());
641 }
642
643 scripts
644 }
645
646 fn request_redraw(&self) {
647 self.inner.borrow().shell_provider.request_redraw();
648 }
649
650 fn arm_timer_thread(&mut self) {
653 let Some(deadline) = self.runtime.next_timer_deadline() else {
654 return;
655 };
656
657 let sender = self.timer_thread.get_or_insert_with(|| {
658 let (tx, rx) = channel::<Instant>();
659 let waker = Arc::clone(&self.waker);
660 std::thread::Builder::new()
661 .name("blitz-script-timers".to_string())
662 .spawn(move || timer_thread_main(rx, waker))
663 .expect("failed to spawn timer thread");
664 tx
665 });
666
667 if sender.send(deadline).is_err() {
670 self.timer_thread = None;
671 }
672 }
673}
674
675fn timer_thread_main(rx: Receiver<Instant>, waker: Arc<Mutex<Option<Waker>>>) {
677 let mut deadline: Option<Instant> = None;
678
679 loop {
680 match deadline {
681 None => match rx.recv() {
682 Ok(new_deadline) => deadline = Some(new_deadline),
683 Err(_) => return,
684 },
685 Some(current) => {
686 let now = Instant::now();
687 if current <= now {
688 if let Some(waker) = waker.lock().unwrap().as_ref() {
689 waker.wake_by_ref();
690 }
691 deadline = None;
692 continue;
693 }
694 match rx.recv_timeout(current - now) {
695 Ok(new_deadline) => deadline = Some(new_deadline.min(current)),
696 Err(RecvTimeoutError::Timeout) => {
697 if let Some(waker) = waker.lock().unwrap().as_ref() {
698 waker.wake_by_ref();
699 }
700 deadline = None;
701 }
702 Err(RecvTimeoutError::Disconnected) => return,
703 }
704 }
705 }
706 }
707}
708
709impl Document for ScriptDocument {
710 fn inner(&self) -> DocGuard<'_> {
711 DocGuard::RefCell(self.inner.borrow())
712 }
713
714 fn inner_mut(&mut self) -> DocGuardMut<'_> {
715 DocGuardMut::RefCell(self.inner.borrow_mut())
716 }
717
718 fn handle_ui_event(&mut self, event: UiEvent) {
719 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
720 let profiling = profiling_boundary.enabled();
721 let handler = ScriptEventHandler {
722 runtime: &mut self.runtime,
723 profiling,
724 };
725 let mut driver = EventDriver::new(&mut self.inner, handler);
726 driver.handle_ui_event(event);
727
728 self.request_redraw();
730 self.arm_timer_thread();
731 }
732
733 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
734 self.poll_with_timer_policy(task_context, None)
735 }
736
737 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
738 self.poll_with_timer_policy(task_context, Some(run_timers))
739 }
740}
741
742impl ScriptDocument {
743 fn poll_with_timer_policy(
745 &mut self,
746 task_context: Option<TaskContext>,
747 settle_timers: Option<bool>,
748 ) -> bool {
749 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
750 let profiling = profiling_boundary.enabled();
751 let poll_started = profiling.then(std::time::Instant::now);
752 let _slice = self.runtime.begin_job_slice();
753 let ran = self.poll_inner(task_context, profiling, settle_timers);
754 if let Some(started) = poll_started {
755 crate::script_stats::record_poll(started.elapsed(), ran);
756 }
757 ran
758 }
759
760 pub fn handle_pointer_move_to_node(&mut self, event: BlitzPointerEvent, node_id: NodeId) {
767 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
768 let profiling = profiling_boundary.enabled();
769 let handler = ScriptEventHandler {
770 runtime: &mut self.runtime,
771 profiling,
772 };
773 let mut driver = EventDriver::new(&mut self.inner, handler);
774 driver.handle_pointer_move_to_node(&event, node_id);
775
776 self.request_redraw();
777 self.arm_timer_thread();
778 }
779
780 pub fn handle_ui_event_to_node(&mut self, event: UiEvent, node_id: NodeId) {
784 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
785 let profiling = profiling_boundary.enabled();
786 let handler = ScriptEventHandler {
787 runtime: &mut self.runtime,
788 profiling,
789 };
790 let mut driver = EventDriver::new(&mut self.inner, handler);
791 driver.handle_ui_event_to_node(event, node_id);
792
793 self.request_redraw();
794 self.arm_timer_thread();
795 }
796
797 fn poll_inner(
800 &mut self,
801 task_context: Option<TaskContext>,
802 profiling: bool,
803 settle_timers: Option<bool>,
804 ) -> bool {
805 if let Some(cx) = &task_context {
807 let mut waker = self.waker.lock().unwrap();
808 let stale = waker
809 .as_ref()
810 .map(|old| !old.will_wake(cx.waker()))
811 .unwrap_or(true);
812 if stale {
813 *waker = Some(cx.waker().clone());
814 }
815 }
816
817 let subdocument_changes = {
822 let mut inner = self.inner.borrow_mut();
823 let waker = task_context.as_ref().map(TaskContext::waker);
824 match settle_timers {
825 Some(run_timers) => inner.poll_subdocuments_for_settle(waker, run_timers),
826 None => inner.poll_subdocuments(waker),
827 }
828 };
829
830 let mut ran = subdocument_changes;
831 ran |= self.runtime.poll_domc();
832 ran |= self.runtime.run_jobs_with_progress("poll microtasks");
835
836 if !self.scripts_executed {
838 let started = profiling.then(std::time::Instant::now);
842 self.execute_scripts();
843 if let Some(started) = started {
844 crate::script_stats::record_work("startup:execute_scripts", started.elapsed());
845 }
846 ran = true;
847 } else {
848 ran |= self.run_pending_scripts();
852 }
853 ran |= self.finish_startup_events();
854
855 if settle_timers.unwrap_or(true) {
856 ran |= self.runtime.run_due_timers(profiling);
857 }
858
859 self.runtime.poll_module_evaluations();
863
864 if let Some(mut hook) = self.poll_hook.take() {
865 let started = profiling.then(std::time::Instant::now);
869 ran |= hook(self, task_context.as_ref());
870 if let Some(started) = started {
871 crate::script_stats::record_work("poll_hook", started.elapsed());
872 }
873 self.poll_hook = Some(hook);
874 }
875
876 self.runtime.sweep_detached_nodes();
881
882 if self.runtime.jobs_remain() {
883 ran = true;
884 self.request_redraw();
885 if let Some(cx) = &task_context {
886 cx.waker().wake_by_ref();
887 }
888 }
889 self.arm_timer_thread();
890 ran
891 }
892}