html_builder/
lib.rs

1/*!
2[`Buffer`] is a magic text buffer that makes writing HTML pleasant.
3
4Here's a teaser of what it looks like in use:
5
6```
7use html_builder::*;                          // Contents added to buf by each statement
8use std::fmt::Write;                          // ---------------------------------------
9                                              //
10let mut buf = Buffer::new();                  //
11let mut html = buf.html().attr("lang='en'");  // <html lang='en'>
12writeln!(html.head().title(), "Title!")?;     // <head><title>Title!
13writeln!(html.body().h1(), "Header!")?;       // </title></head><body><h1>Header!
14buf.finish()                                  // </h1></body></html>
15# ; Ok::<(), std::fmt::Error>(())
16```
17
18## Longer example
19
20```
21use html_builder::*;
22use std::fmt::Write;
23
24// Start by creating a Buffer.  This contains a text buffer that we're going
25// to be writing into.
26let mut buf = Buffer::new();
27
28// The Html5 trait provides various helper methods.  For instance, doctype()
29// simply writes the <!DOCTYPE html> header
30buf.doctype();
31
32// Most helper methods create child nodes.  You can set a node's attributes
33// like so
34let mut html = buf.html().attr("lang='en'");
35
36let mut head = html.head();
37
38// Just like Buffer, nodes are also writable.  Set their contents by
39// writing into them.
40writeln!(head.title(), "Website!")?;
41
42// Meta is a "void element", meaning it doesn't need a closing tag.  This is
43// handled correctly.
44head.meta().attr("charset='utf-8'");
45
46let mut body = html.body();
47writeln!(body.h1(), "It's a website!")?;
48
49// Generating HTML in a loop
50let mut list = body.ul();
51for i in 1..=3 {
52    writeln!(
53        list.li().a().attr(
54            &format!("href='/page_{}.html'", i)
55        ),
56        "Page {}", i,
57    )?
58}
59
60// You can write functions which add subtrees to a node
61fn figure_with_caption(parent: &mut Node, src: &str, cap: &str) {
62    let mut fig = parent.figure();
63    fig.img()
64        .attr(&format!("src='{}'", src))
65        .attr(&format!("alt='{}'", cap));
66    writeln!(fig.figcaption(), "{}", cap).unwrap();
67}
68
69figure_with_caption(&mut body, "img.jpg", "Awesome image");
70
71// Text contents in an inner node
72let mut footer = body.footer();
73writeln!(footer, "Last modified")?;
74writeln!(footer.time(), "2021-04-12")?;
75
76// We also provide a kind of pseudo-node for writing comments
77write!(body.comment(), "Thanks for reading")?;
78
79// Finally, call finish() to extract the buffer.
80buf.finish()
81# ; Ok::<(), std::fmt::Error>(())
82```
83```html
84<!DOCTYPE html>
85<html lang='en'>
86 <head>
87  <title>Website!</title>
88  <meta charset='utf-8'>
89 </head>
90 <body>
91  <h1>It's a website!</h1>
92  <ul>
93   <li><a href='/page_1.html'>Page 1</a></li>
94   <li><a href='/page_2.html'>Page 2</a></li>
95   <li><a href='/page_3.html'>Page 3</a></li>
96  </ul>
97  <figure>
98   <img src='img.jpg' alt='Awesome image'>
99   <figcaption>Awesome image</figcaption>
100  </figure>
101  <footer>
102   Last modified <time>2021-04-12</time>
103  </footer>
104  <!-- Thanks for reading -->
105 </body>
106</html>
107```
108
109*/
110
111mod html;
112pub use html::*;
113
114use std::borrow::Cow;
115use std::fmt::Write;
116use std::sync::{Arc, Mutex, Weak};
117
118/// A buffer for writing HTML into.
119pub struct Buffer {
120    ctx: Arc<Mutex<Ctx>>,
121    node: Node<'static>,
122}
123
124/// An HTML element.
125///
126/// An open-tag is written to the buffer when a `Node` is created, and a
127/// close-tag is written when the `Node` is dropped.  You can set attributes
128/// on a newly-created node using [`attr()`][Node::attr], create child nodes
129/// with [`child()`][Node::child], and write text into the node using the
130/// `Write` impl.
131///
132/// ## Escaping
133///
134/// Text written into a node using its `Write` impl is transformed to make it
135/// render correctly in an HTML-context.  By default, the following characters
136/// are escaped: `&`, `<`, and `>`.  The escaping can be strengthened or
137/// weakened using the [`safe()`][Node::safe] and [`raw()`][Node::raw]
138/// methods respectively.
139pub struct Node<'a> {
140    depth: usize,
141    ctx: Weak<Mutex<Ctx>>,
142    escaping: Escaping,
143    _phantom: std::marker::PhantomData<&'a ()>,
144}
145
146enum Escaping {
147    Raw,
148    Normal,
149    Safe,
150}
151
152/// A self-closing element.
153///
154/// Void elements can't have any contents (since there's no end tag, no
155/// content can be put between the start tag and the end tag).
156pub struct Void<'a> {
157    ctx: Weak<Mutex<Ctx>>,
158    _phantom: std::marker::PhantomData<&'a ()>,
159}
160
161/// A comment.
162///
163/// No escaping is performed on the contents.
164pub struct Comment<'a> {
165    ctx: Weak<Mutex<Ctx>>,
166    _phantom: std::marker::PhantomData<&'a ()>,
167}
168
169#[derive(Default)]
170struct Ctx {
171    wtr: String,
172    stack: Vec<Cow<'static, str>>,
173    tag_open: Option<&'static str>,
174}
175
176impl Buffer {
177    /// Creates a new empty buffer.
178    pub fn new() -> Buffer {
179        Buffer::default()
180    }
181
182    /// Closes all open tags and returns the buffer's contents.
183    pub fn finish(self) -> String {
184        let mutex = Arc::try_unwrap(self.ctx).ok().unwrap();
185        let mut ctx = mutex.into_inner().unwrap();
186        ctx.close_deeper_than(0);
187        ctx.wtr
188    }
189}
190
191impl Default for Buffer {
192    fn default() -> Buffer {
193        let ctx = Arc::new(Mutex::new(Ctx::default()));
194        let node = Node {
195            depth: 0,
196            ctx: Arc::downgrade(&ctx),
197            escaping: Escaping::Normal,
198            _phantom: std::marker::PhantomData,
199        };
200        Buffer { node, ctx }
201    }
202}
203
204impl std::ops::Deref for Buffer {
205    type Target = Node<'static>;
206    fn deref(&self) -> &Node<'static> {
207        &self.node
208    }
209}
210
211impl std::ops::DerefMut for Buffer {
212    fn deref_mut(&mut self) -> &mut Node<'static> {
213        &mut self.node
214    }
215}
216
217impl Ctx {
218    fn close_unclosed(&mut self) {
219        if let Some(closer) = self.tag_open.take() {
220            self.wtr.write_str(closer).unwrap();
221        }
222    }
223
224    fn close_deeper_than(&mut self, depth: usize) {
225        self.close_unclosed();
226        let to_pop = self.stack.len() - depth;
227        for _ in 0..to_pop {
228            if let Some(tag) = self.stack.pop() {
229                writeln!(self.wtr, "{:>w$}/{}>", "<", tag, w = self.stack.len() + 1).unwrap();
230            }
231        }
232    }
233
234    fn open(&mut self, tag: &str, depth: usize) {
235        self.close_deeper_than(depth);
236        write!(self.wtr, "{:>w$}{}", "<", tag, w = depth + 1).unwrap();
237        self.tag_open = Some(">\n");
238    }
239
240    fn open_comment(&mut self, depth: usize) {
241        self.close_deeper_than(depth);
242        write!(self.wtr, "{:>w$}!-- ", "<", w = depth + 1).unwrap();
243        self.tag_open = Some(" -->\n");
244    }
245}
246
247impl<'a> Node<'a> {
248    pub fn child<'b>(&'b mut self, tag: Cow<'static, str>) -> Node<'b> {
249        let ctx = self.ctx.upgrade().unwrap();
250        let mut ctx = ctx.lock().unwrap();
251        ctx.open(&tag, self.depth);
252        ctx.stack.push(tag);
253        Node {
254            depth: self.depth + 1,
255            ctx: self.ctx.clone(),
256            escaping: Escaping::Normal,
257            _phantom: std::marker::PhantomData,
258        }
259    }
260
261    pub fn void_child<'b>(&'b mut self, tag: Cow<'static, str>) -> Void<'b> {
262        let ctx = self.ctx.upgrade().unwrap();
263        let mut ctx = ctx.lock().unwrap();
264        ctx.open(&tag, self.depth);
265        Void {
266            ctx: self.ctx.clone(),
267            _phantom: std::marker::PhantomData,
268        }
269    }
270
271    pub fn comment<'b>(&'b mut self) -> Comment<'b> {
272        let ctx = self.ctx.upgrade().unwrap();
273        let mut ctx = ctx.lock().unwrap();
274        ctx.open_comment(self.depth);
275        Comment {
276            ctx: self.ctx.clone(),
277            _phantom: std::marker::PhantomData,
278        }
279    }
280
281    pub fn attr(self, attr: &str) -> Node<'a> {
282        let ctx = self.ctx.upgrade().unwrap();
283        let mut ctx = ctx.lock().unwrap();
284        if ctx.tag_open.is_some() {
285            write!(ctx.wtr, " {}", attr).unwrap();
286        }
287        self
288    }
289
290    /// Disable escaping
291    ///
292    /// In this mode, written text is passed through unmodified.
293    pub fn raw(mut self) -> Node<'a> {
294        self.escaping = Escaping::Raw;
295        self
296    }
297
298    /// Escape more special characters
299    ///
300    /// In this mode, the following characters are escaped: `&`, `<`, `>`,
301    /// `"`, `'`, and `/`.
302    pub fn safe(mut self) -> Node<'a> {
303        self.escaping = Escaping::Safe;
304        self
305    }
306}
307
308impl<'a> Write for Node<'a> {
309    fn write_str(&mut self, s: &str) -> std::fmt::Result {
310        let mutex = self.ctx.upgrade().unwrap();
311        let mut ctx = mutex.lock().unwrap();
312        ctx.close_deeper_than(self.depth);
313        let s = match self.escaping {
314            Escaping::Raw => s.into(),
315            Escaping::Normal => html_escape::encode_text(s),
316            Escaping::Safe => html_escape::encode_safe(s),
317        };
318        ctx.wtr.write_str(&s)
319    }
320}
321
322impl<'a> Void<'a> {
323    pub fn attr(self, attr: &str) -> Void<'a> {
324        let ctx = self.ctx.upgrade().unwrap();
325        let mut ctx = ctx.lock().unwrap();
326        if ctx.tag_open.is_some() {
327            write!(ctx.wtr, " {}", attr).unwrap();
328        }
329        self
330    }
331}
332
333impl<'a> Write for Comment<'a> {
334    fn write_char(&mut self, c: char) -> std::fmt::Result {
335        let mutex = self.ctx.upgrade().unwrap();
336        let mut ctx = mutex.lock().unwrap();
337        ctx.wtr.write_char(c)
338    }
339    fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
340        let mutex = self.ctx.upgrade().unwrap();
341        let mut ctx = mutex.lock().unwrap();
342        ctx.wtr.write_fmt(args)
343    }
344    fn write_str(&mut self, s: &str) -> std::fmt::Result {
345        let mutex = self.ctx.upgrade().unwrap();
346        let mut ctx = mutex.lock().unwrap();
347        ctx.wtr.write_str(s)
348    }
349}