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
23fn dep_paths_to_strings(paths: &[std::path::PathBuf]) -> Vec<String> {
26 paths
27 .iter()
28 .map(|p| p.to_string_lossy().into_owned())
29 .collect()
30}
31
32thread_local! {
33 static CRATE_PATH: RefCell<proc_macro2::TokenStream> = RefCell::new(quote! { ::md_tmpl });
40}
41
42pub(crate) fn crate_path() -> proc_macro2::TokenStream {
44 CRATE_PATH.with(|cp| cp.borrow().clone())
45}
46
47fn with_crate_path<F: FnOnce() -> R, R>(path: proc_macro2::TokenStream, f: F) -> R {
49 CRATE_PATH.with(|cp| {
50 let old = cp.replace(path);
51 let result = f();
52 cp.replace(old);
53 result
54 })
55}
56
57struct IncludeTemplateInput {
64 path: LitStr,
65 struct_name: Option<Ident>,
66 custom_name: Option<Ident>,
67 crate_path: Option<syn::Path>,
68 env: Vec<(String, syn::Expr)>,
69 import_paths: Vec<(String, syn::Path)>,
70}
71
72impl Parse for IncludeTemplateInput {
73 fn parse(input: ParseStream) -> syn::Result<Self> {
74 let path: LitStr = input.parse()?;
75
76 let struct_name = if input.peek(Token![as]) {
78 let _as: Token![as] = input.parse()?;
79 Some(input.parse()?)
80 } else {
81 None
82 };
83
84 let custom_name = if input.peek(Token![=>]) {
86 let _arrow: Token![=>] = input.parse()?;
87 Some(input.parse()?)
88 } else {
89 None
90 };
91
92 let mut crate_path = None;
95 let mut env = Vec::new();
96 let mut import_paths = Vec::new();
97 while input.peek(Token![,]) {
98 let _comma: Token![,] = input.parse()?;
99 if input.is_empty() {
100 break;
101 }
102 if input.peek(Token![crate]) {
103 let _kw: Token![crate] = input.parse()?;
104 let _eq: Token![=] = input.parse()?;
105 crate_path = Some(input.parse()?);
106 } else {
107 let kw: Ident = input.parse()?;
108 if kw == "env" {
109 let _eq: Token![=] = input.parse()?;
110 env = parse_env_block(input)?;
111 } else if kw == "imports" {
112 let _eq: Token![=] = input.parse()?;
113 import_paths = parse_imports_block(input)?;
114 } else {
115 return Err(syn::Error::new(
116 kw.span(),
117 format!("unknown option '{kw}', expected 'crate', 'env', or 'imports'"),
118 ));
119 }
120 }
121 }
122
123 Ok(Self {
124 path,
125 struct_name,
126 custom_name,
127 crate_path,
128 env,
129 import_paths,
130 })
131 }
132}
133
134struct InlineTemplateInput {
139 source: LitStr,
140 struct_name: Option<Ident>,
141 name: Ident,
142 crate_path: Option<syn::Path>,
143 env: Vec<(String, syn::Expr)>,
144 import_paths: Vec<(String, syn::Path)>,
145}
146
147impl Parse for InlineTemplateInput {
148 fn parse(input: ParseStream) -> syn::Result<Self> {
149 let source: LitStr = input.parse()?;
150 let struct_name = if input.peek(Token![as]) {
151 let _as: Token![as] = input.parse()?;
152 Some(input.parse()?)
153 } else {
154 None
155 };
156 let _: Token![=>] = input.parse()?;
157 let name: Ident = input.parse()?;
158 let mut crate_path = None;
159 let mut env = Vec::new();
160 let mut import_paths = Vec::new();
161 while input.peek(Token![,]) {
162 let _comma: Token![,] = input.parse()?;
163 if input.is_empty() {
164 break;
165 }
166 if input.peek(Token![crate]) {
167 let _kw: Token![crate] = input.parse()?;
168 let _eq: Token![=] = input.parse()?;
169 crate_path = Some(input.parse()?);
170 } else {
171 let kw: Ident = input.parse()?;
172 if kw == "env" {
173 let _eq: Token![=] = input.parse()?;
174 env = parse_env_block(input)?;
175 } else if kw == "imports" {
176 let _eq: Token![=] = input.parse()?;
177 import_paths = parse_imports_block(input)?;
178 } else {
179 return Err(syn::Error::new(
180 kw.span(),
181 format!("unknown option '{kw}', expected 'crate', 'env', or 'imports'"),
182 ));
183 }
184 }
185 }
186 Ok(Self {
187 source,
188 struct_name,
189 name,
190 crate_path,
191 env,
192 import_paths,
193 })
194 }
195}
196
197const RUST_KEYWORDS: &[&str] = &[
200 "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
202 "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
203 "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
204 "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized",
206 "virtual", "yield", "try", "union",
208];
209
210const UNESCAPABLE_KEYWORDS: &[&str] = &["self", "Self", "super", "crate"];
213
214pub(crate) fn make_ident(name: &str) -> Ident {
221 if UNESCAPABLE_KEYWORDS.contains(&name) {
222 format_ident!("__{}", name)
223 } else if RUST_KEYWORDS.contains(&name) {
224 format_ident!("r#{}", name)
225 } else {
226 Ident::new(name, proc_macro2::Span::call_site())
227 }
228}
229
230pub(crate) fn serde_rename_attr(name: &str) -> proc_macro2::TokenStream {
234 if UNESCAPABLE_KEYWORDS.contains(&name) {
235 quote! { #[serde(rename = #name)] }
236 } else {
237 quote! {}
238 }
239}
240
241fn parse_env_block(input: ParseStream) -> syn::Result<Vec<(String, syn::Expr)>> {
246 let content;
247 syn::braced!(content in input);
248 let mut entries = Vec::new();
249 while !content.is_empty() {
250 let key: Ident = content.parse()?;
251 let _colon: Token![:] = content.parse()?;
252 let expr: syn::Expr = content.parse()?;
253 entries.push((key.to_string(), expr));
254 if content.peek(Token![,]) {
255 let _comma: Token![,] = content.parse()?;
256 }
257 }
258 Ok(entries)
259}
260
261fn parse_imports_block(input: ParseStream) -> syn::Result<Vec<(String, syn::Path)>> {
268 let content;
269 syn::braced!(content in input);
270 let mut entries = Vec::new();
271 while !content.is_empty() {
272 let stem: Ident = content.parse()?;
273 let _eq: Token![=] = content.parse()?;
274 let path: syn::Path = content.parse()?;
275 entries.push((stem.to_string(), path));
276 if content.peek(Token![,]) {
277 let _comma: Token![,] = content.parse()?;
278 }
279 }
280 Ok(entries)
281}
282
283fn build_imported_type_paths(
292 fm: &md_tmpl_core::Frontmatter,
293 import_paths: &[(String, syn::Path)],
294) -> std::collections::HashMap<String, proc_macro2::TokenStream> {
295 let mut out = std::collections::HashMap::new();
296 for (param, (stem, type_name)) in &fm.imported_type_params {
297 if let Some((_, base)) = import_paths.iter().find(|(s, _)| s == stem) {
298 let ty = format_ident!("{}", type_name);
299 out.insert(param.clone(), quote! { #base::#ty });
300 }
301 }
302 out
303}
304
305fn eval_env_expr(expr: &syn::Expr) -> Result<md_tmpl_core::Value, String> {
313 match expr {
314 syn::Expr::Lit(lit) => match &lit.lit {
315 syn::Lit::Str(s) => Ok(md_tmpl_core::Value::Str(s.value())),
316 syn::Lit::Int(i) => {
317 let n: i64 = i
318 .base10_parse()
319 .map_err(|e| format!("invalid integer: {e}"))?;
320 Ok(md_tmpl_core::Value::Int(n))
321 }
322 syn::Lit::Float(f) => {
323 let n: f64 = f
324 .base10_parse()
325 .map_err(|e| format!("invalid float: {e}"))?;
326 Ok(md_tmpl_core::Value::Float(n))
327 }
328 syn::Lit::Bool(b) => Ok(md_tmpl_core::Value::Bool(b.value)),
329 _ => Err("env value must be a string, int, float, or bool literal".to_string()),
330 },
331 syn::Expr::Path(p) => {
334 if p.path.is_ident("true") {
335 Ok(md_tmpl_core::Value::Bool(true))
336 } else if p.path.is_ident("false") {
337 Ok(md_tmpl_core::Value::Bool(false))
338 } else {
339 Err(format!(
340 "env value must be a literal, got path: {}",
341 quote! { #expr }
342 ))
343 }
344 }
345 _ => Err(format!(
346 "env value must be a literal, got: {}",
347 quote! { #expr }
348 )),
349 }
350}
351
352fn err_tokens(span: proc_macro2::Span, rel_path: &str, e: &str) -> TokenStream {
354 let msg = format!("template '{rel_path}': {e}");
355 syn::Error::new(span, msg).to_compile_error().into()
356}
357
358#[proc_macro]
403pub fn include_template(input: TokenStream) -> TokenStream {
404 let parsed = parse_macro_input!(input as IncludeTemplateInput);
405 let rel_path = parsed.path.value();
406
407 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
409 .env
410 .iter()
411 .map(|(k, expr)| {
412 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
413 (k.clone(), val)
414 })
415 .collect();
416 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
417 .iter()
418 .map(|(k, v)| (k.as_str(), v.clone()))
419 .collect();
420
421 let (full_path, ast) = match load_and_compile(&rel_path, &env_refs) {
422 Ok(v) => v,
423 Err(e) => return err_tokens(parsed.path.span(), &rel_path, &e),
424 };
425 let CompiledTemplateAst {
426 frontmatter: fm,
427 segments,
428 inline_templates,
429 source_hash,
430 dependency_paths,
431 } = ast;
432 let path_str = full_path.to_string_lossy().to_string();
433 let dep_paths_str = dep_paths_to_strings(&dependency_paths);
434
435 let mod_ident = match parsed.custom_name {
437 Some(ident) => ident,
438 None => make_ident(&stem_from_path(&rel_path)),
439 };
440
441 let crate_path = parsed
443 .crate_path
444 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
445
446 with_crate_path(crate_path.clone(), || {
447 let segments_tokens = segments.iter().map(codegen_segment);
449 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
450 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
451 let v_tokens = codegen_compiled_inline_template(v);
452 quote! { (#k, #v_tokens) }
453 });
454 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
455 d.default_value.as_ref().map(|v| {
456 let name = &d.name;
457 let val_tokens = codegen_value(v);
458 quote! { (#name, #val_tokens) }
459 })
460 });
461 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
462 let val_tokens = codegen_value(v);
463 quote! { (#k, #val_tokens) }
464 });
465
466 let struct_name = parsed
468 .struct_name
469 .unwrap_or_else(|| format_ident!("Params"));
470 let source = StructGenSource::Module {
471 doc_path: &rel_path,
472 };
473 let imported_type_paths = build_imported_type_paths(&fm, &parsed.import_paths);
474 let struct_tokens =
475 generate_struct_tokens(&fm, &struct_name, &source, &imported_type_paths);
476
477 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
479
480 let name_token = if let Some(n) = &fm.name {
481 quote! { Some(#n) }
482 } else {
483 quote! { None }
484 };
485 let desc_token = if let Some(d) = &fm.description {
486 quote! { Some(#d) }
487 } else {
488 quote! { None }
489 };
490
491 let expanded = quote! {
492 pub mod #mod_ident {
493 const _: &str = include_str!(#path_str);
494 #(const _: &str = include_str!(#dep_paths_str);)*
495
496 fn __init_template() -> #crate_path::Template {
497 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
498 segments: &[#(#segments_tokens),*],
499 declared_variables: &[#(#decls_tokens),*],
500 inline_templates: &[#(#inline_templates_tokens),*],
501 source_hash: #source_hash,
502 consts: &[#(#consts_tokens),*],
503 imported_consts: &[#(#imported_consts_tokens),*],
504 name: #name_token,
505 description: #desc_token,
506 })
507 }
508 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
509 #crate_path::__private::LazyLock::new(__init_template);
510
511 pub fn template() -> &'static #crate_path::Template {
513 &*__TEMPLATE
514 }
515
516 #struct_tokens
517 #(#type_alias_tokens)*
518 }
519 };
520 expanded.into()
521 })
522}
523
524#[proc_macro]
559pub fn template(input: TokenStream) -> TokenStream {
560 let parsed = parse_macro_input!(input as InlineTemplateInput);
561 let source = parsed.source.value();
562 let mod_ident = parsed.name;
563
564 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
565 let base_dir = std::path::Path::new(&manifest_dir);
566
567 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
569 .env
570 .iter()
571 .map(|(k, expr)| {
572 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
573 (k.clone(), val)
574 })
575 .collect();
576 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
577 .iter()
578 .map(|(k, v)| (k.as_str(), v.clone()))
579 .collect();
580
581 let ast = match compile::compile_template_to_ast(&source, base_dir, &env_refs) {
582 Ok(v) => v,
583 Err(e) => {
584 let msg = format!("inline template: {e}");
585 return syn::Error::new(parsed.source.span(), msg)
586 .to_compile_error()
587 .into();
588 }
589 };
590 let CompiledTemplateAst {
591 frontmatter: fm,
592 segments,
593 inline_templates,
594 source_hash,
595 dependency_paths,
596 } = ast;
597 let dep_paths_str = dep_paths_to_strings(&dependency_paths);
598
599 let crate_path = parsed
601 .crate_path
602 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
603
604 with_crate_path(crate_path.clone(), || {
605 let segments_tokens = segments.iter().map(codegen_segment);
607 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
608 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
609 let v_tokens = codegen_compiled_inline_template(v);
610 quote! { (#k, #v_tokens) }
611 });
612 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
613 d.default_value.as_ref().map(|v| {
614 let name = &d.name;
615 let val_tokens = codegen_value(v);
616 quote! { (#name, #val_tokens) }
617 })
618 });
619 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
620 let val_tokens = codegen_value(v);
621 quote! { (#k, #val_tokens) }
622 });
623
624 let struct_name = parsed
626 .struct_name
627 .clone()
628 .unwrap_or_else(|| format_ident!("Params"));
629 let source = StructGenSource::Module {
630 doc_path: "<inline>",
631 };
632 let imported_type_paths = build_imported_type_paths(&fm, &parsed.import_paths);
633 let struct_tokens =
634 generate_struct_tokens(&fm, &struct_name, &source, &imported_type_paths);
635
636 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
638
639 let name_token = fm
640 .name
641 .as_ref()
642 .map_or_else(|| quote! { None }, |n| quote! { Some(#n) });
643 let desc_token = fm
644 .description
645 .as_ref()
646 .map_or_else(|| quote! { None }, |d| quote! { Some(#d) });
647
648 let expanded = quote! {
649 pub mod #mod_ident {
650 #(const _: &str = include_str!(#dep_paths_str);)*
651 fn __init_template() -> #crate_path::Template {
652 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
653 segments: &[#(#segments_tokens),*],
654 declared_variables: &[#(#decls_tokens),*],
655 inline_templates: &[#(#inline_templates_tokens),*],
656 source_hash: #source_hash,
657 consts: &[#(#consts_tokens),*],
658 imported_consts: &[#(#imported_consts_tokens),*],
659 name: #name_token,
660 description: #desc_token,
661 })
662 }
663 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
664 #crate_path::__private::LazyLock::new(__init_template);
665
666 pub fn template() -> &'static #crate_path::Template {
668 &*__TEMPLATE
669 }
670
671 #struct_tokens
672 #(#type_alias_tokens)*
673 }
674 };
675 expanded.into()
676 })
677}