rstml_to_string_macro/
lib.rs1use 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 values: Vec<proc_macro2::TokenStream>,
18 diagnostics: Vec<proc_macro2::TokenStream>,
20 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 for attribute in element.attributes() {
54 match attribute {
55 NodeAttribute::Block(block) => {
56 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 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 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#[proc_macro]
144pub fn html(tokens: TokenStream) -> TokenStream {
145 html_inner(tokens, false)
146}
147
148#[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 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 #(#errors;)*
190 #(#docs;)*
192 format!(#html_string, #(#values),*)
193 }
194 }
195 .into()
196}
197
198fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
199 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 element = quote_spanned!(e.span() => element);
215 quote!(let _ = crate::docs::#element)
216 }
217 })
218 .collect()
219}