platelet/
html.rs

1use std::fmt::Write as _;
2
3#[derive(Debug, Clone, PartialEq)]
4pub(crate) enum Node {
5    Text {
6        content: String,
7    },
8    Element {
9        name: String,
10        attrs: Vec<(String, String)>,
11        children: Vec<Node>,
12    },
13    Comment {
14        content: String,
15    },
16    Document {
17        children: Vec<Node>,
18    },
19    Doctype {
20        doctype: String,
21    },
22}
23
24fn html_text_safe(s: &str) -> String {
25    s.replace('&', "&amp;")
26        .replace('<', "&lt;")
27        .replace('>', "&gt;")
28}
29
30fn push_attr_value(s: &mut String, value: &String) {
31    // prefer single quotes - unless it allows us to avoid escaping
32    match (value.contains('\''), value.contains('"')) {
33        (true, true) => {
34            s.push('\'');
35            s.push_str(&value.replace("'", "&#39;"));
36            s.push('\'');
37        }
38        (true, false) => {
39            s.push('"');
40            s.push_str(&value);
41            s.push('"');
42        }
43        (false, true) => {
44            s.push('\'');
45            s.push_str(&value);
46            s.push('\'');
47        }
48        (false, false) => {
49            s.push('\'');
50            s.push_str(&value);
51            s.push('\'');
52        }
53    };
54}
55
56fn push_node_as_string(s: &mut String, n: &Node) {
57    match n {
58        Node::Doctype { doctype } => {
59            s.push_str("<!DOCTYPE ");
60            s.push_str(&html_text_safe(doctype));
61            s.push('>')
62        }
63        Node::Comment { content } => write!(s, "<!--{}-->", html_text_safe(content)).unwrap(),
64        Node::Text { content } => s.push_str(&html_text_safe(content)),
65        Node::Element {
66            name,
67            attrs,
68            children,
69        } => {
70            if name == "pl-template" {
71                for child in children {
72                    push_node_as_string(s, child);
73                }
74                return;
75            }
76            s.push('<');
77            s.push_str(name);
78
79            for (key, value) in attrs {
80                s.push(' ');
81                s.push_str(key);
82                s.push('=');
83                push_attr_value(s, value);
84            }
85
86            match name.as_str() {
87                "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
88                | "meta" | "param" | "source" | "track" | "wbr" => {
89                    s.push('>');
90                }
91                _ => {
92                    s.push('>');
93                    for child in children {
94                        push_node_as_string(s, child);
95                    }
96                    s.push('<');
97                    s.push('/');
98                    s.push_str(name);
99                    s.push('>');
100                }
101            }
102        }
103        Node::Document { children } => {
104            for child in children {
105                push_node_as_string(s, child);
106            }
107        }
108    }
109}
110
111impl Node {
112    pub(crate) fn to_string(&self) -> String {
113        let mut buf = String::with_capacity(1000);
114        push_node_as_string(&mut buf, self);
115        buf
116    }
117}