1use std::{collections::HashMap, str::Chars};
2
3use proc_macro2::{Span, TokenStream, TokenTree};
4use quote::quote;
5use syn::{parse::Parse, parse_macro_input, token::Brace, Block, Expr, Ident, Stmt};
6use virtual_dom::{parse_html, Html};
7
8#[proc_macro]
31pub fn dom(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
32 let parsed_content = input
33 .to_string()
34 .replace("\n", " ");
37 let template = parse_macro_input!(input as Template);
38
39 let tokens = match parse_html(parsed_content.to_string().as_bytes()) {
40 Ok(t) => t,
41 Err(e) => {
42 let e = syn::Error::new(Span::call_site(), e);
43 return proc_macro::TokenStream::from(e.to_compile_error());
44 }
45 };
46
47 let html = to_tokens(&tokens, &template, None, 0);
48
49 if tokens.len() > 1 {
50 let children = quote!(vec![#({#html},)*]);
51 return quote! {
52 {
53 use ::std::collections::HashMap;
54 use ::virtual_dom::*;
55 #children
56 }
57 }
58 .into();
59 }
60
61 let html = html.into_iter().next().expect("no html given");
62 quote! {
63 {
64 use ::std::collections::HashMap;
65 use ::virtual_dom::*;
66 #html
67 }
68 }
69 .into()
70}
71
72#[derive(Clone)]
74struct Template {
75 variables: HashMap<String, Ident>,
76}
77
78impl Parse for Template {
79 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
80 let mut variables = HashMap::new();
81
82 while !input.is_empty() {
83 if input.peek(Brace) {
84 let content;
85 syn::braced!(content in input);
86 for s in content.call(Block::parse_within)? {
87 match s {
88 Stmt::Expr(e, _) => {
89 if let Expr::Path(p) = e {
90 let a = p.path.segments[0].ident.to_string();
91 let ident = p
92 .path
93 .segments
94 .first()
95 .expect("path does not include ident")
96 .ident
97 .clone();
98 variables.insert(a, ident);
99 }
100 },
101 Stmt::Local(_) | Stmt::Item(_) | Stmt::Macro(_) => {
102 return Err(input.error("unexpected statement"));
103 }
104 }
105 }
106
107 continue;
108 }
109 let t = input.parse::<TokenTree>()?;
111
112 if let TokenTree::Literal(l) = t {
114 let text = l.to_string();
115 let mut chars = text.chars();
116 while let Some(c) = chars.next() {
117 if c == '{' {
118 if let Ok(var) = parse_braces(&mut chars) {
119 let ident = Ident::new(&var, l.span());
120 variables.insert(var, ident);
121 }
122 }
123 }
124 }
125 }
126
127 Ok(Template { variables })
128 }
129}
130
131fn parse_braces(chars: &mut Chars) -> Result<String, String> {
134 let mut raw = String::new();
135 let mut variable_name = String::new();
136 let mut var_has_whitespace = false;
137 for c in chars.by_ref() {
138 raw.push(c);
139 match c {
140 ' ' | '\n' if !variable_name.is_empty() => {
142 var_has_whitespace = true;
143 }
144 ' ' | '\n' => {}
146 '}' => {
147 return Ok(variable_name);
148 }
149 c if !c.is_alphabetic() => {
151 return Err(raw);
152 }
153 _ => {
154 if var_has_whitespace {
156 return Err(raw);
157 }
158 variable_name.push(c)
159 }
160 }
161 }
162
163 Err(raw)
164}
165
166fn interpolate_string(text: &str, template: &Template) -> TokenStream {
168 let mut chars = text.chars();
169 let mut variables = vec![];
170 let mut text = String::new();
171 while let Some(c) = chars.next() {
173 if c == '{' {
174 match parse_braces(&mut chars) {
175 Ok(variable_name) => {
176 let variable = template.variables.get(&variable_name).unwrap_or_else(|| panic!(
177 "failed to parse or find variable '{variable_name}'"
178 ));
179 text.push_str("{}");
180 variables.push(quote!(#variable));
181 }
182 Err(t) => text.push_str(&t),
183 }
184 } else {
185 text.push(c);
186 }
187 }
188 if !variables.is_empty() {
189 return quote!(format!(#text, #(#variables,)*));
190 }
191 quote!(String::from(#text))
192}
193
194fn to_tokens(
195 tokens: &Vec<Html>,
196 template: &Template,
197 parent: Option<&Ident>,
198 mut i: usize,
199) -> Vec<TokenStream> {
200 let mut items = vec![];
201
202 for t in tokens {
203 i += 1;
204 match t {
205 Html::Comment { .. } => {}
206 Html::Text { text } => {
207 let text = if text.starts_with("\"") && text.ends_with("\"") {
209 let text = text.get(1..text.len() - 1).unwrap_or("");
210 if text.is_empty() {
211 continue;
212 }
213 text
214 } else {
215 text
216 };
217
218 let mut chars = text.chars();
219 let mut text = String::new();
220 while let Some(c) = chars.next() {
221 if c == '{' {
222 match parse_braces(&mut chars) {
223 Ok(variable_name) => {
224 let variable = template.variables.get(&variable_name).unwrap_or_else(|| panic!(
225 "failed to parse or find variable '{variable_name}'"
226 ));
227 if let Some(parent) = parent {
228 if !text.is_empty() {
229 items.push(
230 quote!(#parent.append_child(DomNode::create_text(#text))),
231 );
232 text.clear();
233 }
234 items.push(quote!(#parent.append_child(#variable)));
235 } else {
236 if !text.is_empty() {
237 items.push(quote!(DomNode::create_text(#text)));
238 text.clear();
239 }
240 items.push(quote!(#variable));
241 }
242 }
243 Err(t) => text.push_str(&t),
244 }
245 } else {
246 text.push(c);
247 }
248 }
249 if !text.is_empty() {
250 if let Some(parent) = parent {
251 items.push(quote!(#parent.append_child(DomNode::create_text(#text))));
252 } else {
253 items.push(quote!(DomNode::create_text(#text)));
254 }
255 }
256 }
257 Html::Element {
258 tag,
259 attributes,
260 children,
261 } => {
262 let attributes_values = attributes.iter().map(|(key, value)| {
263 let key = interpolate_string(key, template);
264 let value = interpolate_string(value, template);
265 quote! {
266 attributes.insert(#key, #value);
267 }
268 });
269
270 let id = Ident::new(&format!("node_{i}"), Span::call_site());
271
272 let el = if !children.is_empty() {
273 let children = to_tokens(children, template, Some(&id), i + tokens.len());
274 quote!(
275 let mut attributes = HashMap::new();
276 #(#attributes_values)*
277 let #id = DomNode::create_element_with_attributes(#tag, attributes);
278 #({#children})*;
279 )
280 } else {
281 quote!(
282 let mut attributes = HashMap::new();
283 #(#attributes_values)*
284 let #id = DomNode::create_element_with_attributes(#tag, attributes);
285 )
286 };
287
288 if let Some(parent) = parent {
289 items.push(quote!(
290 #el
291 #parent.append_child(#id);
292 ))
293 } else {
294 items.push(quote!(
295 #el
296 #id
297 ))
298 }
299 }
300 }
301 }
302
303 items
304}