Skip to main content

html5ever/serialize/
mod.rs

1// Copyright 2014-2017 The html5ever Project Developers. See the
2// COPYRIGHT file at the top-level directory of this distribution.
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10use 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    /// Is scripting enabled? Default: true
30    pub scripting_enabled: bool,
31
32    /// Serialize the root node? Default: ChildrenOnly
33    pub traversal_scope: TraversalScope,
34
35    /// If the serializer is asked to serialize an invalid tree, the default
36    /// behavior is to panic in the event that an `end_elem` is created without a
37    /// matching `start_elem`. Setting this to true will prevent those panics by
38    /// creating a default parent on the element stack. No extra start elem will
39    /// actually be written. Default: false
40    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            // FIXME(#122)
70            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        // When in attribute mode quotes are escaped, but otherwise not. In order to reduce
107        // branching below, when not in attribute mode, just look for the another of the
108        // escaped characters.
109        let maybe_quote = if attr_mode { b'"' } else { b'<' };
110        let find_next_escaped_character = |slice: &[u8]| {
111            // Use highly-optimized memchr to find the next character that needs to be escaped.
112            // Doing this twice is *much* faster than walking the string by characters.
113            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            // Write all text before the search result unconditionally.
123            self.writer.write_all(&bytes[search_start..next_special])?;
124
125            // If we reached the end of the text we can stop processing.
126            if next_special == bytes.len() {
127                break;
128            }
129
130            search_start = next_special + 1;
131            let replacement = match bytes[next_special] {
132                b'&' => "&amp;",
133                b'"' => "&quot;",
134                b'<' => "&lt;",
135                b'>' => "&gt;",
136                0xC2 if bytes.get(next_special + 1) == Some(&0xA0) => {
137                    search_start += 1;
138                    "&nbsp;"
139                },
140                _ => {
141                    //  0xC2 not followed by 0xA0 (not NBSP), so keep looking.
142                    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                    // FIXME(#122)
187                    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}