ramhorns/
encoding.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Utilities dealing with writing the bits of a template or data to the output and
6//! escaping special HTML characters.
7
8use std::fmt;
9use std::io;
10
11#[cfg(feature = "pulldown-cmark")]
12use pulldown_cmark::{html, Event, Parser};
13
14/// A trait that wraps around either a `String` or `std::io::Write`, providing UTF-8 safe
15/// writing boundaries and special HTML character escaping.
16pub trait Encoder {
17    /// Error type for this encoder
18    type Error;
19
20    /// Write a `&str` to this `Encoder` in plain mode.
21    fn write_unescaped(&mut self, part: &str) -> Result<(), Self::Error>;
22
23    /// Write a `&str` to this `Encoder`, escaping special HTML characters.
24    fn write_escaped(&mut self, part: &str) -> Result<(), Self::Error>;
25
26    #[cfg(feature = "pulldown-cmark")]
27    /// Write HTML from an `Iterator` of `pulldown_cmark` `Event`s.
28    fn write_html<'a, I: Iterator<Item = Event<'a>>>(&mut self, iter: I)
29        -> Result<(), Self::Error>;
30
31    /// Write a `Display` implementor to this `Encoder` in plain mode.
32    fn format_unescaped<D: fmt::Display>(&mut self, display: D) -> Result<(), Self::Error>;
33
34    /// Write a `Display` implementor to this `Encoder`, escaping special HTML characters.
35    fn format_escaped<D: fmt::Display>(&mut self, display: D) -> Result<(), Self::Error>;
36}
37
38/// Local helper for escaping stuff into strings.
39struct EscapingStringEncoder<'a>(&'a mut String);
40
41impl<'a> EscapingStringEncoder<'a> {
42    /// Write with escaping special HTML characters. Since we are dealing
43    /// with a String, we don't need to return a `Result`.
44    fn write_escaped(&mut self, part: &str) {
45        let mut start = 0;
46
47        for (idx, byte) in part.bytes().enumerate() {
48            let replace = match byte {
49                b'<' => "&lt;",
50                b'>' => "&gt;",
51                b'&' => "&amp;",
52                b'"' => "&quot;",
53                _ => continue,
54            };
55
56            self.0.push_str(&part[start..idx]);
57            self.0.push_str(replace);
58
59            start = idx + 1;
60        }
61
62        self.0.push_str(&part[start..]);
63    }
64}
65
66/// Provide a `fmt::Write` interface, so we can use `write!` macro.
67impl<'a> fmt::Write for EscapingStringEncoder<'a> {
68    #[inline]
69    fn write_str(&mut self, part: &str) -> fmt::Result {
70        self.write_escaped(part);
71
72        Ok(())
73    }
74}
75
76/// Encoder wrapper around io::Write. We can't implement `Encoder` on a generic here,
77/// because we're implementing it directly for `String`.
78pub(crate) struct EscapingIOEncoder<W: io::Write> {
79    inner: W,
80}
81
82impl<W: io::Write> EscapingIOEncoder<W> {
83    #[inline]
84    pub fn new(inner: W) -> Self {
85        Self { inner }
86    }
87
88    /// Same as `EscapingStringEncoder`, but dealing with byte arrays and writing to
89    /// the inner `io::Write`.
90    fn write_escaped_bytes(&mut self, part: &[u8]) -> io::Result<()> {
91        let mut start = 0;
92
93        for (idx, byte) in part.iter().enumerate() {
94            let replace: &[u8] = match *byte {
95                b'<' => b"&lt;",
96                b'>' => b"&gt;",
97                b'&' => b"&amp;",
98                b'"' => b"&quot;",
99                _ => continue,
100            };
101
102            self.inner.write_all(&part[start..idx])?;
103            self.inner.write_all(replace)?;
104
105            start = idx + 1;
106        }
107
108        self.inner.write_all(&part[start..])
109    }
110}
111
112// Additionally we implement `io::Write` for it directly. This allows us to use
113// the `write!` macro for formatting without allocations.
114impl<W: io::Write> io::Write for EscapingIOEncoder<W> {
115    #[inline]
116    fn write(&mut self, part: &[u8]) -> io::Result<usize> {
117        self.write_escaped_bytes(part).map(|()| part.len())
118    }
119
120    #[inline]
121    fn write_all(&mut self, part: &[u8]) -> io::Result<()> {
122        self.write_escaped_bytes(part)
123    }
124
125    #[inline]
126    fn flush(&mut self) -> io::Result<()> {
127        Ok(())
128    }
129}
130
131impl<W: io::Write> Encoder for EscapingIOEncoder<W> {
132    type Error = io::Error;
133
134    #[inline]
135    fn write_unescaped(&mut self, part: &str) -> io::Result<()> {
136        self.inner.write_all(part.as_bytes())
137    }
138
139    #[inline]
140    fn write_escaped(&mut self, part: &str) -> io::Result<()> {
141        self.write_escaped_bytes(part.as_bytes())
142    }
143
144    #[cfg(feature = "pulldown-cmark")]
145    #[inline]
146    fn write_html<'a, I: Iterator<Item = Event<'a>>>(&mut self, iter: I) -> io::Result<()> {
147        html::write_html_io(&mut self.inner, iter)
148    }
149
150    #[inline]
151    fn format_unescaped<D: fmt::Display>(&mut self, display: D) -> Result<(), Self::Error> {
152        write!(self.inner, "{}", display)
153    }
154
155    #[inline]
156    fn format_escaped<D: fmt::Display>(&mut self, display: D) -> Result<(), Self::Error> {
157        use io::Write;
158
159        write!(self, "{}", display)
160    }
161}
162
163/// Error type for `String`, impossible to instantiate.
164/// Rust optimizes `Result<(), NeverError>` to 0-size.
165pub enum NeverError {}
166
167impl Encoder for String {
168    // Change this to `!` once stabilized.
169    type Error = NeverError;
170
171    #[inline]
172    fn write_unescaped(&mut self, part: &str) -> Result<(), Self::Error> {
173        self.push_str(part);
174
175        Ok(())
176    }
177
178    #[inline]
179    fn write_escaped(&mut self, part: &str) -> Result<(), Self::Error> {
180        EscapingStringEncoder(self).write_escaped(part);
181
182        Ok(())
183    }
184
185    #[cfg(feature = "pulldown-cmark")]
186    #[inline]
187    fn write_html<'a, I: Iterator<Item = Event<'a>>>(
188        &mut self,
189        iter: I,
190    ) -> Result<(), Self::Error> {
191        html::push_html(self, iter);
192
193        Ok(())
194    }
195
196    #[inline]
197    fn format_unescaped<D: fmt::Display>(&mut self, display: D) -> Result<(), Self::Error> {
198        use std::fmt::Write;
199
200        // Never fails for a string
201        let _ = write!(self, "{}", display);
202
203        Ok(())
204    }
205
206    #[inline]
207    fn format_escaped<D: fmt::Display>(&mut self, display: D) -> Result<(), Self::Error> {
208        use std::fmt::Write;
209
210        // Never fails for a string
211        let _ = write!(EscapingStringEncoder(self), "{}", display);
212
213        Ok(())
214    }
215}
216
217#[cfg(feature = "pulldown-cmark")]
218/// Parse and encode the markdown using pulldown_cmark
219pub fn encode_cmark<E: Encoder>(source: &str, encoder: &mut E) -> Result<(), E::Error> {
220    let parser = Parser::new(source);
221
222    encoder.write_html(parser)
223}