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};
8
9use blitz_dom::node::Attribute;
10use blitz_dom::{DocumentMutator, HtmlParserProvider, NodeId};
11use html5ever::{
12    QualName,
13    tendril::{StrTendril, TendrilSink},
14    tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink},
15};
16
17/// Convert an html5ever Attribute which uses tendril for its value to a blitz Attribute
18/// which uses String.
19fn html5ever_to_blitz_attr(attr: html5ever::Attribute) -> Attribute {
20    Attribute {
21        name: attr.name,
22        value: attr.value.to_string(),
23    }
24}
25
26#[derive(Copy, Clone, Default, Debug)]
27pub struct HtmlProvider;
28
29impl HtmlParserProvider for HtmlProvider {
30    fn parse_inner_html<'m2, 'doc2>(
31        &self,
32        mutr: &'m2 mut DocumentMutator<'doc2>,
33        element_id: NodeId,
34        html: &str,
35    ) {
36        DocumentHtmlParser::parse_inner_html_into_mutator(mutr, element_id, html);
37    }
38
39    fn parse_document(
40        &self,
41        html: &str,
42        config: blitz_dom::DocumentConfig,
43    ) -> Box<dyn blitz_dom::Document> {
44        Box::new(crate::HtmlDocument::from_html(html, config))
45    }
46}
47
48pub struct DocumentHtmlParser<'m, 'doc> {
49    document_mutator: RefCell<&'m mut DocumentMutator<'doc>>,
50
51    /// Errors that occurred during parsing.
52    pub errors: RefCell<Vec<Cow<'static, str>>>,
53
54    /// The document's quirks mode.
55    pub quirks_mode: Cell<QuirksMode>,
56    pub is_xml: bool,
57}
58
59impl<'m, 'doc> DocumentHtmlParser<'m, 'doc> {
60    #[track_caller]
61    /// Get a mutable borrow of the DocumentMutator
62    fn mutr(&self) -> RefMut<'_, &'m mut DocumentMutator<'doc>> {
63        self.document_mutator.borrow_mut()
64    }
65}
66
67impl<'m, 'doc> DocumentHtmlParser<'m, 'doc> {
68    pub fn new(mutr: &'m mut DocumentMutator<'doc>) -> DocumentHtmlParser<'m, 'doc> {
69        DocumentHtmlParser {
70            document_mutator: RefCell::new(mutr),
71            errors: RefCell::new(Vec::new()),
72            quirks_mode: Cell::new(QuirksMode::NoQuirks),
73            is_xml: false,
74        }
75    }
76
77    /// Detects documents without an XML or DOCTYPE declaration whose root `<html>` element
78    /// declares the XHTML namespace (e.g. `<html xmlns="http://www.w3.org/1999/xhtml">`)
79    fn root_element_has_xhtml_namespace(html: &str) -> bool {
80        let rest = html.trim_start_matches('\u{feff}').trim_start();
81        let Some(rest) = rest.strip_prefix("<html") else {
82            return false;
83        };
84        let Some(tag_end) = rest.find('>') else {
85            return false;
86        };
87        rest[..tag_end].contains("xmlns=\"http://www.w3.org/1999/xhtml\"")
88            || rest[..tag_end].contains("xmlns='http://www.w3.org/1999/xhtml'")
89    }
90
91    pub fn parse_into_mutator<'a, 'd>(mutr: &'a mut DocumentMutator<'d>, html: &str) {
92        let is_xhtml_doc = html.starts_with("<?xml")
93            || html.starts_with("<!DOCTYPE") && {
94                let first_line = html.lines().next().unwrap();
95                first_line.contains("XHTML") || first_line.contains("xhtml")
96            }
97            || Self::root_element_has_xhtml_namespace(html);
98
99        if is_xhtml_doc {
100            Self::parse_xml_into_mutator(mutr, html);
101        } else {
102            // Parse as HTML
103            let mut sink = DocumentHtmlParser::new(mutr);
104            sink.is_xml = false;
105            let opts = ParseOpts {
106                tokenizer: TokenizerOpts::default(),
107                tree_builder: TreeBuilderOpts {
108                    exact_errors: false,
109                    scripting_enabled: false, // Enables parsing of <noscript> tags
110                    iframe_srcdoc: false,
111                    drop_doctype: true,
112                    quirks_mode: QuirksMode::NoQuirks,
113                },
114            };
115            html5ever::parse_document(sink, opts)
116                .from_utf8()
117                .read_from(&mut html.as_bytes())
118                .unwrap();
119        }
120    }
121
122    /// Parse the input as XML (XHTML), regardless of its content.
123    ///
124    /// [`parse_into_mutator`](Self::parse_into_mutator) sniffs the content to decide between HTML
125    /// and XML parsing, but the sniffing cannot detect all XHTML documents (e.g. ones with an
126    /// `<!DOCTYPE html>` doctype). Callers which know the document is XHTML from out-of-band
127    /// information (a `Content-Type` header or an `.xht`/`.xhtml` file extension) should use
128    /// this method instead.
129    pub fn parse_xml_into_mutator<'a, 'd>(mutr: &'a mut DocumentMutator<'d>, xml: &str) {
130        let mut sink = DocumentHtmlParser::new(mutr);
131        sink.is_xml = true;
132        xml5ever::driver::parse_document(sink, Default::default())
133            .from_utf8()
134            .read_from(&mut xml.as_bytes())
135            .unwrap();
136    }
137
138    pub fn parse_inner_html_into_mutator<'a, 'd>(
139        mutr: &'a mut DocumentMutator<'d>,
140        element_id: NodeId,
141        html: &str,
142    ) {
143        let sink = DocumentHtmlParser::new(mutr);
144
145        let opts = ParseOpts {
146            tokenizer: TokenizerOpts::default(),
147            tree_builder: TreeBuilderOpts {
148                exact_errors: false,
149                scripting_enabled: false, // Enables parsing of <noscript> tags
150                iframe_srcdoc: false,
151                drop_doctype: true,
152                quirks_mode: QuirksMode::NoQuirks,
153            },
154        };
155        html5ever::driver::parse_fragment_for_element(sink, opts, element_id, false, None)
156            .from_utf8()
157            .read_from(&mut html.as_bytes())
158            .unwrap();
159
160        // html5ever creates a new fragment root node under the document node and parses the nodes into that fragment root.
161        // So here we move the children of the fragment root to element_id and then drop the fragment root.
162        let document_id = mutr.doc.root_node().id;
163        let fragment_root_id = mutr.last_child_id(document_id).unwrap();
164        let child_ids = mutr.child_ids(fragment_root_id);
165        mutr.append_children(element_id, &child_ids);
166        mutr.remove_and_drop_node(fragment_root_id);
167    }
168}
169
170impl<'m, 'doc> TreeSink for DocumentHtmlParser<'m, 'doc> {
171    type Output = ();
172
173    // we use the ID of the nodes in the tree as the handle
174    type Handle = NodeId;
175
176    type ElemName<'a>
177        = Ref<'a, QualName>
178    where
179        Self: 'a;
180
181    fn finish(self) -> Self::Output {
182        #[cfg(feature = "tracing")]
183        for error in self.errors.borrow().iter() {
184            tracing::error!("{error}");
185        }
186    }
187
188    fn parse_error(&self, msg: Cow<'static, str>) {
189        self.errors.borrow_mut().push(msg);
190    }
191
192    fn get_document(&self) -> Self::Handle {
193        self.document_mutator.borrow().doc.root_node().id
194    }
195
196    fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
197        Ref::map(self.document_mutator.borrow(), |docm| {
198            docm.element_name(*target)
199                .expect("TreeSink::elem_name called on a node which is not an element!")
200        })
201    }
202
203    fn create_element(
204        &self,
205        name: QualName,
206        attrs: Vec<html5ever::Attribute>,
207        _flags: ElementFlags,
208    ) -> Self::Handle {
209        let attrs = attrs.into_iter().map(html5ever_to_blitz_attr).collect();
210        self.mutr().create_element(name, attrs)
211    }
212
213    fn create_comment(&self, text: StrTendril) -> Self::Handle {
214        self.mutr().create_comment_node(&text)
215    }
216
217    fn create_pi(&self, _target: StrTendril, _data: StrTendril) -> Self::Handle {
218        self.mutr().create_comment_node("")
219    }
220
221    fn append(&self, parent_id: &Self::Handle, child: NodeOrText<Self::Handle>) {
222        match child {
223            NodeOrText::AppendNode(id) => self.mutr().append_children(*parent_id, &[id]),
224            // If content to append is text, first attempt to append it to the last child of parent.
225            // Else create a new text node and append it to the parent
226            NodeOrText::AppendText(text) => {
227                let last_child_id = self.mutr().last_child_id(*parent_id);
228                let has_appended = if let Some(id) = last_child_id {
229                    self.mutr().append_text_to_node(id, &text).is_ok()
230                } else {
231                    false
232                };
233                if !has_appended {
234                    let new_child_id = self.mutr().create_text_node(&text);
235                    self.mutr().append_children(*parent_id, &[new_child_id]);
236                }
237            }
238        }
239    }
240
241    // Note: The tree builder promises we won't have a text node after the insertion point.
242    // https://github.com/servo/html5ever/blob/main/rcdom/lib.rs#L338
243    fn append_before_sibling(&self, sibling_id: &Self::Handle, new_node: NodeOrText<Self::Handle>) {
244        match new_node {
245            NodeOrText::AppendNode(id) => self.mutr().insert_nodes_before(*sibling_id, &[id]),
246            // If content to append is text, first attempt to append it to the node before sibling_node
247            // Else create a new text node and insert it before sibling_node
248            NodeOrText::AppendText(text) => {
249                let previous_sibling_id = self.mutr().previous_sibling_id(*sibling_id);
250                let has_appended = if let Some(id) = previous_sibling_id {
251                    self.mutr().append_text_to_node(id, &text).is_ok()
252                } else {
253                    false
254                };
255                if !has_appended {
256                    let new_child_id = self.mutr().create_text_node(&text);
257                    self.mutr()
258                        .insert_nodes_before(*sibling_id, &[new_child_id]);
259                }
260            }
261        };
262    }
263
264    fn append_based_on_parent_node(
265        &self,
266        element: &Self::Handle,
267        prev_element: &Self::Handle,
268        child: NodeOrText<Self::Handle>,
269    ) {
270        if self.mutr().node_has_parent(*element) {
271            self.append_before_sibling(element, child);
272        } else {
273            self.append(prev_element, child);
274        }
275    }
276
277    fn append_doctype_to_document(
278        &self,
279        _name: StrTendril,
280        _public_id: StrTendril,
281        _system_id: StrTendril,
282    ) {
283        // Ignore. We don't care about the DOCTYPE for now.
284    }
285
286    fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle {
287        // TODO: implement templates properly. This should allow to function like regular elements.
288        *target
289    }
290
291    fn same_node(&self, x: &Self::Handle, y: &Self::Handle) -> bool {
292        x == y
293    }
294
295    fn set_quirks_mode(&self, mode: QuirksMode) {
296        self.quirks_mode.set(mode);
297    }
298
299    fn add_attrs_if_missing(&self, target: &Self::Handle, attrs: Vec<html5ever::Attribute>) {
300        let attrs = attrs.into_iter().map(html5ever_to_blitz_attr).collect();
301        self.mutr().add_attrs_if_missing(*target, attrs);
302    }
303
304    fn remove_from_parent(&self, target: &Self::Handle) {
305        self.mutr().remove_node(*target);
306    }
307
308    fn reparent_children(&self, old_parent_id: &Self::Handle, new_parent_id: &Self::Handle) {
309        self.mutr()
310            .reparent_children(*old_parent_id, *new_parent_id);
311    }
312}
313
314#[test]
315fn parses_some_html() {
316    use blitz_dom::{BaseDocument, DocumentConfig};
317
318    let html = "<!DOCTYPE html><html><body><h1>hello world</h1></body></html>";
319    let mut doc = BaseDocument::new(DocumentConfig::default());
320    let mut mutr = doc.mutate();
321    let sink = DocumentHtmlParser::new(&mut mutr);
322
323    html5ever::parse_document(sink, Default::default())
324        .from_utf8()
325        .read_from(&mut html.as_bytes())
326        .unwrap();
327
328    drop(mutr);
329    doc.print_tree()
330
331    // Now our tree should have some nodes in it
332}