Skip to main content

ramhorns_ext/
encoding.rs

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