298 lines
9.5 KiB
Rust
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()
|
|
}
|
|
}
|