html_to_string_macro/
lib.rs

1use std::convert::TryFrom;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::Expr;
6use syn_rsx::{parse, Node, NodeType};
7
8fn walk_nodes<'a>(nodes: &'a Vec<Node>, context: Option<NodeType>) -> (String, Vec<&'a Expr>) {
9    let mut out = String::new();
10    let mut values = vec![];
11
12    for node in nodes {
13        match node {
14            Node::Doctype(doctype) => {
15                let value = String::try_from(&doctype.value)
16                    .expect("could not convert node value to string");
17                out.push_str(&format!("<!DOCTYPE {}>", value));
18            }
19            Node::Element(element) => {
20                let name = element.name.to_string();
21                out.push_str(&format!("<{}", name));
22
23                // attributes
24                let (html_string, attribute_values) =
25                    walk_nodes(&element.attributes, Some(NodeType::Attribute));
26                out.push_str(&html_string);
27                values.extend(attribute_values);
28                out.push('>');
29
30                // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
31                match name.as_str() {
32                    "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
33                    | "meta" | "param" | "source" | "track" | "wbr" => continue,
34                    _ => (),
35                }
36
37                // children
38                let (html_string, children_values) = walk_nodes(&element.children, None);
39                out.push_str(&html_string);
40                values.extend(children_values);
41
42                out.push_str(&format!("</{}>", name));
43            }
44            Node::Attribute(attribute) => {
45                out.push_str(&format!(" {}", attribute.key.to_string()));
46                if let Some(value) = &attribute.value {
47                    out.push_str(r#"="{}""#);
48                    values.push(value);
49                }
50            }
51            Node::Text(text) => {
52                out.push_str("{}");
53                values.push(&text.value);
54            }
55            Node::Fragment(fragment) => {
56                let (html_string, children_values) =
57                    walk_nodes(&fragment.children, Some(NodeType::Fragment));
58                out.push_str(&html_string);
59                values.extend(children_values);
60            }
61            Node::Comment(comment) => {
62                out.push_str("<!-- {} -->");
63                values.push(&comment.value);
64            }
65            Node::Block(block) => {
66                // If the nodes parent is an attribute we prefix with whitespace
67                if matches!(context, Some(NodeType::Attribute)) {
68                    out.push(' ');
69                }
70
71                out.push_str("{}");
72                values.push(&block.value);
73            }
74        }
75    }
76
77    (out, values)
78}
79
80/// Converts HTML to `String`.
81///
82/// Values returned from braced blocks `{}` are expected to return something
83/// that implements `Display`.
84///
85/// See [syn-rsx docs](https://docs.rs/syn-rsx/) for supported tags and syntax.
86///
87/// # Example
88///
89/// ```
90/// use html_to_string_macro::html;
91///
92/// let world = "planet";
93/// assert_eq!(html!(<div>"hello "{world}</div>), "<div>hello planet</div>");
94/// ```
95#[proc_macro]
96pub fn html(tokens: TokenStream) -> TokenStream {
97    match parse(tokens) {
98        Ok(nodes) => {
99            let (html_string, values) = walk_nodes(&nodes, None);
100            quote! { format!(#html_string, #(#values),*) }
101        }
102        Err(error) => error.to_compile_error(),
103    }
104    .into()
105}