diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-29 03:54:54 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-29 03:54:54 +0800 |
| commit | ca7527681b609fedc368ea973022b004469035e6 (patch) | |
| tree | 47109782fa5a4435889d93a823db3a4e1f5e7bd6 /just_template_macros/src | |
| parent | ba15b7c06468cb6c52c8d2a53419fd83f9ebcb8b (diff) | |
feat(just_template): add proc-macro `tmpl!` and restructure crate
Move the old `tmpl!` and `tmpl_param!` macros into a dedicated
`just_template_macros` proc-macro crate. The new `tmpl!` macro supports
both simple parameter assignment and multi-arm implementation blocks
with per-arm overrides. Remove the deprecated `deprecated` module and
update tests accordingly.
Diffstat (limited to 'just_template_macros/src')
| -rw-r--r-- | just_template_macros/src/lib.rs | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/just_template_macros/src/lib.rs b/just_template_macros/src/lib.rs new file mode 100644 index 0000000..3f0b4fb --- /dev/null +++ b/just_template_macros/src/lib.rs @@ -0,0 +1,206 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Ident, Token, braced}; + +// ── Parsing ───────────────────────────────────────────────────────────────── + +/// Top-level item inside `tmpl! { }` or after the template ident in `tmpl!(tmpl, ...)` +enum TopItem { + /// `key = value` — simple parameter + Param { key: Ident, value: Expr }, + /// `name { arm, arm, ... }` — implementation block + ImplBlock { name: Ident, arms: Vec<Arm> }, +} + +/// An arm inside an implementation block +enum Arm { + /// `key = value` — single-param arm + Single { key: Ident, value: Expr }, + /// `{ key = value, key = value, ... }` — multi-param arm + Multi(Vec<(Ident, Expr)>), +} + +// ── Parse implementations ────────────────────────────────────────────────── + +impl Parse for TopItem { + fn parse(input: ParseStream) -> syn::Result<Self> { + let ident: Ident = input.parse()?; + let ahead = input.lookahead1(); + if ahead.peek(Token![=]) { + let _eq: Token![=] = input.parse()?; + let value: Expr = input.parse()?; + Ok(TopItem::Param { key: ident, value }) + } else if ahead.peek(syn::token::Brace) { + let content; + braced!(content in input); + let arms: Vec<Arm> = content + .parse_terminated(Arm::parse, Token![,])? + .into_iter() + .collect(); + Ok(TopItem::ImplBlock { name: ident, arms }) + } else { + Err(ahead.error()) + } + } +} + +impl Parse for Arm { + fn parse(input: ParseStream) -> syn::Result<Self> { + if input.peek(syn::token::Brace) { + let content; + braced!(content in input); + let params: Vec<(Ident, Expr)> = content + .parse_terminated(Self::parse_single, Token![,])? + .into_iter() + .collect(); + Ok(Arm::Multi(params)) + } else { + let (key, value) = Self::parse_single(input)?; + Ok(Arm::Single { key, value }) + } + } +} + +impl Arm { + fn parse_single(input: ParseStream) -> syn::Result<(Ident, Expr)> { + let key: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; + let value: Expr = input.parse()?; + Ok((key, value)) + } +} + +// ── Macro entry point ────────────────────────────────────────────────────── + +fn split_input(input: ParseStream) -> syn::Result<(Ident, Vec<TopItem>)> { + // Try to parse: `ident , items` (explicit template variable) + // If the first token is an ident followed by `,`, it's explicit. + // Otherwise, default to `tmpl` and parse as items. + + let first: Ident = input.parse()?; + if input.peek(Token![,]) { + let _comma: Token![,] = input.parse()?; + let items: Vec<TopItem> = input + .parse_terminated(TopItem::parse, Token![,])? + .into_iter() + .collect(); + Ok((first, items)) + } else { + // The first ident is actually the start of the first item. + // We need to re-parse. Use `syn::Result` to signal this. + // Tricky: we've already consumed `first`. Since syn::parse uses + // ParseStream which is a cursor, we actually can't rewind easily. + // + // Solution: use a custom approach — treat the first ident as + // the start of an item, and parse the rest as items. + let items = { + // Re-build: first ident + rest + let mut items: Vec<TopItem> = Vec::new(); + + // Parse the first item starting with `first` + let ahead = input.lookahead1(); + if ahead.peek(Token![=]) { + let _eq: Token![=] = input.parse()?; + let value: Expr = input.parse()?; + items.push(TopItem::Param { key: first, value }); + } else if ahead.peek(syn::token::Brace) { + let content; + braced!(content in input); + let arms: Vec<Arm> = content + .parse_terminated(Arm::parse, Token![,])? + .into_iter() + .collect(); + items.push(TopItem::ImplBlock { name: first, arms }); + } else { + return Err(ahead.error()); + } + + // Parse remaining items + while !input.is_empty() { + let _comma: Token![,] = input.parse()?; + if input.is_empty() { + break; // trailing comma + } + items.push(input.parse()?); + } + + items + }; + + Ok((Ident::new("tmpl", proc_macro2::Span::call_site()), items)) + } +} + +#[proc_macro] +pub fn tmpl(input: TokenStream) -> TokenStream { + match syn::parse::Parser::parse(split_input, input) { + Ok((template_var, items)) => { + let stmts = generate(&template_var, &items); + let expanded = quote! { { #(#stmts)* } }; + expanded.into() + } + Err(e) => e.to_compile_error().into(), + } +} + +// ── Code generation ──────────────────────────────────────────────────────── + +fn generate(template_var: &Ident, items: &[TopItem]) -> Vec<proc_macro2::TokenStream> { + let mut stmts: Vec<proc_macro2::TokenStream> = Vec::new(); + + for item in items { + match item { + TopItem::Param { key, value } => { + let key_str = key.to_string(); + stmts.push(quote! { + #template_var.insert_param( + #key_str.to_string(), + ::std::string::ToString::to_string(&#value), + ); + }); + } + TopItem::ImplBlock { name, arms } => { + let name_str = name.to_string(); + // Generate push statements for each arm + let push_stmts: Vec<proc_macro2::TokenStream> = + arms.iter().map(|arm| gen_arm_push(name, arm)).collect(); + stmts.push(quote! { + let #name = #template_var.add_impl(#name_str.to_string()); + #(#push_stmts)* + }); + } + } + } + + stmts +} + +fn gen_arm_push(name: &Ident, arm: &Arm) -> proc_macro2::TokenStream { + match arm { + Arm::Single { key, value } => { + let key_str = key.to_string(); + quote! { + #name.push(::std::collections::HashMap::from([ + (#key_str.to_string(), ::std::string::ToString::to_string(&#value)), + ])); + } + } + Arm::Multi(params) => { + let entries: Vec<_> = params + .iter() + .map(|(key, value)| { + let k = key.to_string(); + quote! { + (#k.to_string(), ::std::string::ToString::to_string(&#value)) + } + }) + .collect(); + quote! { + #name.push(::std::collections::HashMap::from([ + #(#entries),* + ])); + } + } + } +} |
