Skip to main content

blitz_html/
html_sink.rs

1//! An implementation for Html5ever's sink trait, allowing us to parse HTML into a DOM.
2
3use html5ever::ParseOpts;
4use html5ever::tokenizer::TokenizerOpts;
5use html5ever::tree_builder::TreeBuilderOpts;
6use std::borrow::Cow;
7use std::cell::{Cell, Ref, RefCell, RefMut};
8use std::sync::Arc;
9
10use blitz_dom::node::{Attribute, MarkupNode, NodeFlags};
11use blitz_dom::{DocumentMutator, HtmlParserProvider, NodeId};
12use html5ever::{
13    QualName,
14    tendril::{StrTendril, TendrilSink},
15    tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink},
16};
17
18/// Convert an html5ever Attribute, which uses tendril for its value, to a blitz
19/// Attribute, which interns its value.
20///
21/// This is the highest-volume construction site in the engine: every attribute
22/// of every element of every parsed document arrives here. It is therefore
23/// where interning pays off most, because a document's repeated `class` strings
24/// collapse as the tree is built rather than after it.
25fn html5ever_to_blitz_attr(attr: html5ever::Attribute) -> Attribute {
26    Attribute {
27        name: attr.name,
28        value: attr.value.as_ref().into(),
29    }
30}
31
32#[derive(Copy, Clone, Default, Debug)]
33pub struct HtmlProvider;
34
35impl HtmlParserProvider for HtmlProvider {
36    fn parse_inner_html<'m2, 'doc2>(
37        &self,
38        mutr: &'m2 mut DocumentMutator<'doc2>,
39        element_id: NodeId,
40        html: &str,
41    ) {
42        DocumentHtmlParser::parse_inner_html_into_mutator(mutr, element_id, html, false);
43    }
44
45    fn parse_fragment<'m2, 'doc2>(
46        &self,
47        mutr: &'m2 mut DocumentMutator<'doc2>,
48        element_id: NodeId,
49        html: &str,
50        allow_declarative_shadow_roots: bool,
51    ) {
52        DocumentHtmlParser::parse_inner_html_into_mutator(
53            mutr,
54            element_id,
55            html,
56            allow_declarative_shadow_roots,
57        );
58    }
59
60    fn parse_document(
61        &self,
62        html: &str,
63        config: blitz_dom::DocumentConfig,
64    ) -> Box<dyn blitz_dom::Document> {
65        Box::new(crate::HtmlDocument::from_html(html, config))
66    }
67}
68
69pub struct DocumentHtmlParser<'m, 'doc> {
70    document_mutator: RefCell<&'m mut DocumentMutator<'doc>>,
71
72    /// Errors that occurred during parsing.
73    pub errors: RefCell<Vec<Cow<'static, str>>>,
74
75    /// The document's quirks mode.
76    pub quirks_mode: Cell<QuirksMode>,
77    pub is_xml: bool,
78    document_id: Option<NodeId>,
79    parsing_document: bool,
80    /// Document parse and `setHTMLUnsafe` allow declarative shadow roots.
81    /// `innerHTML`, `DOMParser`, and `insertAdjacentHTML` do not.
82    allow_declarative_shadow_roots: bool,
83}
84
85impl<'m, 'doc> DocumentHtmlParser<'m, 'doc> {
86    #[track_caller]
87    /// Get a mutable borrow of the DocumentMutator
88    fn mutr(&self) -> RefMut<'_, &'m mut DocumentMutator<'doc>> {
89        self.document_mutator.borrow_mut()
90    }
91
92    fn adopt_created(&self, node_id: NodeId) {
93        if let Some(document_id) = self.document_id {
94            self.mutr().adopt_node(node_id, document_id);
95        }
96    }
97}
98
99impl<'m, 'doc> DocumentHtmlParser<'m, 'doc> {
100    pub fn new(mutr: &'m mut DocumentMutator<'doc>) -> DocumentHtmlParser<'m, 'doc> {
101        DocumentHtmlParser {
102            document_mutator: RefCell::new(mutr),
103            errors: RefCell::new(Vec::new()),
104            quirks_mode: Cell::new(QuirksMode::NoQuirks),
105            is_xml: false,
106            document_id: None,
107            parsing_document: true,
108            allow_declarative_shadow_roots: true,
109        }
110    }
111
112    /// Parse into a detached document. The caller's MIME type selects the
113    /// parser independently of declarations in the source.
114    pub fn parse_inert_into_mutator(
115        mutr: &mut DocumentMutator<'_>,
116        document_id: NodeId,
117        source: &str,
118        is_xml: bool,
119    ) -> Vec<Cow<'static, str>> {
120        let mut sink = DocumentHtmlParser::new(mutr);
121        sink.document_id = Some(document_id);
122        sink.is_xml = is_xml;
123        sink.allow_declarative_shadow_roots = false;
124        if is_xml {
125            let errors = xml5ever::driver::parse_document(sink, Default::default()).one(source);
126            Self::repair_xml_prolog(mutr, document_id, source);
127            errors
128        } else {
129            let opts = ParseOpts {
130                tokenizer: TokenizerOpts::default(),
131                tree_builder: TreeBuilderOpts {
132                    exact_errors: false,
133                    scripting_enabled: false,
134                    iframe_srcdoc: false,
135                    drop_doctype: false,
136                    quirks_mode: QuirksMode::NoQuirks,
137                },
138            };
139            html5ever::parse_document(sink, opts).one(source)
140        }
141    }
142
143    /// Undo two xml5ever tokenizer habits that XML documents must not show:
144    /// the XML declaration surfaces as a `<?xml ...?>` processing instruction,
145    /// and DOCTYPE names are ASCII-lowercased (HTML's rule, not XML's). The
146    /// declaration is dropped and the name restored from the source.
147    fn repair_xml_prolog(mutr: &mut DocumentMutator<'_>, document_id: NodeId, source: &str) {
148        let children: Vec<NodeId> = mutr
149            .doc
150            .get_node(document_id)
151            .map(|node| node.children.to_vec())
152            .unwrap_or_default();
153        let name = source.find("<!DOCTYPE").map(|start| {
154            source[start + "<!DOCTYPE".len()..]
155                .trim_start()
156                .split(|c: char| c.is_whitespace() || c == '[' || c == '>')
157                .next()
158                .unwrap_or_default()
159                .to_owned()
160        });
161        for child in children {
162            let markup = mutr
163                .doc
164                .get_node(child)
165                .and_then(|node| node.markup.clone());
166            match markup.as_deref() {
167                Some(MarkupNode::ProcessingInstruction { target }) if target == "xml" => {
168                    mutr.remove_node(child);
169                }
170                Some(MarkupNode::Doctype {
171                    name: parsed,
172                    public_id,
173                    system_id,
174                }) => {
175                    if let Some(original) = &name
176                        && original.eq_ignore_ascii_case(parsed)
177                        && original != parsed
178                    {
179                        let repaired = MarkupNode::Doctype {
180                            name: original.clone(),
181                            public_id: public_id.clone(),
182                            system_id: system_id.clone(),
183                        };
184                        if let Some(node) = mutr.doc.get_node_mut(child) {
185                            node.markup = Some(Arc::new(repaired));
186                        }
187                    }
188                }
189                _ => {}
190            }
191        }
192    }
193
194    /// Detects documents without an XML or DOCTYPE declaration whose root `<html>` element
195    /// declares the XHTML namespace (e.g. `<html xmlns="http://www.w3.org/1999/xhtml">`)
196    fn root_element_has_xhtml_namespace(html: &str) -> bool {
197        let rest = html.trim_start_matches('\u{feff}').trim_start();
198        let Some(rest) = rest.strip_prefix("<html") else {
199            return false;
200        };
201        let Some(tag_end) = rest.find('>') else {
202            return false;
203        };
204        rest[..tag_end].contains("xmlns=\"http://www.w3.org/1999/xhtml\"")
205            || rest[..tag_end].contains("xmlns='http://www.w3.org/1999/xhtml'")
206    }
207
208    pub fn is_xhtml_document(html: &str) -> bool {
209        html.starts_with("<?xml")
210            || html.starts_with("<!DOCTYPE") && {
211                let first_line = html.lines().next().unwrap();
212                first_line.contains("XHTML") || first_line.contains("xhtml")
213            }
214            || Self::root_element_has_xhtml_namespace(html)
215    }
216
217    pub fn parse_into_mutator<'a, 'd>(mutr: &'a mut DocumentMutator<'d>, html: &str) {
218        let mut sink = DocumentHtmlParser::new(mutr);
219
220        let is_xhtml_doc = Self::is_xhtml_document(html);
221
222        if is_xhtml_doc {
223            // Parse as XHTML
224            sink.is_xml = true;
225            xml5ever::driver::parse_document(sink, Default::default())
226                .from_utf8()
227                .read_from(&mut html.as_bytes())
228                .unwrap();
229        } else {
230            // Parse as HTML
231            sink.is_xml = false;
232            let opts = ParseOpts {
233                tokenizer: TokenizerOpts::default(),
234                tree_builder: TreeBuilderOpts {
235                    exact_errors: false,
236                    scripting_enabled: true, // scripting runs, so noscript contents are text
237                    iframe_srcdoc: false,
238                    drop_doctype: false,
239                    quirks_mode: QuirksMode::NoQuirks,
240                },
241            };
242            html5ever::parse_document(sink, opts)
243                .from_utf8()
244                .read_from(&mut html.as_bytes())
245                .unwrap();
246        }
247    }
248
249    /// Parse HTML into a new document node. Declarative shadow roots attach.
250    /// Scripts stay inert, matching `Document.parseHTMLUnsafe`.
251    pub fn parse_html_unsafe_into_mutator(
252        mutr: &mut DocumentMutator<'_>,
253        document_id: NodeId,
254        source: &str,
255    ) -> Vec<Cow<'static, str>> {
256        let mut sink = DocumentHtmlParser::new(mutr);
257        sink.document_id = Some(document_id);
258        sink.is_xml = false;
259        sink.allow_declarative_shadow_roots = true;
260        let opts = ParseOpts {
261            tokenizer: TokenizerOpts::default(),
262            tree_builder: TreeBuilderOpts {
263                exact_errors: false,
264                scripting_enabled: false,
265                iframe_srcdoc: false,
266                drop_doctype: false,
267                quirks_mode: QuirksMode::NoQuirks,
268            },
269        };
270        html5ever::parse_document(sink, opts).one(source)
271    }
272
273    pub fn parse_inner_html_into_mutator<'a, 'd>(
274        mutr: &'a mut DocumentMutator<'d>,
275        element_id: NodeId,
276        html: &str,
277        allow_declarative_shadow_roots: bool,
278    ) {
279        let mut sink = DocumentHtmlParser::new(mutr);
280        sink.parsing_document = false;
281        sink.allow_declarative_shadow_roots = allow_declarative_shadow_roots;
282
283        let opts = ParseOpts {
284            tokenizer: TokenizerOpts::default(),
285            tree_builder: TreeBuilderOpts {
286                exact_errors: false,
287                scripting_enabled: true, // scripting runs, so noscript contents are text
288                iframe_srcdoc: false,
289                drop_doctype: true,
290                quirks_mode: QuirksMode::NoQuirks,
291            },
292        };
293        html5ever::driver::parse_fragment_for_element(sink, opts, element_id, false, None)
294            .from_utf8()
295            .read_from(&mut html.as_bytes())
296            .unwrap();
297
298        // html5ever creates a new fragment root node under the document node and parses the nodes into that fragment root.
299        // So here we move the children of the fragment root to the context's insertion target and then drop the fragment root.
300        let document_id = mutr.doc.root_node().id;
301        let fragment_root_id = mutr.last_child_id(document_id).unwrap();
302        let child_ids = mutr.child_ids(fragment_root_id);
303        let target_id = mutr
304            .doc
305            .get_node(element_id)
306            .and_then(|node| node.element_data())
307            .and_then(|element| element.template_contents)
308            .unwrap_or(element_id);
309        mutr.append_children(target_id, &child_ids);
310        mutr.remove_and_drop_node(fragment_root_id);
311    }
312}
313
314impl<'m, 'doc> TreeSink for DocumentHtmlParser<'m, 'doc> {
315    type Output = Vec<Cow<'static, str>>;
316
317    // we use the ID of the nodes in the tree as the handle
318    type Handle = NodeId;
319
320    type ElemName<'a>
321        = Ref<'a, QualName>
322    where
323        Self: 'a;
324
325    fn finish(self) -> Self::Output {
326        #[cfg(feature = "tracing")]
327        for error in self.errors.borrow().iter() {
328            tracing::error!("{error}");
329        }
330        self.errors.into_inner()
331    }
332
333    fn parse_error(&self, msg: Cow<'static, str>) {
334        self.errors.borrow_mut().push(msg);
335    }
336
337    fn get_document(&self) -> Self::Handle {
338        self.document_id
339            .unwrap_or_else(|| self.document_mutator.borrow().doc.root_node().id)
340    }
341
342    fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
343        Ref::map(self.document_mutator.borrow(), |docm| {
344            docm.element_name(*target)
345                .expect("TreeSink::elem_name called on a node which is not an element!")
346        })
347    }
348
349    fn create_element(
350        &self,
351        name: QualName,
352        attrs: Vec<html5ever::Attribute>,
353        _flags: ElementFlags,
354    ) -> Self::Handle {
355        let inert_script = self.document_id.is_some() && name.local.as_ref() == "script";
356        let attrs = attrs.into_iter().map(html5ever_to_blitz_attr).collect();
357        let id = self.mutr().create_element(name, attrs);
358        self.adopt_created(id);
359        if inert_script {
360            self.mutr()
361                .doc
362                .get_node_mut(id)
363                .expect("new parser element missing")
364                .flags
365                .insert(NodeFlags::IS_PARSER_INERT_SCRIPT);
366        }
367        id
368    }
369
370    fn create_comment(&self, text: StrTendril) -> Self::Handle {
371        let id = self.mutr().create_comment_node(&text);
372        self.adopt_created(id);
373        id
374    }
375
376    fn create_pi(&self, target: StrTendril, data: StrTendril) -> Self::Handle {
377        if self.document_id.is_none() {
378            return self.mutr().create_comment_node("");
379        }
380        let id = self.mutr().create_comment_node(&data);
381        self.adopt_created(id);
382        self.mutr()
383            .doc
384            .get_node_mut(id)
385            .expect("new processing instruction missing")
386            .markup = Some(Arc::new(MarkupNode::ProcessingInstruction {
387            target: target.to_string(),
388        }));
389        id
390    }
391
392    fn append(&self, parent_id: &Self::Handle, child: NodeOrText<Self::Handle>) {
393        match child {
394            NodeOrText::AppendNode(id) => self.mutr().append_children(*parent_id, &[id]),
395            // If content to append is text, first attempt to append it to the last child of parent.
396            // Else create a new text node and append it to the parent
397            NodeOrText::AppendText(text) => {
398                let last_child_id = self.mutr().last_child_id(*parent_id);
399                let has_appended = if let Some(id) = last_child_id {
400                    self.mutr().append_text_to_node(id, &text).is_ok()
401                } else {
402                    false
403                };
404                if !has_appended {
405                    let new_child_id = self.mutr().create_text_node(&text);
406                    self.adopt_created(new_child_id);
407                    self.mutr().append_children(*parent_id, &[new_child_id]);
408                }
409            }
410        }
411    }
412
413    // Note: The tree builder promises we won't have a text node after the insertion point.
414    // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_contexts
415    fn append_before_sibling(&self, sibling_id: &Self::Handle, new_node: NodeOrText<Self::Handle>) {
416        match new_node {
417            NodeOrText::AppendNode(id) => self.mutr().insert_nodes_before(*sibling_id, &[id]),
418            // If content to append is text, first attempt to append it to the node before sibling_node
419            // Else create a new text node and insert it before sibling_node
420            NodeOrText::AppendText(text) => {
421                let previous_sibling_id = self.mutr().previous_sibling_id(*sibling_id);
422                let has_appended = if let Some(id) = previous_sibling_id {
423                    self.mutr().append_text_to_node(id, &text).is_ok()
424                } else {
425                    false
426                };
427                if !has_appended {
428                    let new_child_id = self.mutr().create_text_node(&text);
429                    self.adopt_created(new_child_id);
430                    self.mutr()
431                        .insert_nodes_before(*sibling_id, &[new_child_id]);
432                }
433            }
434        };
435    }
436
437    fn append_based_on_parent_node(
438        &self,
439        element: &Self::Handle,
440        prev_element: &Self::Handle,
441        child: NodeOrText<Self::Handle>,
442    ) {
443        if self.mutr().node_has_parent(*element) {
444            self.append_before_sibling(element, child);
445        } else {
446            self.append(prev_element, child);
447        }
448    }
449
450    fn append_doctype_to_document(
451        &self,
452        name: StrTendril,
453        public_id: StrTendril,
454        system_id: StrTendril,
455    ) {
456        let document_id = self.get_document();
457        let mut mutr = self.mutr();
458        let id = mutr.create_comment_node("");
459        mutr.adopt_node(id, document_id);
460        mutr.doc
461            .get_node_mut(id)
462            .expect("new doctype missing")
463            .markup = Some(Arc::new(MarkupNode::Doctype {
464            name: name.to_string(),
465            public_id: public_id.to_string(),
466            system_id: system_id.to_string(),
467        }));
468        mutr.append_children(document_id, &[id]);
469    }
470
471    fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle {
472        // While a declarative template is open, its children belong to the
473        // shadow root. The association is on the document: a streaming parse
474        // builds a fresh parser for every tree-sink call.
475        #[cfg(feature = "shadow-dom")]
476        if let Some(shadow) = self.mutr().declarative_shadow_for_template(*target) {
477            return shadow;
478        }
479        self.mutr().template_contents(*target)
480    }
481
482    fn allow_declarative_shadow_roots(&self, _intended_parent: &Self::Handle) -> bool {
483        // The feature gate is compile-time. Leaving the flag true without the
484        // shadow-dom feature would make html5ever build an off-tree template
485        // and then a second, inserted one.
486        self.allow_declarative_shadow_roots && cfg!(feature = "shadow-dom")
487    }
488
489    fn attach_declarative_shadow(
490        &self,
491        location: &Self::Handle,
492        template: &Self::Handle,
493        attrs: &[html5ever::Attribute],
494    ) -> bool {
495        #[cfg(not(feature = "shadow-dom"))]
496        {
497            let _ = (location, template, attrs);
498            return false;
499        }
500        #[cfg(feature = "shadow-dom")]
501        {
502            let mut mode = None;
503            let mut clonable = false;
504            let mut serializable = false;
505            let mut delegates_focus = false;
506            for attr in attrs {
507                let name = attr.name.local.as_ref();
508                if name == "shadowrootmode" {
509                    let value = attr.value.as_ref();
510                    if value.eq_ignore_ascii_case("open") {
511                        mode = Some(blitz_dom::node::ShadowRootMode::Open);
512                    } else if value.eq_ignore_ascii_case("closed") {
513                        mode = Some(blitz_dom::node::ShadowRootMode::Closed);
514                    }
515                } else if name == "shadowrootclonable" {
516                    clonable = true;
517                } else if name == "shadowrootserializable" {
518                    serializable = true;
519                } else if name == "shadowrootdelegatesfocus" {
520                    delegates_focus = true;
521                }
522            }
523            let Some(mode) = mode else {
524                return false;
525            };
526            self.mutr().try_attach_declarative_shadow(
527                *location,
528                *template,
529                mode,
530                clonable,
531                serializable,
532                delegates_focus,
533            )
534        }
535    }
536
537    fn pop(&self, node: &Self::Handle) {
538        // html5ever inserts a declarative candidate off-tree. On failure it
539        // pops that node and inserts a different element for the same tag, so
540        // the off-tree node has to be dropped here or it leaks. On success the
541        // template is never inserted; the end-tag pop drops it after its
542        // children have gone to the shadow root.
543        let drop_template = {
544            let mutr = self.mutr();
545            mutr.doc.get_node(*node).is_some_and(|current| {
546                current.parent.is_none()
547                    && current
548                        .element_data()
549                        .is_some_and(|element| element.name.local.as_ref() == "template")
550            })
551        };
552        if !drop_template {
553            return;
554        }
555        #[cfg(feature = "shadow-dom")]
556        self.mutr().forget_declarative_template(*node);
557        self.mutr().remove_and_drop_node(*node);
558    }
559
560    fn same_node(&self, x: &Self::Handle, y: &Self::Handle) -> bool {
561        x == y
562    }
563
564    fn set_quirks_mode(&self, mode: QuirksMode) {
565        self.quirks_mode.set(mode);
566        if self.parsing_document {
567            self.mutr().doc.set_document_quirks_mode(match mode {
568                QuirksMode::NoQuirks => 0,
569                QuirksMode::LimitedQuirks => 1,
570                QuirksMode::Quirks => 2,
571            });
572        }
573    }
574
575    fn add_attrs_if_missing(&self, target: &Self::Handle, attrs: Vec<html5ever::Attribute>) {
576        let attrs = attrs.into_iter().map(html5ever_to_blitz_attr).collect();
577        self.mutr().add_attrs_if_missing(*target, attrs);
578    }
579
580    fn remove_from_parent(&self, target: &Self::Handle) {
581        self.mutr().remove_node(*target);
582    }
583
584    fn reparent_children(&self, old_parent_id: &Self::Handle, new_parent_id: &Self::Handle) {
585        self.mutr()
586            .reparent_children(*old_parent_id, *new_parent_id);
587    }
588}
589
590#[test]
591fn parses_some_html() {
592    use blitz_dom::{BaseDocument, DocumentConfig};
593
594    let html = "<!DOCTYPE html><html><body><h1>hello world</h1></body></html>";
595    let mut doc = BaseDocument::new(DocumentConfig::default());
596    let mut mutr = doc.mutate();
597    let sink = DocumentHtmlParser::new(&mut mutr);
598
599    html5ever::parse_document(sink, Default::default())
600        .from_utf8()
601        .read_from(&mut html.as_bytes())
602        .unwrap();
603
604    drop(mutr);
605    doc.print_tree()
606
607    // Now our tree should have some nodes in it
608}