Skip to main content

blitz_html/
preload.rs

1//! Script discovery without native DOM nodes, text storage or resource loads.
2
3use std::borrow::Cow;
4use std::cell::{Ref, RefCell};
5use std::rc::{Rc, Weak};
6
7use html5ever::tendril::{StrTendril, TendrilSink};
8use html5ever::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeBuilderOpts, TreeSink};
9use html5ever::{Attribute, ParseOpts, QualName};
10
11/// Source metadata for a script in the navigation's HTML tree.
12///
13/// Values retain html5ever's decoded attribute tendrils. Inline script text
14/// is not retained. Template contents are excluded.
15#[derive(Clone, Debug)]
16pub struct ScriptTag {
17    pub src: Option<StrTendril>,
18    pub script_type: StrTendril,
19    pub nomodule: bool,
20    pub mount: Option<StrTendril>,
21}
22
23impl ScriptTag {
24    fn from_attributes(attributes: &[Attribute]) -> Self {
25        let attribute = |name: &str| {
26            attributes
27                .iter()
28                .find(|attribute| attribute.name.local.as_ref() == name)
29                .map(|attribute| attribute.value.clone())
30        };
31        Self {
32            src: attribute("src"),
33            script_type: attribute("type").unwrap_or_default(),
34            nomodule: attribute("nomodule").is_some(),
35            mount: attribute("mount"),
36        }
37    }
38
39    /// Match the script types supported by blitz-script's navigation loader.
40    ///
41    /// Import maps are inline-only and do not name a prefetchable script.
42    pub fn is_javascript(&self) -> bool {
43        match self.script_type.trim().to_ascii_lowercase().as_str() {
44            "module" => true,
45            "" | "text/javascript" | "application/javascript" => !self.nomodule,
46            _ => false,
47        }
48    }
49}
50
51/// Discover script tags in HTML document order without constructing a Blitz DOM.
52///
53/// The ordinary html5ever tree builder handles raw text, comments, foreign
54/// content, ignored tags and malformed markup. Its sink retains script metadata
55/// and ancestry, not a second document. Closed branches without scripts lose
56/// their strong handles as the tree builder releases them.
57///
58/// This function is for HTML. XHTML callers retain their XML parsing path.
59pub fn scan_script_tags(html: &str) -> Vec<ScriptTag> {
60    let sink = PreloadSink {
61        root: PreloadNode::new(None, None),
62        scripts: RefCell::new(Vec::new()),
63    };
64    html5ever::parse_document(
65        sink,
66        ParseOpts {
67            tree_builder: TreeBuilderOpts {
68                scripting_enabled: true,
69                drop_doctype: true,
70                ..Default::default()
71            },
72            ..Default::default()
73        },
74    )
75    .one(html)
76}
77
78struct PreloadNode {
79    name: RefCell<Option<QualName>>,
80    script: Option<ScriptTag>,
81    parent: RefCell<Option<Rc<PreloadNode>>>,
82    children: RefCell<Vec<Weak<PreloadNode>>>,
83    template_contents: RefCell<Option<Rc<PreloadNode>>>,
84}
85
86impl PreloadNode {
87    fn new(name: Option<QualName>, script: Option<ScriptTag>) -> Rc<Self> {
88        Rc::new(Self {
89            name: RefCell::new(name),
90            script,
91            parent: RefCell::new(None),
92            children: RefCell::new(Vec::new()),
93            template_contents: RefCell::new(None),
94        })
95    }
96
97    fn detach(node: &Rc<Self>) {
98        let parent = node.parent.borrow_mut().take();
99        if let Some(parent) = parent {
100            let pointer = Rc::as_ptr(node);
101            parent
102                .children
103                .borrow_mut()
104                .retain(|child| child.as_ptr() != pointer);
105        }
106    }
107
108    fn append(parent: &Rc<Self>, node: Rc<Self>) {
109        Self::detach(&node);
110        *node.parent.borrow_mut() = Some(Rc::clone(parent));
111        parent.children.borrow_mut().push(Rc::downgrade(&node));
112    }
113}
114
115struct PreloadSink {
116    root: Rc<PreloadNode>,
117    // Anchor script branches until document order and template exclusion can
118    // be determined. Child edges are weak, so unrelated branches are released.
119    scripts: RefCell<Vec<Rc<PreloadNode>>>,
120}
121
122impl TreeSink for PreloadSink {
123    type Output = Vec<ScriptTag>;
124    type Handle = Rc<PreloadNode>;
125    type ElemName<'a>
126        = Ref<'a, QualName>
127    where
128        Self: 'a;
129
130    fn finish(self) -> Self::Output {
131        let mut scripts = Vec::new();
132        let mut stack = vec![Rc::clone(&self.root)];
133        while let Some(node) = stack.pop() {
134            if let Some(script) = &node.script {
135                scripts.push(script.clone());
136            }
137            stack.extend(
138                node.children
139                    .borrow()
140                    .iter()
141                    .rev()
142                    .filter_map(Weak::upgrade),
143            );
144        }
145        scripts
146    }
147
148    fn parse_error(&self, _: Cow<'static, str>) {}
149
150    fn get_document(&self) -> Self::Handle {
151        Rc::clone(&self.root)
152    }
153
154    fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
155        Ref::map(target.name.borrow(), |name| {
156            name.as_ref().expect("preload handle is not an element")
157        })
158    }
159
160    fn create_element(
161        &self,
162        name: QualName,
163        attributes: Vec<Attribute>,
164        _: ElementFlags,
165    ) -> Self::Handle {
166        let script =
167            (name.local.as_ref() == "script").then(|| ScriptTag::from_attributes(&attributes));
168        let node = PreloadNode::new(Some(name), script);
169        if node.script.is_some() {
170            self.scripts.borrow_mut().push(Rc::clone(&node));
171        }
172        node
173    }
174
175    fn create_comment(&self, _: StrTendril) -> Self::Handle {
176        PreloadNode::new(None, None)
177    }
178
179    fn create_pi(&self, _: StrTendril, _: StrTendril) -> Self::Handle {
180        PreloadNode::new(None, None)
181    }
182
183    fn append(&self, parent: &Self::Handle, child: NodeOrText<Self::Handle>) {
184        if let NodeOrText::AppendNode(node) = child {
185            PreloadNode::append(parent, node);
186        }
187    }
188
189    fn append_before_sibling(&self, sibling: &Self::Handle, child: NodeOrText<Self::Handle>) {
190        let NodeOrText::AppendNode(node) = child else {
191            return;
192        };
193        if Rc::ptr_eq(sibling, &node) {
194            return;
195        }
196        let parent = sibling.parent.borrow().clone();
197        let Some(parent) = parent else {
198            return;
199        };
200        PreloadNode::detach(&node);
201        let pointer = Rc::as_ptr(sibling);
202        let mut children = parent.children.borrow_mut();
203        let index = children
204            .iter()
205            .position(|child| child.as_ptr() == pointer)
206            .expect("preload sibling missing from parent");
207        *node.parent.borrow_mut() = Some(Rc::clone(&parent));
208        children.insert(index, Rc::downgrade(&node));
209    }
210
211    fn append_based_on_parent_node(
212        &self,
213        element: &Self::Handle,
214        previous: &Self::Handle,
215        child: NodeOrText<Self::Handle>,
216    ) {
217        let has_parent = element.parent.borrow().is_some();
218        if has_parent {
219            self.append_before_sibling(element, child);
220        } else {
221            self.append(previous, child);
222        }
223    }
224
225    fn append_doctype_to_document(&self, _: StrTendril, _: StrTendril, _: StrTendril) {}
226
227    fn allow_declarative_shadow_roots(&self, _: &Self::Handle) -> bool {
228        // Prefetch does not build shadow trees. A declarative template stays a
229        // normal template, and finish() does not walk template contents, so
230        // scripts that live only inside the shadow are not prefetched.
231        false
232    }
233
234    fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle {
235        let mut contents = target.template_contents.borrow_mut();
236        Rc::clone(contents.get_or_insert_with(|| PreloadNode::new(None, None)))
237    }
238
239    fn same_node(&self, left: &Self::Handle, right: &Self::Handle) -> bool {
240        Rc::ptr_eq(left, right)
241    }
242
243    fn set_quirks_mode(&self, _: QuirksMode) {}
244
245    fn add_attrs_if_missing(&self, _: &Self::Handle, _: Vec<Attribute>) {
246        // Document tree building merges attributes onto html/body, neither
247        // of which carries script metadata.
248    }
249
250    fn remove_from_parent(&self, target: &Self::Handle) {
251        PreloadNode::detach(target);
252    }
253
254    fn reparent_children(&self, previous: &Self::Handle, parent: &Self::Handle) {
255        let children: Vec<_> = previous
256            .children
257            .borrow_mut()
258            .drain(..)
259            .filter_map(|child| child.upgrade())
260            .collect();
261        for child in children {
262            PreloadNode::append(parent, child);
263        }
264    }
265}