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                    continue;
143                },
144            };
145            self.writer.write_all(replacement.as_bytes())?;
146        }
147
148        Ok(())
149    }
150}
151
152impl<Wr: Write> Serializer for HtmlSerializer<Wr> {
153    fn start_elem<'a, AttrIter>(&mut self, name: QualName, attrs: AttrIter) -> io::Result<()>
154    where
155        AttrIter: Iterator<Item = AttrRef<'a>>,
156    {
157        let html_name = match name.ns {
158            ns!(html) => Some(name.local.clone()),
159            _ => None,
160        };
161
162        if self.parent().ignore_children {
163            self.stack.push(ElemInfo {
164                html_name,
165                ignore_children: true,
166            });
167            return Ok(());
168        }
169
170        self.writer.write_all(b"<")?;
171        self.writer.write_all(tagname(&name).as_bytes())?;
172        for (name, value) in attrs {
173            self.writer.write_all(b" ")?;
174
175            match name.ns {
176                ns!() => (),
177                ns!(xml) => self.writer.write_all(b"xml:")?,
178                ns!(xmlns) => {
179                    if name.local != local_name!("xmlns") {
180                        self.writer.write_all(b"xmlns:")?;
181                    }
182                },
183                ns!(xlink) => self.writer.write_all(b"xlink:")?,
184                ref ns => {
185                    // FIXME(#122)
186                    warn!("attr with weird namespace {ns:?}");
187                    self.writer.write_all(b"unknown_namespace:")?;
188                },
189            }
190
191            self.writer.write_all(name.local.as_bytes())?;
192            self.writer.write_all(b"=\"")?;
193            self.write_escaped(value, true)?;
194            self.writer.write_all(b"\"")?;
195        }
196        self.writer.write_all(b">")?;
197
198        let ignore_children = name.ns == ns!(html)
199            && matches!(
200                name.local,
201                local_name!("area")
202                    | local_name!("base")
203                    | local_name!("basefont")
204                    | local_name!("bgsound")
205                    | local_name!("br")
206                    | local_name!("col")
207                    | local_name!("embed")
208                    | local_name!("frame")
209                    | local_name!("hr")
210                    | local_name!("img")
211                    | local_name!("input")
212                    | local_name!("keygen")
213                    | local_name!("link")
214                    | local_name!("meta")
215                    | local_name!("param")
216                    | local_name!("source")
217                    | local_name!("track")
218                    | local_name!("wbr")
219            );
220
221        self.stack.push(ElemInfo {
222            html_name,
223            ignore_children,
224        });
225
226        Ok(())
227    }
228
229    fn end_elem(&mut self, name: QualName) -> io::Result<()> {
230        let info = match self.stack.pop() {
231            Some(info) => info,
232            None if self.opts.create_missing_parent => {
233                warn!("missing ElemInfo, creating default.");
234                Default::default()
235            },
236            _ => panic!("no ElemInfo"),
237        };
238        if info.ignore_children {
239            return Ok(());
240        }
241
242        self.writer.write_all(b"</")?;
243        self.writer.write_all(tagname(&name).as_bytes())?;
244        self.writer.write_all(b">")
245    }
246
247    fn write_text(&mut self, text: &str) -> io::Result<()> {
248        let escape = match self.parent().html_name {
249            Some(local_name!("style"))
250            | Some(local_name!("script"))
251            | Some(local_name!("xmp"))
252            | Some(local_name!("iframe"))
253            | Some(local_name!("noembed"))
254            | Some(local_name!("noframes"))
255            | Some(local_name!("plaintext")) => false,
256
257            Some(local_name!("noscript")) => !self.opts.scripting_enabled,
258
259            _ => true,
260        };
261
262        if escape {
263            self.write_escaped(text, false)
264        } else {
265            self.writer.write_all(text.as_bytes())
266        }
267    }
268
269    fn write_comment(&mut self, text: &str) -> io::Result<()> {
270        self.writer.write_all(b"<!--")?;
271        self.writer.write_all(text.as_bytes())?;
272        self.writer.write_all(b"-->")
273    }
274
275    fn write_doctype(&mut self, name: &str) -> io::Result<()> {
276        self.writer.write_all(b"<!DOCTYPE ")?;
277        self.writer.write_all(name.as_bytes())?;
278        self.writer.write_all(b">")
279    }
280
281    fn write_processing_instruction(&mut self, target: &str, data: &str) -> io::Result<()> {
282        self.writer.write_all(b"<?")?;
283        self.writer.write_all(target.as_bytes())?;
284        self.writer.write_all(b" ")?;
285        self.writer.write_all(data.as_bytes())?;
286        self.writer.write_all(b">")
287    }
288}