rstml_to_string_macro/
lib.rs

1use std::collections::HashSet;
2
3use proc_macro::TokenStream;
4use proc_macro2::{Literal, TokenTree};
5use quote::{quote, quote_spanned, ToTokens};
6use rstml::{
7    node::{Node, NodeAttribute, NodeName},
8    Parser, ParserConfig,
9};
10use syn::spanned::Spanned;
11
12#[derive(Default)]
13struct WalkNodesOutput<'a> {
14    static_format: String,
15    // Use proc_macro2::TokenStream instead of syn::Expr
16    // to provide more errors to the end user.
17    values: Vec<proc_macro2::TokenStream>,
18    // Additional diagnostic messages.
19    diagnostics: Vec<proc_macro2::TokenStream>,
20    // Collect elements to provide semantic highlight based on element tag.
21    // No differences between open tag and closed tag.
22    // Also multiple tags with same name can be present,
23    // because we need to mark each of them.
24    collected_elements: Vec<&'a NodeName>,
25}
26impl<'a> WalkNodesOutput<'a> {
27    fn extend(&mut self, other: WalkNodesOutput<'a>) {
28        self.static_format.push_str(&other.static_format);
29        self.values.extend(other.values);
30        self.diagnostics.extend(other.diagnostics);
31        self.collected_elements.extend(other.collected_elements);
32    }
33}
34
35fn walk_nodes<'a>(empty_elements: &HashSet<&str>, nodes: &'a Vec<Node>) -> WalkNodesOutput<'a> {
36    let mut out = WalkNodesOutput::default();
37
38    for node in nodes {
39        match node {
40            Node::Doctype(doctype) => {
41                let value = &doctype.value.to_token_stream_string();
42                out.static_format.push_str(&format!("<!DOCTYPE {}>", value));
43            }
44            Node::Element(element) => {
45                let name = element.name().to_string();
46                out.static_format.push_str(&format!("<{}", name));
47                out.collected_elements.push(&element.open_tag.name);
48                if let Some(e) = &element.close_tag {
49                    out.collected_elements.push(&e.name)
50                }
51
52                // attributes
53                for attribute in element.attributes() {
54                    match attribute {
55                        NodeAttribute::Block(block) => {
56                            // If the nodes parent is an attribute we prefix with whitespace
57                            out.static_format.push(' ');
58                            out.static_format.push_str("{}");
59                            out.values.push(block.to_token_stream());
60                        }
61                        NodeAttribute::Attribute(attribute) => {
62                            out.static_format.push_str(&format!(" {}", attribute.key));
63                            if let Some(value) = attribute.value() {
64                                out.static_format.push_str(r#"="{}""#);
65                                out.values.push(value.to_token_stream());
66                            }
67                        }
68                    }
69                }
70                // Ignore childs of special Empty elements
71                if empty_elements.contains(element.open_tag.name.to_string().as_str()) {
72                    out.static_format
73                        .push_str(&format!("/</{}>", element.open_tag.name));
74                    if !element.children.is_empty() {
75                        let warning = proc_macro2_diagnostics::Diagnostic::spanned(
76                            element.open_tag.name.span(),
77                            proc_macro2_diagnostics::Level::Warning,
78                            "Element is processed as empty, and cannot have any child",
79                        );
80                        out.diagnostics.push(warning.emit_as_expr_tokens())
81                    }
82
83                    continue;
84                }
85                out.static_format.push('>');
86
87                // children
88                let other_output = walk_nodes(empty_elements, &element.children);
89                out.extend(other_output);
90                out.static_format.push_str(&format!("</{}>", name));
91            }
92            Node::Text(text) => {
93                out.static_format.push_str(&text.value_string());
94            }
95            Node::RawText(text) => {
96                out.static_format.push_str("{}");
97                let tokens = text.to_string_best();
98                let literal = Literal::string(&tokens);
99
100                out.values.push(TokenTree::from(literal).into());
101            }
102            Node::Fragment(fragment) => {
103                let other_output = walk_nodes(empty_elements, &fragment.children);
104                out.extend(other_output)
105            }
106            Node::Comment(comment) => {
107                out.static_format.push_str("<!-- {} -->");
108                out.values.push(comment.value.to_token_stream());
109            }
110            Node::Block(block) => {
111                out.static_format.push_str("{}");
112                out.values.push(block.to_token_stream());
113            }
114        }
115    }
116
117    out
118}
119
120/// Converts HTML to `String`.
121///
122/// Values returned from braced blocks `{}` are expected to return something
123/// that implements `Display`.
124///
125/// See [rstml docs](https://docs.rs/rstml/) for supported tags and syntax.
126///
127/// # Example
128///
129/// ```
130/// use rstml_to_string_macro::html;
131/// // using this macro, one should write docs module on top level of crate.
132/// // Macro will link html tags to them.
133/// pub mod docs {
134///     /// Element has open and close tags, content and attributes.
135///     pub fn element() {}
136/// }
137/// # fn main (){
138///
139/// let world = "planet";
140/// assert_eq!(html!(<div>"hello "{world}</div>), "<div>hello planet</div>");
141/// # }
142/// ```
143#[proc_macro]
144pub fn html(tokens: TokenStream) -> TokenStream {
145    html_inner(tokens, false)
146}
147
148/// Same as html but also emit IDE helper statements.
149/// Open tests.rs in ide to see semantic highlight/goto def and docs.
150#[proc_macro]
151pub fn html_ide(tokens: TokenStream) -> TokenStream {
152    html_inner(tokens, true)
153}
154
155fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream {
156    // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
157    let empty_elements: HashSet<_> = [
158        "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
159        "source", "track", "wbr",
160    ]
161    .into_iter()
162    .collect();
163    let config = ParserConfig::new()
164        .recover_block(true)
165        .always_self_closed_elements(empty_elements.clone())
166        .raw_text_elements(["script", "style"].into_iter().collect());
167
168    let parser = Parser::new(config);
169    let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
170
171    let WalkNodesOutput {
172        static_format: html_string,
173        values,
174        collected_elements: elements,
175        diagnostics,
176    } = walk_nodes(&empty_elements, &nodes);
177    let docs = if ide_helper {
178        generate_tags_docs(elements)
179    } else {
180        vec![]
181    };
182    let errors = errors
183        .into_iter()
184        .map(|e| e.emit_as_expr_tokens())
185        .chain(diagnostics);
186    quote! {
187        {
188            // Make sure that "compile_error!(..);"  can be used in this context.
189            #(#errors;)*
190            // Make sure that "enum x{};" and "let _x = crate::element;"  can be used in this context
191            #(#docs;)*
192            format!(#html_string, #(#values),*)
193        }
194    }
195    .into()
196}
197
198fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
199    // Mark some of elements as type,
200    // and other as elements as fn in crate::docs,
201    // to give an example how to link tag with docs.
202    let elements_as_type: HashSet<&'static str> = vec!["html", "head", "meta", "link", "body"]
203        .into_iter()
204        .collect();
205
206    elements
207        .into_iter()
208        .map(|e| {
209            if elements_as_type.contains(&*e.to_string()) {
210                let element = quote_spanned!(e.span() => enum);
211                quote!({#element X{}})
212            } else {
213                // let _ = crate::docs::element;
214                let element = quote_spanned!(e.span() => element);
215                quote!(let _ = crate::docs::#element)
216            }
217        })
218        .collect()
219}