stardom_render/
lib.rs

1mod node;
2
3use std::{
4    io::{self, Write},
5    str,
6};
7
8use node::NodeKind;
9pub use node::NodeRef;
10
11// Reference: https://developer.mozilla.org/en-US/docs/Glossary/Void_element
12const VOID_ELEMENTS: &[&str] = &[
13    "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
14    "track", "wbr",
15];
16
17pub fn render_string(node: &NodeRef) -> String {
18    let mut buf = vec![];
19    render(&mut buf, node).unwrap();
20    // SAFETY: render can only produce valid UTF-8
21    unsafe { String::from_utf8_unchecked(buf) }
22}
23
24pub fn render<W: Write>(mut w: W, node: &NodeRef) -> io::Result<()> {
25    match &*node.kind() {
26        NodeKind::Text(text) => {
27            write!(w, "{}", escape(text))?;
28        }
29        NodeKind::Raw(raw) => {
30            write!(w, "{raw}")?;
31        }
32        NodeKind::Fragment(children) => {
33            write!(w, "{}", render_children(children, ""))?;
34        }
35        NodeKind::Element {
36            namespace,
37            name,
38            attrs,
39            children,
40        } => {
41            let tag = namespace
42                .as_ref()
43                .map(|ns| format!("{ns}:{name}"))
44                .unwrap_or_else(|| name.clone());
45
46            let attrs = attrs
47                .iter()
48                .map(|(name, value)| format!(" {name}=\"{}\"", escape(value)))
49                .collect::<Vec<_>>()
50                .join("");
51
52            if !children.is_empty() {
53                write!(
54                    w,
55                    "<{tag}{attrs}>\n{children}\n</{tag}>",
56                    children = render_children(children, "  ")
57                )?;
58            } else if VOID_ELEMENTS.contains(&tag.as_str()) {
59                write!(w, "<{tag}{attrs}>")?;
60            } else {
61                write!(w, "<{tag}{attrs}></{tag}>")?;
62            }
63        }
64    }
65
66    Ok(())
67}
68
69fn render_children(children: &[NodeRef], indent: &str) -> String {
70    let mut buf = vec![];
71    for child in children {
72        render(&mut buf, child).unwrap();
73
74        buf.push(b'\n');
75    }
76
77    // SAFETY: render can only produce valid UTF-8
78    let text = unsafe { String::from_utf8_unchecked(buf) };
79    text.lines()
80        .map(|line| format!("{indent}{line}"))
81        .collect::<Vec<_>>()
82        .join("\n")
83}
84
85// Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-for-html-contexts
86pub fn escape(text: &str) -> String {
87    let mut output = String::new();
88    for c in text.chars() {
89        match c {
90            '&' => output.push_str("&amp;"),
91            '<' => output.push_str("&lt;"),
92            '>' => output.push_str("&gt;"),
93            '"' => output.push_str("&quot;"),
94            '\'' => output.push_str("&#x27;"),
95            _ => output.push(c),
96        }
97    }
98    output
99}