maud/maud_macros/src/generate.rs
2018-05-29 19:18:30 +12:00

280 lines
8.7 KiB
Rust

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()
}
}