use std::{collections::HashMap, str::Chars};
use proc_macro2::{Span, TokenStream, TokenTree};
use quote::quote;
use syn::{parse::Parse, parse_macro_input, token::Brace, Block, Expr, Ident, Stmt};
use virtual_dom::{parse_html, Html};
#[proc_macro]
pub fn dom(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let parsed_content = input
.to_string()
.replace("\n", " ");
let template = parse_macro_input!(input as Template);
let tokens = match parse_html(parsed_content.to_string().as_bytes()) {
Ok(t) => t,
Err(e) => {
let e = syn::Error::new(Span::call_site(), e);
return proc_macro::TokenStream::from(e.to_compile_error());
}
};
let html = to_tokens(&tokens, &template, None, 0);
if tokens.len() > 1 {
let children = quote!(vec![#({#html},)*]);
return quote! {
{
use ::std::collections::HashMap;
use ::virtual_dom::*;
#children
}
}
.into();
}
let html = html.into_iter().next().expect("no html given");
quote! {
{
use ::std::collections::HashMap;
use ::virtual_dom::*;
#html
}
}
.into()
}
#[derive(Clone)]
struct Template {
variables: HashMap<String, Ident>,
}
impl Parse for Template {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut variables = HashMap::new();
while !input.is_empty() {
if input.peek(Brace) {
let content;
syn::braced!(content in input);
for s in content.call(Block::parse_within)? {
match s {
Stmt::Expr(e, _) => {
if let Expr::Path(p) = e {
let a = p.path.segments[0].ident.to_string();
let ident = p
.path
.segments
.first()
.expect("path does not include ident")
.ident
.clone();
variables.insert(a, ident);
}
},
Stmt::Local(_) | Stmt::Item(_) | Stmt::Macro(_) => {
return Err(input.error("unexpected statement"));
}
}
}
continue;
}
let t = input.parse::<TokenTree>()?;
if let TokenTree::Literal(l) = t {
let text = l.to_string();
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c == '{' {
if let Ok(var) = parse_braces(&mut chars) {
let ident = Ident::new(&var, l.span());
variables.insert(var, ident);
}
}
}
}
}
Ok(Template { variables })
}
}
fn parse_braces(chars: &mut Chars) -> Result<String, String> {
let mut raw = String::new();
let mut variable_name = String::new();
let mut var_has_whitespace = false;
for c in chars.by_ref() {
raw.push(c);
match c {
' ' | '\n' if !variable_name.is_empty() => {
var_has_whitespace = true;
}
' ' | '\n' => {}
'}' => {
return Ok(variable_name);
}
c if !c.is_alphabetic() => {
return Err(raw);
}
_ => {
if var_has_whitespace {
return Err(raw);
}
variable_name.push(c)
}
}
}
Err(raw)
}
fn interpolate_string(text: &str, template: &Template) -> TokenStream {
let mut chars = text.chars();
let mut variables = vec![];
let mut text = String::new();
while let Some(c) = chars.next() {
if c == '{' {
match parse_braces(&mut chars) {
Ok(variable_name) => {
let variable = template.variables.get(&variable_name).unwrap_or_else(|| panic!(
"failed to parse or find variable '{variable_name}'"
));
text.push_str("{}");
variables.push(quote!(#variable));
}
Err(t) => text.push_str(&t),
}
} else {
text.push(c);
}
}
if !variables.is_empty() {
return quote!(format!(#text, #(#variables,)*));
}
quote!(String::from(#text))
}
fn to_tokens(
tokens: &Vec<Html>,
template: &Template,
parent: Option<&Ident>,
mut i: usize,
) -> Vec<TokenStream> {
let mut items = vec![];
for t in tokens {
i += 1;
match t {
Html::Comment { .. } => {}
Html::Text { text } => {
let text = if text.starts_with("\"") && text.ends_with("\"") {
let text = text.get(1..text.len() - 1).unwrap_or("");
if text.is_empty() {
continue;
}
text
} else {
text
};
let mut chars = text.chars();
let mut text = String::new();
while let Some(c) = chars.next() {
if c == '{' {
match parse_braces(&mut chars) {
Ok(variable_name) => {
let variable = template.variables.get(&variable_name).unwrap_or_else(|| panic!(
"failed to parse or find variable '{variable_name}'"
));
if let Some(parent) = parent {
if !text.is_empty() {
items.push(
quote!(#parent.append_child(DomNode::create_text(#text))),
);
text.clear();
}
items.push(quote!(#parent.append_child(#variable)));
} else {
if !text.is_empty() {
items.push(quote!(DomNode::create_text(#text)));
text.clear();
}
items.push(quote!(#variable));
}
}
Err(t) => text.push_str(&t),
}
} else {
text.push(c);
}
}
if !text.is_empty() {
if let Some(parent) = parent {
items.push(quote!(#parent.append_child(DomNode::create_text(#text))));
} else {
items.push(quote!(DomNode::create_text(#text)));
}
}
}
Html::Element {
tag,
attributes,
children,
} => {
let attributes_values = attributes.iter().map(|(key, value)| {
let key = interpolate_string(key, template);
let value = interpolate_string(value, template);
quote! {
attributes.insert(#key, #value);
}
});
let id = Ident::new(&format!("node_{i}"), Span::call_site());
let el = if !children.is_empty() {
let children = to_tokens(children, template, Some(&id), i + tokens.len());
quote!(
let mut attributes = HashMap::new();
#(#attributes_values)*
let #id = DomNode::create_element_with_attributes(#tag, attributes);
#({#children})*;
)
} else {
quote!(
let mut attributes = HashMap::new();
#(#attributes_values)*
let #id = DomNode::create_element_with_attributes(#tag, attributes);
)
};
if let Some(parent) = parent {
items.push(quote!(
#el
#parent.append_child(#id);
))
} else {
items.push(quote!(
#el
#id
))
}
}
}
}
items
}