use maud_htmlescape::Escaper; use proc_macro::{ Delimiter, Group, Literal, quote, Span, Ident, TokenStream, TokenTree, }; use ast::*; pub fn generate(markups: Vec<Markup>, output_ident: TokenTree) -> TokenStream { let mut build = Builder::new(output_ident.clone()); Generator::new(output_ident).markups(markups, &mut build); build.finish() } struct Generator { output_ident: TokenTree, } impl Generator { fn new(output_ident: TokenTree) -> Generator { Generator { output_ident } } fn builder(&self) -> Builder { Builder::new(self.output_ident.clone()) } fn markups(&self, markups: Vec<Markup>, build: &mut Builder) { for markup in markups { self.markup(markup, build); } } fn markup(&self, markup: Markup, build: &mut Builder) { match markup { Markup::Block(Block { markups, span }) => { if markups.iter().any(|markup| matches!(*markup, Markup::Let { .. })) { build.push_tokens(self.block(Block { markups, span })); } else { self.markups(markups, build); } }, Markup::Literal { content, .. } => build.push_escaped(&content), Markup::Symbol { symbol } => self.name(symbol, build), Markup::Splice { expr } => build.push_tokens(self.splice(expr)), Markup::Element { name, attrs, body } => self.element(name, attrs, body, build), Markup::Let { tokens } => build.push_tokens(tokens), Markup::Special { segments } => { for segment in segments { build.push_tokens(self.special(segment)); } }, Markup::Match { head, arms, arms_span } => { build.push_tokens({ let body = arms .into_iter() .map(|arm| self.special(arm)) .collect(); let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body)); body.set_span(arms_span); quote!($head $body) }); }, } } fn block(&self, Block { markups, span }: Block) -> TokenStream { let mut build = self.builder(); self.markups(markups, &mut build); let mut block = TokenTree::Group(Group::new(Delimiter::Brace, build.finish())); block.set_span(span); TokenStream::from(block) } fn splice(&self, expr: TokenStream) -> TokenStream { let output_ident = self.output_ident.clone(); quote!({ // Create a local trait alias so that autoref works trait Render: maud::Render { fn __maud_render_to(&self, output_ident: &mut String) { maud::Render::render_to(self, output_ident); } } impl<T: maud::Render> Render for T {} $expr.__maud_render_to(&mut $output_ident); }) } fn element( &self, name: TokenStream, attrs: Attrs, body: Option<Box<Markup>>, build: &mut Builder, ) { build.push_str("<"); self.name(name.clone(), build); self.attrs(attrs, build); build.push_str(">"); if let Some(body) = body { self.markup(*body, build); build.push_str("</"); self.name(name, build); build.push_str(">"); } } fn name(&self, name: TokenStream, build: &mut Builder) { let string = name.into_iter().map(|token| token.to_string()).collect::<String>(); build.push_escaped(&string); } fn attrs(&self, attrs: Attrs, build: &mut Builder) { for Attribute { name, attr_type } in desugar_attrs(attrs) { match attr_type { AttrType::Normal { value } => { build.push_str(" "); self.name(name, build); build.push_str("=\""); self.markup(value, build); build.push_str("\""); }, AttrType::Empty { toggler: None } => { build.push_str(" "); self.name(name, build); }, AttrType::Empty { toggler: Some(toggler) } => { let head = desugar_toggler(toggler); build.push_tokens({ let mut build = self.builder(); build.push_str(" "); self.name(name, &mut build); let body = build.finish(); quote!($head { $body }) }) }, } } } fn special(&self, Special { head, body }: Special) -> TokenStream { let body = self.block(body); quote!($head $body) } } //////////////////////////////////////////////////////// fn desugar_attrs(Attrs { classes_static, classes_toggled, ids, attrs }: Attrs) -> Vec<Attribute> { let classes = desugar_classes_or_ids("class", classes_static, classes_toggled); let ids = desugar_classes_or_ids("id", ids, vec![]); classes.into_iter().chain(ids).chain(attrs).collect() } fn desugar_classes_or_ids( attr_name: &'static str, values_static: Vec<ClassOrId>, values_toggled: Vec<(ClassOrId, Toggler)>, ) -> Option<Attribute> { if values_static.is_empty() && values_toggled.is_empty() { return None; } let mut markups = Vec::new(); let mut leading_space = false; for symbol in values_static { markups.extend(prepend_leading_space(symbol, &mut leading_space)); } for (symbol, toggler) in values_toggled { let body = Block { markups: prepend_leading_space(symbol, &mut leading_space), span: toggler.cond_span, }; let head = desugar_toggler(toggler); markups.push(Markup::Special { segments: vec![Special { head, body }], }); } Some(Attribute { name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))), attr_type: AttrType::Normal { value: Markup::Block(Block { markups, span: Span::call_site(), }), }, }) } fn prepend_leading_space(symbol: TokenStream, leading_space: &mut bool) -> Vec<Markup> { let mut markups = Vec::new(); if *leading_space { markups.push(Markup::Literal { content: " ".to_owned(), span: span_tokens(symbol.clone()), }); } *leading_space = true; markups.push(Markup::Symbol { symbol }); markups } fn desugar_toggler(Toggler { mut cond, cond_span }: Toggler) -> TokenStream { // If the expression contains an opening brace `{`, // wrap it in parentheses to avoid parse errors if cond.clone().into_iter().any(|token| match token { TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => true, _ => false, }) { let mut wrapped_cond = TokenTree::Group(Group::new(Delimiter::Parenthesis, cond)); wrapped_cond.set_span(cond_span); cond = TokenStream::from(wrapped_cond); } quote!(if $cond) } fn span_tokens<I: IntoIterator<Item=TokenTree>>(tokens: I) -> Span { tokens .into_iter() .fold(None, |span: Option<Span>, token| Some(match span { None => token.span(), Some(span) => span.join(token.span()).unwrap_or(span), })) .unwrap_or_else(Span::def_site) } //////////////////////////////////////////////////////// struct Builder { output_ident: TokenTree, tokens: Vec<TokenTree>, tail: String, } impl Builder { fn new(output_ident: TokenTree) -> Builder { Builder { output_ident, tokens: Vec::new(), tail: String::new(), } } fn push_str(&mut self, string: &str) { self.tail.push_str(string); } fn push_escaped(&mut self, string: &str) { use std::fmt::Write; Escaper::new(&mut self.tail).write_str(string).unwrap(); } fn push_tokens<T: IntoIterator<Item=TokenTree>>(&mut self, tokens: T) { self.cut(); self.tokens.extend(tokens); } fn cut(&mut self) { if self.tail.is_empty() { return; } let push_str_expr = { let output_ident = self.output_ident.clone(); let string = TokenTree::Literal(Literal::string(&self.tail)); quote!($output_ident.push_str($string);) }; self.tail.clear(); self.tokens.extend(push_str_expr); } fn finish(mut self) -> TokenStream { self.cut(); self.tokens.into_iter().collect() } }