Skip to main content

pagesift_html/
serialize.rs

1//! 把子树序列化回 HTML,转义和空元素规则和 html5ever 的序列化器一致(scraper 的 `ElementRef::html` 用的就是它)。
2//! 不同的是属性保持源码顺序(scraper 按名字排序),和 BeautifulSoup 一样。
3
4use crate::{NodeRef, Tag};
5
6impl NodeRef<'_> {
7    /// 节点自身及其子树的 HTML(元素的 outer HTML)。
8    pub fn html(&self) -> String {
9        let mut out = String::new();
10        write_tree(*self, &mut out, true);
11        out
12    }
13
14    /// 子节点的 HTML(元素的 inner HTML)。
15    pub fn inner_html(&self) -> String {
16        let mut out = String::new();
17        for c in self.children() {
18            write_tree(c, &mut out, false);
19        }
20        out
21    }
22}
23
24/// 不写子节点和结束标签的元素。
25fn is_void(tag: Option<Tag>) -> bool {
26    use Tag::*;
27    matches!(
28        tag,
29        Some(
30            Area | Base
31                | Basefont
32                | Bgsound
33                | Br
34                | Col
35                | Embed
36                | Frame
37                | Hr
38                | Img
39                | Input
40                | Keygen
41                | Link
42                | Meta
43                | Param
44                | Source
45                | Track
46                | Wbr
47        )
48    )
49}
50
51/// 文本不转义的父元素。noscript 也会被转义:scraper 序列化时关掉了 scripting。
52fn is_raw_text(tag: Option<Tag>) -> bool {
53    use Tag::*;
54    matches!(
55        tag,
56        Some(Style | Script | Xmp | Iframe | Noembed | Noframes | Plaintext)
57    )
58}
59
60/// 非递归地先序遍历 `root` 的子树。`root_is_top` 时 `root` 的文本按没有父元素处理(总是转义)。
61fn write_tree(root: NodeRef<'_>, out: &mut String, root_is_top: bool) {
62    let mut n = root;
63    loop {
64        let raw = !(root_is_top && n == root) && is_raw_text(n.parent().and_then(|p| p.tag()));
65        open(n, out, raw);
66        if !is_void(n.tag())
67            && let Some(c) = n.first_child()
68        {
69            n = c;
70            continue;
71        }
72        loop {
73            close(n, out);
74            if n == root {
75                return;
76            }
77            match n.next_sibling() {
78                Some(s) => {
79                    n = s;
80                    break;
81                }
82                None => match n.parent() {
83                    Some(p) => n = p,
84                    None => return,
85                },
86            }
87        }
88    }
89}
90
91fn open(n: NodeRef<'_>, out: &mut String, raw: bool) {
92    if let Some(e) = n.as_element() {
93        out.push('<');
94        out.push_str(e.name());
95        for (k, v) in e.attrs() {
96            out.push(' ');
97            out.push_str(k);
98            out.push_str("=\"");
99            escape(v, true, out);
100            out.push('"');
101        }
102        out.push('>');
103    } else if let Some(t) = n.as_text() {
104        if raw {
105            out.push_str(t);
106        } else {
107            escape(t, false, out);
108        }
109    } else if let Some(c) = n.as_comment() {
110        out.push_str("<!--");
111        out.push_str(c);
112        out.push_str("-->");
113    } else if !n.is_document() {
114        // 树里不保留 doctype 的名字,HTML 文档里它总是 html
115        out.push_str("<!DOCTYPE html>");
116    }
117}
118
119fn close(n: NodeRef<'_>, out: &mut String) {
120    if let Some(e) = n.as_element()
121        && !is_void(Some(e.tag()))
122    {
123        out.push_str("</");
124        out.push_str(e.name());
125        out.push('>');
126    }
127}
128
129fn escape(s: &str, attr: bool, out: &mut String) {
130    let b = s.as_bytes();
131    let mut last = 0;
132    for (i, &c) in b.iter().enumerate() {
133        let rep = match c {
134            b'&' => "&amp;",
135            b'<' => "&lt;",
136            b'>' => "&gt;",
137            b'"' if attr => "&quot;",
138            // U+00A0 的 UTF-8 编码是 C2 A0
139            0xA0 if i > 0 && b[i - 1] == 0xC2 => "&nbsp;",
140            _ => continue,
141        };
142        let start = if c == 0xA0 { i - 1 } else { i };
143        out.push_str(&s[last..start]);
144        out.push_str(rep);
145        last = i + 1;
146    }
147    out.push_str(&s[last..]);
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::{Html, Tag};
153
154    #[test]
155    fn outer_and_inner_html() {
156        let doc = Html::parse(
157            "<div class=\"a&quot;b\" title='x\u{a0}y'>1 &lt; 2 &amp; <br><img src=x>\u{a0}<!--c-->\
158             <script>if (a < b) {}</script><style>a>b{}</style><noscript><b>n</b></noscript><svg><style>s&lt;x</style></svg></div>",
159        );
160        let div = doc
161            .root()
162            .descendants()
163            .find(|n| n.tag() == Some(Tag::Div))
164            .unwrap();
165        assert_eq!(
166            div.html(),
167            "<div class=\"a&quot;b\" title=\"x&nbsp;y\">1 &lt; 2 &amp; <br><img src=\"x\">&nbsp;<!--c-->\
168             <script>if (a < b) {}</script><style>a>b{}</style><noscript>&lt;b&gt;n&lt;/b&gt;</noscript><svg><style>s&lt;x</style></svg></div>"
169        );
170        let br = div.children().find(|n| n.tag() == Some(Tag::Br)).unwrap();
171        assert_eq!(br.html(), "<br>");
172        let script = div
173            .children()
174            .find(|n| n.tag() == Some(Tag::Script))
175            .unwrap();
176        assert_eq!(script.inner_html(), "if (a < b) {}");
177        assert_eq!(script.first_child().unwrap().html(), "if (a &lt; b) {}");
178    }
179}