html5ever/serialize/
mod.rs1use log::warn;
11pub use markup5ever::serialize::{AttrRef, Serialize, Serializer, TraversalScope};
12use markup5ever::{local_name, ns};
13use memchr::{memchr2, memchr3};
14use std::io::{self, Write};
15
16use crate::{LocalName, QualName};
17
18pub fn serialize<Wr, T>(writer: Wr, node: &T, opts: SerializeOpts) -> io::Result<()>
19where
20 Wr: Write,
21 T: Serialize,
22{
23 let mut ser = HtmlSerializer::new(writer, opts.clone());
24 node.serialize(&mut ser, opts.traversal_scope)
25}
26
27#[derive(Clone)]
28pub struct SerializeOpts {
29 pub scripting_enabled: bool,
31
32 pub traversal_scope: TraversalScope,
34
35 pub create_missing_parent: bool,
41}
42
43impl Default for SerializeOpts {
44 fn default() -> SerializeOpts {
45 SerializeOpts {
46 scripting_enabled: true,
47 traversal_scope: TraversalScope::ChildrenOnly(None),
48 create_missing_parent: false,
49 }
50 }
51}
52
53#[derive(Default)]
54struct ElemInfo {
55 html_name: Option<LocalName>,
56 ignore_children: bool,
57}
58
59pub struct HtmlSerializer<Wr: Write> {
60 pub writer: Wr,
61 opts: SerializeOpts,
62 stack: Vec<ElemInfo>,
63}
64
65fn tagname(name: &QualName) -> LocalName {
66 match name.ns {
67 ns!(html) | ns!(mathml) | ns!(svg) => (),
68 ref ns => {
69 warn!("node with weird namespace {ns:?}");
71 },
72 }
73
74 name.local.clone()
75}
76
77impl<Wr: Write> HtmlSerializer<Wr> {
78 pub fn new(writer: Wr, opts: SerializeOpts) -> Self {
79 let html_name = match opts.traversal_scope {
80 TraversalScope::IncludeNode | TraversalScope::ChildrenOnly(None) => None,
81 TraversalScope::ChildrenOnly(Some(ref n)) => Some(tagname(n)),
82 };
83 HtmlSerializer {
84 writer,
85 opts,
86 stack: vec![ElemInfo {
87 html_name,
88 ignore_children: false,
89 }],
90 }
91 }
92
93 fn parent(&mut self) -> &mut ElemInfo {
94 if self.stack.is_empty() {
95 if self.opts.create_missing_parent {
96 warn!("ElemInfo stack empty, creating new parent");
97 self.stack.push(Default::default());
98 } else {
99 panic!("no parent ElemInfo")
100 }
101 }
102 self.stack.last_mut().unwrap()
103 }
104
105 fn write_escaped(&mut self, text: &str, attr_mode: bool) -> io::Result<()> {
106 let maybe_quote = if attr_mode { b'"' } else { b'<' };
110 let find_next_escaped_character = |slice: &[u8]| {
111 let result = memchr3(maybe_quote, b'<', b'>', slice).unwrap_or(slice.len());
114 memchr2(b'&', 0xC2, &slice[..result]).unwrap_or(result)
115 };
116
117 let bytes = text.as_bytes();
118 let mut search_start = 0;
119 while search_start < text.len() {
120 let next_special = find_next_escaped_character(&bytes[search_start..]) + search_start;
121
122 self.writer.write_all(&bytes[search_start..next_special])?;
124
125 if next_special == bytes.len() {
127 break;
128 }
129
130 search_start = next_special + 1;
131 let replacement = match bytes[next_special] {
132 b'&' => "&",
133 b'"' => """,
134 b'<' => "<",
135 b'>' => ">",
136 0xC2 if bytes.get(next_special + 1) == Some(&0xA0) => {
137 search_start += 1;
138 " "
139 },
140 _ => {
141 self.writer.write_all(&bytes[next_special..search_start])?;
143 continue;
144 },
145 };
146 self.writer.write_all(replacement.as_bytes())?;
147 }
148
149 Ok(())
150 }
151}
152
153impl<Wr: Write> Serializer for HtmlSerializer<Wr> {
154 fn start_elem<'a, AttrIter>(&mut self, name: QualName, attrs: AttrIter) -> io::Result<()>
155 where
156 AttrIter: Iterator<Item = AttrRef<'a>>,
157 {
158 let html_name = match name.ns {
159 ns!(html) => Some(name.local.clone()),
160 _ => None,
161 };
162
163 if self.parent().ignore_children {
164 self.stack.push(ElemInfo {
165 html_name,
166 ignore_children: true,
167 });
168 return Ok(());
169 }
170
171 self.writer.write_all(b"<")?;
172 self.writer.write_all(tagname(&name).as_bytes())?;
173 for (name, value) in attrs {
174 self.writer.write_all(b" ")?;
175
176 match name.ns {
177 ns!() => (),
178 ns!(xml) => self.writer.write_all(b"xml:")?,
179 ns!(xmlns) => {
180 if name.local != local_name!("xmlns") {
181 self.writer.write_all(b"xmlns:")?;
182 }
183 },
184 ns!(xlink) => self.writer.write_all(b"xlink:")?,
185 ref ns => {
186 warn!("attr with weird namespace {ns:?}");
188 self.writer.write_all(b"unknown_namespace:")?;
189 },
190 }
191
192 self.writer.write_all(name.local.as_bytes())?;
193 self.writer.write_all(b"=\"")?;
194 self.write_escaped(value, true)?;
195 self.writer.write_all(b"\"")?;
196 }
197 self.writer.write_all(b">")?;
198
199 let ignore_children = name.ns == ns!(html)
200 && matches!(
201 name.local,
202 local_name!("area")
203 | local_name!("base")
204 | local_name!("basefont")
205 | local_name!("bgsound")
206 | local_name!("br")
207 | local_name!("col")
208 | local_name!("embed")
209 | local_name!("frame")
210 | local_name!("hr")
211 | local_name!("img")
212 | local_name!("input")
213 | local_name!("keygen")
214 | local_name!("link")
215 | local_name!("meta")
216 | local_name!("param")
217 | local_name!("source")
218 | local_name!("track")
219 | local_name!("wbr")
220 );
221
222 self.stack.push(ElemInfo {
223 html_name,
224 ignore_children,
225 });
226
227 Ok(())
228 }
229
230 fn end_elem(&mut self, name: QualName) -> io::Result<()> {
231 let info = match self.stack.pop() {
232 Some(info) => info,
233 None if self.opts.create_missing_parent => {
234 warn!("missing ElemInfo, creating default.");
235 Default::default()
236 },
237 _ => panic!("no ElemInfo"),
238 };
239 if info.ignore_children {
240 return Ok(());
241 }
242
243 self.writer.write_all(b"</")?;
244 self.writer.write_all(tagname(&name).as_bytes())?;
245 self.writer.write_all(b">")
246 }
247
248 fn write_text(&mut self, text: &str) -> io::Result<()> {
249 let escape = match self.parent().html_name {
250 Some(local_name!("style"))
251 | Some(local_name!("script"))
252 | Some(local_name!("xmp"))
253 | Some(local_name!("iframe"))
254 | Some(local_name!("noembed"))
255 | Some(local_name!("noframes"))
256 | Some(local_name!("plaintext")) => false,
257
258 Some(local_name!("noscript")) => !self.opts.scripting_enabled,
259
260 _ => true,
261 };
262
263 if escape {
264 self.write_escaped(text, false)
265 } else {
266 self.writer.write_all(text.as_bytes())
267 }
268 }
269
270 fn write_comment(&mut self, text: &str) -> io::Result<()> {
271 self.writer.write_all(b"<!--")?;
272 self.writer.write_all(text.as_bytes())?;
273 self.writer.write_all(b"-->")
274 }
275
276 fn write_doctype(&mut self, name: &str) -> io::Result<()> {
277 self.writer.write_all(b"<!DOCTYPE ")?;
278 self.writer.write_all(name.as_bytes())?;
279 self.writer.write_all(b">")
280 }
281
282 fn write_processing_instruction(&mut self, target: &str, data: &str) -> io::Result<()> {
283 self.writer.write_all(b"<?")?;
284 self.writer.write_all(target.as_bytes())?;
285 self.writer.write_all(b" ")?;
286 self.writer.write_all(data.as_bytes())?;
287 self.writer.write_all(b">")
288 }
289}