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}