pagesift_html/
serialize.rs1use crate::{NodeRef, Tag};
5
6impl NodeRef<'_> {
7 pub fn html(&self) -> String {
9 let mut out = String::new();
10 write_tree(*self, &mut out, true);
11 out
12 }
13
14 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
24fn 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
51fn 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
60fn 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 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'&' => "&",
135 b'<' => "<",
136 b'>' => ">",
137 b'"' if attr => """,
138 0xA0 if i > 0 && b[i - 1] == 0xC2 => " ",
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"b\" title='x\u{a0}y'>1 < 2 & <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<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"b\" title=\"x y\">1 < 2 & <br><img src=\"x\"> <!--c-->\
168 <script>if (a < b) {}</script><style>a>b{}</style><noscript><b>n</b></noscript><svg><style>s<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 < b) {}");
178 }
179}