Skip to main content

serval_scripted_dom/
serialize.rs

1//! HTML serialization of the scripted DOM via html5ever's serializer — the inverse
2//! of the `TreeSink` parse path serval-static-dom already implements. Powers
3//! `outerHTML` / `innerHTML` and the verso flip's DOM-snapshot layer.
4//!
5//! This side only walks the tree and names start/end elements, text, comments, and
6//! doctype; html5ever owns the correctness it is famous for: void elements (no close
7//! tag), attribute and text escaping, and raw-text elements (`<script>`/`<style>`).
8//! Mirrors servo's `dom/node.rs` serialize.
9
10use std::io;
11
12use html5ever::serialize::{SerializeOpts, serialize as html5ever_serialize};
13use layout_dom_api::NodeKind;
14use markup5ever::serialize::{Serialize, Serializer, TraversalScope};
15
16use crate::{NodeId, ScriptedDom};
17
18/// A `(ScriptedDom, NodeId)` pair html5ever's serializer can walk.
19struct SerializableNode<'a> {
20    dom: &'a ScriptedDom,
21    id: NodeId,
22}
23
24impl SerializableNode<'_> {
25    fn child(&self, id: NodeId) -> SerializableNode<'_> {
26        SerializableNode { dom: self.dom, id }
27    }
28
29    /// Serialize every child as its own included node (shared by element bodies,
30    /// the document/fragment roots, and `ChildrenOnly`).
31    fn serialize_children<S: Serializer>(&self, serializer: &mut S) -> io::Result<()> {
32        for &child in &self.dom.node(self.id).children {
33            self.child(child)
34                .serialize(serializer, TraversalScope::IncludeNode)?;
35        }
36        Ok(())
37    }
38}
39
40impl Serialize for SerializableNode<'_> {
41    fn serialize<S: Serializer>(
42        &self,
43        serializer: &mut S,
44        traversal_scope: TraversalScope,
45    ) -> io::Result<()> {
46        let node = self.dom.node(self.id);
47        match traversal_scope {
48            TraversalScope::IncludeNode => match node.kind {
49                NodeKind::Element => match &node.name {
50                    Some(name) => {
51                        serializer.start_elem(
52                            name.clone(),
53                            node.attrs.iter().map(|(n, v)| (n, v.as_str())),
54                        )?;
55                        self.serialize_children(serializer)?;
56                        serializer.end_elem(name.clone())?;
57                    },
58                    // A nameless "element" is malformed; emit its children rather
59                    // than a bare tag.
60                    None => self.serialize_children(serializer)?,
61                },
62                NodeKind::Text => {
63                    if let Some(text) = &node.text {
64                        serializer.write_text(text)?;
65                    }
66                },
67                NodeKind::Comment => {
68                    if let Some(text) = &node.text {
69                        serializer.write_comment(text)?;
70                    }
71                },
72                NodeKind::Doctype => {
73                    serializer.write_doctype(node.text.as_deref().unwrap_or("html"))?;
74                },
75                NodeKind::Document | NodeKind::DocumentFragment => {
76                    self.serialize_children(serializer)?;
77                },
78                // Processing instructions are not valid HTML (html5ever parses them
79                // as bogus comments); the scripted DOM never holds one, so emit
80                // nothing rather than invalid markup.
81                NodeKind::ProcessingInstruction => {},
82            },
83            TraversalScope::ChildrenOnly(_) => self.serialize_children(serializer)?,
84        }
85        Ok(())
86    }
87}
88
89impl ScriptedDom {
90    /// Serialize `node` and its subtree to HTML (`outerHTML`).
91    ///
92    /// Routes through html5ever's serializer, so void elements, attribute/text
93    /// escaping, and raw-text elements are handled by the engine, not here.
94    pub fn outer_html(&self, node: NodeId) -> String {
95        self.serialize_scope(node, TraversalScope::IncludeNode)
96    }
97
98    /// Serialize only `node`'s children (`innerHTML`).
99    pub fn inner_html(&self, node: NodeId) -> String {
100        self.serialize_scope(node, TraversalScope::ChildrenOnly(None))
101    }
102
103    fn serialize_scope(&self, node: NodeId, traversal_scope: TraversalScope) -> String {
104        let mut buf = Vec::new();
105        let opts = SerializeOpts {
106            traversal_scope,
107            ..SerializeOpts::default()
108        };
109        // Writing to an in-memory Vec is infallible; html5ever emits UTF-8.
110        html5ever_serialize(
111            &mut buf,
112            &SerializableNode {
113                dom: self,
114                id: node,
115            },
116            opts,
117        )
118        .expect("serializing the scripted DOM to a Vec is infallible");
119        String::from_utf8(buf).expect("html5ever emits valid UTF-8")
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use html5ever::{local_name, ns};
127    use layout_dom_api::{LayoutDomMut, QualName};
128
129    fn html(local: markup5ever::LocalName) -> QualName {
130        QualName::new(None, ns!(html), local)
131    }
132
133    #[test]
134    fn outer_html_emits_tag_attrs_and_escaped_text() {
135        let mut dom = ScriptedDom::new();
136        let div = dom.create_element(html(local_name!("div")));
137        dom.set_attribute(div, QualName::new(None, ns!(), local_name!("class")), "x");
138        let text = dom.create_text("hi & bye");
139        dom.append_child(div, text);
140        // html5ever owns the `&` escaping.
141        assert_eq!(dom.outer_html(div), r#"<div class="x">hi &amp; bye</div>"#);
142    }
143
144    #[test]
145    fn inner_html_skips_the_node_itself() {
146        let mut dom = ScriptedDom::new();
147        let div = dom.create_element(html(local_name!("div")));
148        let span = dom.create_element(html(local_name!("span")));
149        dom.append_child(div, span);
150        assert_eq!(dom.inner_html(div), "<span></span>");
151    }
152
153    #[test]
154    fn void_elements_get_no_close_tag() {
155        let mut dom = ScriptedDom::new();
156        let br = dom.create_element(html(local_name!("br")));
157        assert_eq!(dom.outer_html(br), "<br>");
158    }
159}