- Mantiene fijas las rutas absolutas en las funciones de la macro html!. - Introduce un alias interno del crate (`extern crate self as pagetop;`) en `src/lib.rs` para que las expansiones funcionen uniformemente en tests, ejemplos y dependencias externas.
384 lines
12 KiB
Rust
384 lines
12 KiB
Rust
use proc_macro2::{Ident, Span, TokenStream};
|
|
use quote::{quote, ToTokens};
|
|
use syn::{parse_quote, token::Brace, Expr, Local};
|
|
|
|
use crate::maud::{ast::*, escape};
|
|
|
|
pub fn generate(markups: Markups<Element>, output_ident: Ident) -> TokenStream {
|
|
let mut build = Builder::new(output_ident.clone());
|
|
Generator::new(output_ident).markups(markups, &mut build);
|
|
build.finish()
|
|
}
|
|
|
|
struct Generator {
|
|
output_ident: Ident,
|
|
}
|
|
|
|
impl Generator {
|
|
fn new(output_ident: Ident) -> Generator {
|
|
Generator { output_ident }
|
|
}
|
|
|
|
fn builder(&self) -> Builder {
|
|
Builder::new(self.output_ident.clone())
|
|
}
|
|
|
|
fn markups<E: Into<Element>>(&self, markups: Markups<E>, build: &mut Builder) {
|
|
for markup in markups.markups {
|
|
self.markup(markup, build);
|
|
}
|
|
}
|
|
|
|
fn markup<E: Into<Element>>(&self, markup: Markup<E>, build: &mut Builder) {
|
|
match markup {
|
|
Markup::Block(block) => {
|
|
if block.markups.markups.iter().any(|markup| {
|
|
matches!(
|
|
*markup,
|
|
Markup::ControlFlow(ControlFlow {
|
|
kind: ControlFlowKind::Let(_),
|
|
..
|
|
})
|
|
)
|
|
}) {
|
|
self.block(block, build);
|
|
} else {
|
|
self.markups(block.markups, build);
|
|
}
|
|
}
|
|
Markup::Lit(lit) => build.push_escaped(&lit.to_string()),
|
|
Markup::Splice { expr, .. } => self.splice(expr, build),
|
|
Markup::Element(element) => self.element(element.into(), build),
|
|
Markup::ControlFlow(control_flow) => self.control_flow(control_flow, build),
|
|
Markup::Semi(_) => {}
|
|
}
|
|
}
|
|
|
|
fn block<E: Into<Element>>(&self, block: Block<E>, build: &mut Builder) {
|
|
let markups = {
|
|
let mut build = self.builder();
|
|
self.markups(block.markups, &mut build);
|
|
build.finish()
|
|
};
|
|
|
|
build.push_tokens(quote!({ #markups }));
|
|
}
|
|
|
|
fn splice(&self, expr: Expr, build: &mut Builder) {
|
|
let output_ident = &self.output_ident;
|
|
build.push_tokens(
|
|
quote!(pagetop::html::html_private::render_to!(&(#expr), &mut #output_ident);),
|
|
);
|
|
}
|
|
|
|
fn element(&self, element: Element, build: &mut Builder) {
|
|
let element_name = element.name.clone().unwrap_or_else(|| parse_quote!(div));
|
|
build.push_str("<");
|
|
self.name(element_name.clone(), build);
|
|
self.attrs(element.attrs, build);
|
|
build.push_str(">");
|
|
if let ElementBody::Block(block) = element.body {
|
|
self.markups(block.markups, build);
|
|
build.push_str("</");
|
|
self.name(element_name, build);
|
|
build.push_str(">");
|
|
}
|
|
}
|
|
|
|
fn name(&self, name: HtmlName, build: &mut Builder) {
|
|
build.push_escaped(&name.to_string());
|
|
}
|
|
|
|
fn name_or_markup(&self, name: HtmlNameOrMarkup, build: &mut Builder) {
|
|
match name {
|
|
HtmlNameOrMarkup::HtmlName(name) => self.name(name, build),
|
|
HtmlNameOrMarkup::Markup(markup) => self.markup(markup, build),
|
|
}
|
|
}
|
|
|
|
fn attr(&self, name: HtmlName, value: AttributeType, build: &mut Builder) {
|
|
match value {
|
|
AttributeType::Normal { value, .. } => {
|
|
build.push_str(" ");
|
|
self.name(name, build);
|
|
build.push_str("=\"");
|
|
self.markup(value, build);
|
|
build.push_str("\"");
|
|
}
|
|
AttributeType::Optional {
|
|
toggler: Toggler { cond, .. },
|
|
..
|
|
} => {
|
|
let inner_value: Expr = parse_quote!(inner_value);
|
|
|
|
let body = {
|
|
let mut build = self.builder();
|
|
build.push_str(" ");
|
|
self.name(name, &mut build);
|
|
build.push_str("=\"");
|
|
self.splice(inner_value.clone(), &mut build);
|
|
build.push_str("\"");
|
|
build.finish()
|
|
};
|
|
build.push_tokens(quote!(if let Some(#inner_value) = (#cond) { #body }));
|
|
}
|
|
AttributeType::Empty(None) => {
|
|
build.push_str(" ");
|
|
self.name(name, build);
|
|
}
|
|
AttributeType::Empty(Some(Toggler { cond, .. })) => {
|
|
let body = {
|
|
let mut build = self.builder();
|
|
build.push_str(" ");
|
|
self.name(name, &mut build);
|
|
build.finish()
|
|
};
|
|
build.push_tokens(quote!(if (#cond) { #body }));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
|
let (classes, id, named_attrs) = split_attrs(attrs);
|
|
|
|
if !classes.is_empty() {
|
|
let mut toggle_class_exprs = vec![];
|
|
|
|
build.push_str(" ");
|
|
self.name(parse_quote!(class), build);
|
|
build.push_str("=\"");
|
|
for (i, (name, toggler)) in classes.into_iter().enumerate() {
|
|
if let Some(toggler) = toggler {
|
|
toggle_class_exprs.push((i > 0, name, toggler));
|
|
} else {
|
|
if i > 0 {
|
|
build.push_str(" ");
|
|
}
|
|
self.name_or_markup(name, build);
|
|
}
|
|
}
|
|
|
|
for (not_first, name, toggler) in toggle_class_exprs {
|
|
let body = {
|
|
let mut build = self.builder();
|
|
if not_first {
|
|
build.push_str(" ");
|
|
}
|
|
self.name_or_markup(name, &mut build);
|
|
build.finish()
|
|
};
|
|
build.push_tokens(quote!(if (#toggler) { #body }));
|
|
}
|
|
|
|
build.push_str("\"");
|
|
}
|
|
|
|
if let Some(id) = id {
|
|
build.push_str(" ");
|
|
self.name(parse_quote!(id), build);
|
|
build.push_str("=\"");
|
|
self.name_or_markup(id, build);
|
|
build.push_str("\"");
|
|
}
|
|
|
|
for (name, attr_type) in named_attrs {
|
|
self.attr(name, attr_type, build);
|
|
}
|
|
}
|
|
|
|
fn control_flow<E: Into<Element>>(&self, control_flow: ControlFlow<E>, build: &mut Builder) {
|
|
match control_flow.kind {
|
|
ControlFlowKind::If(if_) => self.control_flow_if(if_, build),
|
|
ControlFlowKind::Let(let_) => self.control_flow_let(let_, build),
|
|
ControlFlowKind::For(for_) => self.control_flow_for(for_, build),
|
|
ControlFlowKind::While(while_) => self.control_flow_while(while_, build),
|
|
ControlFlowKind::Match(match_) => self.control_flow_match(match_, build),
|
|
}
|
|
}
|
|
|
|
fn control_flow_if<E: Into<Element>>(
|
|
&self,
|
|
IfExpr {
|
|
if_token,
|
|
cond,
|
|
then_branch,
|
|
else_branch,
|
|
}: IfExpr<E>,
|
|
build: &mut Builder,
|
|
) {
|
|
build.push_tokens(quote!(#if_token #cond));
|
|
self.block(then_branch, build);
|
|
|
|
if let Some((_, else_token, if_or_block)) = else_branch {
|
|
build.push_tokens(quote!(#else_token));
|
|
self.control_flow_if_or_block(*if_or_block, build);
|
|
}
|
|
}
|
|
|
|
fn control_flow_if_or_block<E: Into<Element>>(
|
|
&self,
|
|
if_or_block: IfOrBlock<E>,
|
|
build: &mut Builder,
|
|
) {
|
|
match if_or_block {
|
|
IfOrBlock::If(if_) => self.control_flow_if(if_, build),
|
|
IfOrBlock::Block(block) => self.block(block, build),
|
|
}
|
|
}
|
|
|
|
fn control_flow_let(&self, let_: Local, build: &mut Builder) {
|
|
build.push_tokens(let_.to_token_stream());
|
|
}
|
|
|
|
fn control_flow_for<E: Into<Element>>(
|
|
&self,
|
|
ForExpr {
|
|
for_token,
|
|
pat,
|
|
in_token,
|
|
expr,
|
|
body,
|
|
}: ForExpr<E>,
|
|
build: &mut Builder,
|
|
) {
|
|
build.push_tokens(quote!(#for_token #pat #in_token (#expr)));
|
|
self.block(body, build);
|
|
}
|
|
|
|
fn control_flow_while<E: Into<Element>>(
|
|
&self,
|
|
WhileExpr {
|
|
while_token,
|
|
cond,
|
|
body,
|
|
}: WhileExpr<E>,
|
|
build: &mut Builder,
|
|
) {
|
|
build.push_tokens(quote!(#while_token #cond));
|
|
self.block(body, build);
|
|
}
|
|
|
|
fn control_flow_match<E: Into<Element>>(
|
|
&self,
|
|
MatchExpr {
|
|
match_token,
|
|
expr,
|
|
brace_token,
|
|
arms,
|
|
}: MatchExpr<E>,
|
|
build: &mut Builder,
|
|
) {
|
|
let arms = {
|
|
let mut build = self.builder();
|
|
for MatchArm {
|
|
pat,
|
|
guard,
|
|
fat_arrow_token,
|
|
body,
|
|
comma_token,
|
|
} in arms
|
|
{
|
|
build.push_tokens(quote!(#pat));
|
|
if let Some((if_token, cond)) = guard {
|
|
build.push_tokens(quote!(#if_token #cond));
|
|
}
|
|
build.push_tokens(quote!(#fat_arrow_token));
|
|
self.block(
|
|
Block {
|
|
brace_token: Brace(Span::call_site()),
|
|
markups: Markups {
|
|
markups: vec![body],
|
|
},
|
|
},
|
|
&mut build,
|
|
);
|
|
build.push_tokens(quote!(#comma_token));
|
|
}
|
|
build.finish()
|
|
};
|
|
|
|
let mut arm_block = TokenStream::new();
|
|
|
|
brace_token.surround(&mut arm_block, |tokens| {
|
|
arms.to_tokens(tokens);
|
|
});
|
|
|
|
build.push_tokens(quote!(#match_token #expr #arm_block));
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
fn split_attrs(
|
|
attrs: Vec<Attribute>,
|
|
) -> (
|
|
Vec<(HtmlNameOrMarkup, Option<Expr>)>,
|
|
Option<HtmlNameOrMarkup>,
|
|
Vec<(HtmlName, AttributeType)>,
|
|
) {
|
|
let mut classes = vec![];
|
|
let mut id = None;
|
|
let mut named_attrs = vec![];
|
|
|
|
for attr in attrs {
|
|
match attr {
|
|
Attribute::Class { name, toggler, .. } => {
|
|
classes.push((name, toggler.map(|toggler| toggler.cond)))
|
|
}
|
|
Attribute::Id { name, .. } => id = Some(name),
|
|
Attribute::Named { name, attr_type } => named_attrs.push((name, attr_type)),
|
|
}
|
|
}
|
|
|
|
(classes, id, named_attrs)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
struct Builder {
|
|
output_ident: Ident,
|
|
tokens: TokenStream,
|
|
tail: String,
|
|
}
|
|
|
|
impl Builder {
|
|
fn new(output_ident: Ident) -> Builder {
|
|
Builder {
|
|
output_ident,
|
|
tokens: TokenStream::new(),
|
|
tail: String::new(),
|
|
}
|
|
}
|
|
|
|
fn push_str(&mut self, string: &'static str) {
|
|
self.tail.push_str(string);
|
|
}
|
|
|
|
fn push_escaped(&mut self, string: &str) {
|
|
escape::escape_to_string(string, &mut self.tail);
|
|
}
|
|
|
|
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 tail = &self.tail;
|
|
quote!(#output_ident.push_str(#tail);)
|
|
};
|
|
self.tail.clear();
|
|
self.tokens.extend(push_str_expr);
|
|
}
|
|
|
|
fn finish(mut self) -> TokenStream {
|
|
self.cut();
|
|
self.tokens
|
|
}
|
|
}
|