maud/maud_macros/src/generate.rs
2020-08-31 11:57:55 +03:00

298 lines
9.5 KiB
Rust

use maud_htmlescape::Escaper;
use proc_macro2::{
Delimiter,
Group,
Literal,
Span,
Ident,
TokenStream,
TokenTree,
};
use quote::quote;
use proc_macro_error::SpanRange;
use crate::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, outer_span }) => {
if markups.iter().any(|markup| matches!(*markup, Markup::Let { .. })) {
build.push_tokens(self.block(Block { markups, outer_span }));
} else {
self.markups(markups, build);
}
},
Markup::Literal { content, .. } => build.push_escaped(&content),
Markup::Symbol { symbol } => self.name(symbol.into(), build),
Markup::Splice { expr, .. } => build.push_tokens(self.splice(expr.into())),
Markup::Element { name, attrs, body } => self.element(name.into(), attrs, body, build),
Markup::Let { tokens, .. } => build.push_tokens(tokens.into()),
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.match_arm(arm))
.collect();
let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body));
body.set_span(arms_span.collapse());
let head: TokenStream = head.into();
quote!(#head #body)
});
},
}
}
fn block(&self, Block { markups, outer_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(outer_span.collapse());
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 ::std::string::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: ElementBody,
build: &mut Builder,
) {
build.push_str("<");
self.name(name.clone(), build);
self.attrs(attrs, build);
build.push_str(">");
if let ElementBody::Block { block } = body {
self.markups(block.markups, 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.into(), build);
build.push_str("=\"");
self.markup(value, build);
build.push_str("\"");
},
AttrType::Empty { toggler: None } => {
build.push_str(" ");
self.name(name.into(), 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.into(), &mut build);
let body = build.finish();
quote!(#head { #body })
})
},
}
}
}
fn special(&self, Special { head, body, .. }: Special) -> TokenStream {
let body = self.block(body);
let head: TokenStream = head.into();
quote!(#head #body)
}
fn match_arm(&self, MatchArm { head, body }: MatchArm) -> TokenStream {
let body = self.block(body);
let head: TokenStream = head.into();
quote!(#head #body)
}
}
////////////////////////////////////////////////////////
fn desugar_attrs(attrs: Attrs) -> Vec<Attribute> {
let mut classes_static = vec![];
let mut classes_toggled = vec![];
let mut ids = vec![];
let mut attributes = vec![];
for attr in attrs {
match attr {
Attr::Class { name, toggler, .. } => {
if let Some(toggler) = toggler {
classes_toggled.push((name, toggler));
} else {
classes_static.push(name);
}
},
Attr::Id { name, .. } => ids.push(name),
Attr::Attribute { attribute } => attributes.push(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(attributes).collect()
}
fn desugar_classes_or_ids(
attr_name: &'static str,
values_static: Vec<Markup>,
values_toggled: Vec<(Markup, 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 name in values_static {
markups.extend(prepend_leading_space(name, &mut leading_space));
}
for (name, toggler) in values_toggled {
let body = Block {
markups: prepend_leading_space(name, &mut leading_space),
outer_span: toggler.cond_span,
};
let head = desugar_toggler(toggler);
markups.push(Markup::Special {
segments: vec![Special { at_span: SpanRange::call_site(), head: head.into(), body }],
});
}
Some(Attribute {
name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))).into(),
attr_type: AttrType::Normal {
value: Markup::Block(Block {
markups,
outer_span: SpanRange::call_site(),
}),
},
})
}
fn prepend_leading_space(name: Markup, leading_space: &mut bool) -> Vec<Markup> {
let mut markups = Vec::new();
if *leading_space {
markups.push(Markup::Literal {
content: " ".to_owned(),
span: name.span(),
});
}
*leading_space = true;
markups.push(name);
markups
}
fn desugar_toggler(Toggler { cond, cond_span }: Toggler) -> TokenStream {
let mut cond = TokenStream::from(cond);
// If the expression contains an opening brace `{`,
// wrap it in parentheses to avoid parse errors
if cond.clone().into_iter().any(is_braced_block) {
let mut wrapped_cond = TokenTree::Group(Group::new(Delimiter::Parenthesis, cond));
wrapped_cond.set_span(cond_span.collapse());
cond = TokenStream::from(wrapped_cond);
}
quote!(if #cond)
}
fn is_braced_block(token: TokenTree) -> bool {
matches!(token, TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace)
}
////////////////////////////////////////////////////////
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(&mut self, tokens: TokenStream) {
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()
}
}