1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4mod codegen;
5mod compile;
6mod struct_gen;
7mod type_gen;
8
9use std::cell::RefCell;
10
11use codegen::{codegen_compiled_inline_template, codegen_segment, codegen_value, codegen_var_decl};
12use compile::{CompiledTemplateAst, load_and_compile, stem_from_path};
13use proc_macro::TokenStream;
14use quote::{format_ident, quote};
15use struct_gen::{StructGenSource, generate_struct_tokens};
16use syn::{
17 Ident, LitStr, Token,
18 parse::{Parse, ParseStream},
19 parse_macro_input,
20};
21use type_gen::generate_type_alias_tokens;
22
23thread_local! {
24 static CRATE_PATH: RefCell<proc_macro2::TokenStream> = RefCell::new(quote! { ::md_tmpl });
31}
32
33pub(crate) fn crate_path() -> proc_macro2::TokenStream {
35 CRATE_PATH.with(|cp| cp.borrow().clone())
36}
37
38fn with_crate_path<F: FnOnce() -> R, R>(path: proc_macro2::TokenStream, f: F) -> R {
40 CRATE_PATH.with(|cp| {
41 let old = cp.replace(path);
42 let result = f();
43 cp.replace(old);
44 result
45 })
46}
47
48struct IncludeTemplateInput {
55 path: LitStr,
56 struct_name: Option<Ident>,
57 custom_name: Option<Ident>,
58 crate_path: Option<syn::Path>,
59 env: Vec<(String, syn::Expr)>,
60 import_paths: Vec<(String, syn::Path)>,
61}
62
63impl Parse for IncludeTemplateInput {
64 fn parse(input: ParseStream) -> syn::Result<Self> {
65 let path: LitStr = input.parse()?;
66
67 let struct_name = if input.peek(Token![as]) {
69 let _as: Token![as] = input.parse()?;
70 Some(input.parse()?)
71 } else {
72 None
73 };
74
75 let custom_name = if input.peek(Token![=>]) {
77 let _arrow: Token![=>] = input.parse()?;
78 Some(input.parse()?)
79 } else {
80 None
81 };
82
83 let mut crate_path = None;
86 let mut env = Vec::new();
87 let mut import_paths = Vec::new();
88 while input.peek(Token![,]) {
89 let _comma: Token![,] = input.parse()?;
90 if input.is_empty() {
91 break;
92 }
93 if input.peek(Token![crate]) {
94 let _kw: Token![crate] = input.parse()?;
95 let _eq: Token![=] = input.parse()?;
96 crate_path = Some(input.parse()?);
97 } else {
98 let kw: Ident = input.parse()?;
99 if kw == "env" {
100 let _eq: Token![=] = input.parse()?;
101 env = parse_env_block(input)?;
102 } else if kw == "imports" {
103 let _eq: Token![=] = input.parse()?;
104 import_paths = parse_imports_block(input)?;
105 } else {
106 return Err(syn::Error::new(
107 kw.span(),
108 format!("unknown option '{kw}', expected 'crate', 'env', or 'imports'"),
109 ));
110 }
111 }
112 }
113
114 Ok(Self {
115 path,
116 struct_name,
117 custom_name,
118 crate_path,
119 env,
120 import_paths,
121 })
122 }
123}
124
125struct InlineTemplateInput {
130 source: LitStr,
131 struct_name: Option<Ident>,
132 name: Ident,
133 crate_path: Option<syn::Path>,
134 env: Vec<(String, syn::Expr)>,
135 import_paths: Vec<(String, syn::Path)>,
136}
137
138impl Parse for InlineTemplateInput {
139 fn parse(input: ParseStream) -> syn::Result<Self> {
140 let source: LitStr = input.parse()?;
141 let struct_name = if input.peek(Token![as]) {
142 let _as: Token![as] = input.parse()?;
143 Some(input.parse()?)
144 } else {
145 None
146 };
147 let _: Token![=>] = input.parse()?;
148 let name: Ident = input.parse()?;
149 let mut crate_path = None;
150 let mut env = Vec::new();
151 let mut import_paths = Vec::new();
152 while input.peek(Token![,]) {
153 let _comma: Token![,] = input.parse()?;
154 if input.is_empty() {
155 break;
156 }
157 if input.peek(Token![crate]) {
158 let _kw: Token![crate] = input.parse()?;
159 let _eq: Token![=] = input.parse()?;
160 crate_path = Some(input.parse()?);
161 } else {
162 let kw: Ident = input.parse()?;
163 if kw == "env" {
164 let _eq: Token![=] = input.parse()?;
165 env = parse_env_block(input)?;
166 } else if kw == "imports" {
167 let _eq: Token![=] = input.parse()?;
168 import_paths = parse_imports_block(input)?;
169 } else {
170 return Err(syn::Error::new(
171 kw.span(),
172 format!("unknown option '{kw}', expected 'crate', 'env', or 'imports'"),
173 ));
174 }
175 }
176 }
177 Ok(Self {
178 source,
179 struct_name,
180 name,
181 crate_path,
182 env,
183 import_paths,
184 })
185 }
186}
187
188const RUST_KEYWORDS: &[&str] = &[
191 "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
193 "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
194 "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
195 "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized",
197 "virtual", "yield", "try", "union",
199];
200
201const UNESCAPABLE_KEYWORDS: &[&str] = &["self", "Self", "super", "crate"];
204
205pub(crate) fn make_ident(name: &str) -> Ident {
212 if UNESCAPABLE_KEYWORDS.contains(&name) {
213 format_ident!("__{}", name)
214 } else if RUST_KEYWORDS.contains(&name) {
215 format_ident!("r#{}", name)
216 } else {
217 Ident::new(name, proc_macro2::Span::call_site())
218 }
219}
220
221pub(crate) fn serde_rename_attr(name: &str) -> proc_macro2::TokenStream {
225 if UNESCAPABLE_KEYWORDS.contains(&name) {
226 quote! { #[serde(rename = #name)] }
227 } else {
228 quote! {}
229 }
230}
231
232fn parse_env_block(input: ParseStream) -> syn::Result<Vec<(String, syn::Expr)>> {
237 let content;
238 syn::braced!(content in input);
239 let mut entries = Vec::new();
240 while !content.is_empty() {
241 let key: Ident = content.parse()?;
242 let _colon: Token![:] = content.parse()?;
243 let expr: syn::Expr = content.parse()?;
244 entries.push((key.to_string(), expr));
245 if content.peek(Token![,]) {
246 let _comma: Token![,] = content.parse()?;
247 }
248 }
249 Ok(entries)
250}
251
252fn parse_imports_block(input: ParseStream) -> syn::Result<Vec<(String, syn::Path)>> {
259 let content;
260 syn::braced!(content in input);
261 let mut entries = Vec::new();
262 while !content.is_empty() {
263 let stem: Ident = content.parse()?;
264 let _eq: Token![=] = content.parse()?;
265 let path: syn::Path = content.parse()?;
266 entries.push((stem.to_string(), path));
267 if content.peek(Token![,]) {
268 let _comma: Token![,] = content.parse()?;
269 }
270 }
271 Ok(entries)
272}
273
274fn build_imported_type_paths(
283 fm: &md_tmpl_core::Frontmatter,
284 import_paths: &[(String, syn::Path)],
285) -> std::collections::HashMap<String, proc_macro2::TokenStream> {
286 let mut out = std::collections::HashMap::new();
287 for (param, (stem, type_name)) in &fm.imported_type_params {
288 if let Some((_, base)) = import_paths.iter().find(|(s, _)| s == stem) {
289 let ty = format_ident!("{}", type_name);
290 out.insert(param.clone(), quote! { #base::#ty });
291 }
292 }
293 out
294}
295
296fn eval_env_expr(expr: &syn::Expr) -> Result<md_tmpl_core::Value, String> {
304 match expr {
305 syn::Expr::Lit(lit) => match &lit.lit {
306 syn::Lit::Str(s) => Ok(md_tmpl_core::Value::Str(s.value())),
307 syn::Lit::Int(i) => {
308 let n: i64 = i
309 .base10_parse()
310 .map_err(|e| format!("invalid integer: {e}"))?;
311 Ok(md_tmpl_core::Value::Int(n))
312 }
313 syn::Lit::Float(f) => {
314 let n: f64 = f
315 .base10_parse()
316 .map_err(|e| format!("invalid float: {e}"))?;
317 Ok(md_tmpl_core::Value::Float(n))
318 }
319 syn::Lit::Bool(b) => Ok(md_tmpl_core::Value::Bool(b.value)),
320 _ => Err("env value must be a string, int, float, or bool literal".to_string()),
321 },
322 syn::Expr::Path(p) => {
325 if p.path.is_ident("true") {
326 Ok(md_tmpl_core::Value::Bool(true))
327 } else if p.path.is_ident("false") {
328 Ok(md_tmpl_core::Value::Bool(false))
329 } else {
330 Err(format!(
331 "env value must be a literal, got path: {}",
332 quote! { #expr }
333 ))
334 }
335 }
336 _ => Err(format!(
337 "env value must be a literal, got: {}",
338 quote! { #expr }
339 )),
340 }
341}
342
343fn err_tokens(span: proc_macro2::Span, rel_path: &str, e: &str) -> TokenStream {
345 let msg = format!("template '{rel_path}': {e}");
346 syn::Error::new(span, msg).to_compile_error().into()
347}
348
349#[proc_macro]
394pub fn include_template(input: TokenStream) -> TokenStream {
395 let parsed = parse_macro_input!(input as IncludeTemplateInput);
396 let rel_path = parsed.path.value();
397
398 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
400 .env
401 .iter()
402 .map(|(k, expr)| {
403 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
404 (k.clone(), val)
405 })
406 .collect();
407 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
408 .iter()
409 .map(|(k, v)| (k.as_str(), v.clone()))
410 .collect();
411
412 let (full_path, ast) = match load_and_compile(&rel_path, &env_refs) {
413 Ok(v) => v,
414 Err(e) => return err_tokens(parsed.path.span(), &rel_path, &e),
415 };
416 let CompiledTemplateAst {
417 frontmatter: fm,
418 segments,
419 inline_templates,
420 source_hash,
421 } = ast;
422 let path_str = full_path.to_string_lossy().to_string();
423
424 let mod_ident = match parsed.custom_name {
426 Some(ident) => ident,
427 None => make_ident(&stem_from_path(&rel_path)),
428 };
429
430 let crate_path = parsed
432 .crate_path
433 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
434
435 with_crate_path(crate_path.clone(), || {
436 let segments_tokens = segments.iter().map(codegen_segment);
438 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
439 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
440 let v_tokens = codegen_compiled_inline_template(v);
441 quote! { (#k, #v_tokens) }
442 });
443 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
444 d.default_value.as_ref().map(|v| {
445 let name = &d.name;
446 let val_tokens = codegen_value(v);
447 quote! { (#name, #val_tokens) }
448 })
449 });
450 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
451 let val_tokens = codegen_value(v);
452 quote! { (#k, #val_tokens) }
453 });
454
455 let struct_name = parsed
457 .struct_name
458 .unwrap_or_else(|| format_ident!("Params"));
459 let source = StructGenSource::Module {
460 doc_path: &rel_path,
461 };
462 let imported_type_paths = build_imported_type_paths(&fm, &parsed.import_paths);
463 let struct_tokens =
464 generate_struct_tokens(&fm, &struct_name, &source, &imported_type_paths);
465
466 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
468
469 let name_token = if let Some(n) = &fm.name {
470 quote! { Some(#n) }
471 } else {
472 quote! { None }
473 };
474 let desc_token = if let Some(d) = &fm.description {
475 quote! { Some(#d) }
476 } else {
477 quote! { None }
478 };
479
480 let expanded = quote! {
481 pub mod #mod_ident {
482 const _: &str = include_str!(#path_str);
483
484 fn __init_template() -> #crate_path::Template {
485 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
486 segments: &[#(#segments_tokens),*],
487 declared_variables: &[#(#decls_tokens),*],
488 inline_templates: &[#(#inline_templates_tokens),*],
489 source_hash: #source_hash,
490 consts: &[#(#consts_tokens),*],
491 imported_consts: &[#(#imported_consts_tokens),*],
492 name: #name_token,
493 description: #desc_token,
494 })
495 }
496 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
497 #crate_path::__private::LazyLock::new(__init_template);
498
499 pub fn template() -> &'static #crate_path::Template {
501 &*__TEMPLATE
502 }
503
504 #struct_tokens
505 #(#type_alias_tokens)*
506 }
507 };
508 expanded.into()
509 })
510}
511
512#[proc_macro]
547pub fn template(input: TokenStream) -> TokenStream {
548 let parsed = parse_macro_input!(input as InlineTemplateInput);
549 let source = parsed.source.value();
550 let mod_ident = parsed.name;
551
552 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
553 let base_dir = std::path::Path::new(&manifest_dir);
554
555 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
557 .env
558 .iter()
559 .map(|(k, expr)| {
560 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
561 (k.clone(), val)
562 })
563 .collect();
564 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
565 .iter()
566 .map(|(k, v)| (k.as_str(), v.clone()))
567 .collect();
568
569 let ast = match compile::compile_template_to_ast(&source, base_dir, &env_refs) {
570 Ok(v) => v,
571 Err(e) => {
572 let msg = format!("inline template: {e}");
573 return syn::Error::new(parsed.source.span(), msg)
574 .to_compile_error()
575 .into();
576 }
577 };
578 let CompiledTemplateAst {
579 frontmatter: fm,
580 segments,
581 inline_templates,
582 source_hash,
583 } = ast;
584
585 let crate_path = parsed
587 .crate_path
588 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
589
590 with_crate_path(crate_path.clone(), || {
591 let segments_tokens = segments.iter().map(codegen_segment);
593 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
594 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
595 let v_tokens = codegen_compiled_inline_template(v);
596 quote! { (#k, #v_tokens) }
597 });
598 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
599 d.default_value.as_ref().map(|v| {
600 let name = &d.name;
601 let val_tokens = codegen_value(v);
602 quote! { (#name, #val_tokens) }
603 })
604 });
605 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
606 let val_tokens = codegen_value(v);
607 quote! { (#k, #val_tokens) }
608 });
609
610 let struct_name = parsed
612 .struct_name
613 .clone()
614 .unwrap_or_else(|| format_ident!("Params"));
615 let source = StructGenSource::Module {
616 doc_path: "<inline>",
617 };
618 let imported_type_paths = build_imported_type_paths(&fm, &parsed.import_paths);
619 let struct_tokens =
620 generate_struct_tokens(&fm, &struct_name, &source, &imported_type_paths);
621
622 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
624
625 let name_token = if let Some(n) = &fm.name {
626 quote! { Some(#n) }
627 } else {
628 quote! { None }
629 };
630 let desc_token = if let Some(d) = &fm.description {
631 quote! { Some(#d) }
632 } else {
633 quote! { None }
634 };
635
636 let expanded = quote! {
637 pub mod #mod_ident {
638 fn __init_template() -> #crate_path::Template {
639 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
640 segments: &[#(#segments_tokens),*],
641 declared_variables: &[#(#decls_tokens),*],
642 inline_templates: &[#(#inline_templates_tokens),*],
643 source_hash: #source_hash,
644 consts: &[#(#consts_tokens),*],
645 imported_consts: &[#(#imported_consts_tokens),*],
646 name: #name_token,
647 description: #desc_token,
648 })
649 }
650 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
651 #crate_path::__private::LazyLock::new(__init_template);
652
653 pub fn template() -> &'static #crate_path::Template {
655 &*__TEMPLATE
656 }
657
658 #struct_tokens
659 #(#type_alias_tokens)*
660 }
661 };
662 expanded.into()
663 })
664}