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}
61
62impl Parse for IncludeTemplateInput {
63 fn parse(input: ParseStream) -> syn::Result<Self> {
64 let path: LitStr = input.parse()?;
65
66 let struct_name = if input.peek(Token![as]) {
68 let _as: Token![as] = input.parse()?;
69 Some(input.parse()?)
70 } else {
71 None
72 };
73
74 let custom_name = if input.peek(Token![=>]) {
76 let _arrow: Token![=>] = input.parse()?;
77 Some(input.parse()?)
78 } else {
79 None
80 };
81
82 let mut crate_path = None;
84 let mut env = Vec::new();
85 while input.peek(Token![,]) {
86 let _comma: Token![,] = input.parse()?;
87 if input.is_empty() {
88 break;
89 }
90 if input.peek(Token![crate]) {
91 let _kw: Token![crate] = input.parse()?;
92 let _eq: Token![=] = input.parse()?;
93 crate_path = Some(input.parse()?);
94 } else {
95 let kw: Ident = input.parse()?;
96 if kw == "env" {
97 let _eq: Token![=] = input.parse()?;
98 env = parse_env_block(input)?;
99 } else {
100 return Err(syn::Error::new(
101 kw.span(),
102 format!("unknown option '{kw}', expected 'crate' or 'env'"),
103 ));
104 }
105 }
106 }
107
108 Ok(Self {
109 path,
110 struct_name,
111 custom_name,
112 crate_path,
113 env,
114 })
115 }
116}
117
118struct InlineTemplateInput {
123 source: LitStr,
124 struct_name: Option<Ident>,
125 name: Ident,
126 crate_path: Option<syn::Path>,
127 env: Vec<(String, syn::Expr)>,
128}
129
130impl Parse for InlineTemplateInput {
131 fn parse(input: ParseStream) -> syn::Result<Self> {
132 let source: LitStr = input.parse()?;
133 let struct_name = if input.peek(Token![as]) {
134 let _as: Token![as] = input.parse()?;
135 Some(input.parse()?)
136 } else {
137 None
138 };
139 let _: Token![=>] = input.parse()?;
140 let name: Ident = input.parse()?;
141 let mut crate_path = None;
142 let mut env = Vec::new();
143 while input.peek(Token![,]) {
144 let _comma: Token![,] = input.parse()?;
145 if input.is_empty() {
146 break;
147 }
148 if input.peek(Token![crate]) {
149 let _kw: Token![crate] = input.parse()?;
150 let _eq: Token![=] = input.parse()?;
151 crate_path = Some(input.parse()?);
152 } else {
153 let kw: Ident = input.parse()?;
154 if kw == "env" {
155 let _eq: Token![=] = input.parse()?;
156 env = parse_env_block(input)?;
157 } else {
158 return Err(syn::Error::new(
159 kw.span(),
160 format!("unknown option '{kw}', expected 'crate' or 'env'"),
161 ));
162 }
163 }
164 }
165 Ok(Self {
166 source,
167 struct_name,
168 name,
169 crate_path,
170 env,
171 })
172 }
173}
174
175const RUST_KEYWORDS: &[&str] = &[
178 "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
180 "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
181 "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
182 "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized",
184 "virtual", "yield", "try", "union",
186];
187
188fn make_module_ident(stem: &str) -> Ident {
191 if RUST_KEYWORDS.contains(&stem) {
192 format_ident!("r#{}", stem)
193 } else {
194 Ident::new(stem, proc_macro2::Span::call_site())
195 }
196}
197
198fn parse_env_block(input: ParseStream) -> syn::Result<Vec<(String, syn::Expr)>> {
203 let content;
204 syn::braced!(content in input);
205 let mut entries = Vec::new();
206 while !content.is_empty() {
207 let key: Ident = content.parse()?;
208 let _colon: Token![:] = content.parse()?;
209 let expr: syn::Expr = content.parse()?;
210 entries.push((key.to_string(), expr));
211 if content.peek(Token![,]) {
212 let _comma: Token![,] = content.parse()?;
213 }
214 }
215 Ok(entries)
216}
217
218fn eval_env_expr(expr: &syn::Expr) -> Result<md_tmpl_core::Value, String> {
227 match expr {
228 syn::Expr::Lit(lit) => match &lit.lit {
229 syn::Lit::Str(s) => Ok(md_tmpl_core::Value::Str(s.value())),
230 syn::Lit::Int(i) => {
231 let n: i64 = i
232 .base10_parse()
233 .map_err(|e| format!("invalid integer: {e}"))?;
234 Ok(md_tmpl_core::Value::Int(n))
235 }
236 syn::Lit::Float(f) => {
237 let n: f64 = f
238 .base10_parse()
239 .map_err(|e| format!("invalid float: {e}"))?;
240 Ok(md_tmpl_core::Value::Float(n))
241 }
242 syn::Lit::Bool(b) => Ok(md_tmpl_core::Value::Bool(b.value)),
243 _ => Err(
244 "env value must be a string, int, float, bool literal, or env!(\"VAR\")"
245 .to_string(),
246 ),
247 },
248 syn::Expr::Macro(m) => {
249 let path = &m.mac.path;
251 let is_env = path.is_ident("env");
252 if !is_env {
253 return Err(format!(
254 "only literals and env!() are supported in macro env values, got: {}",
255 quote! { #expr }
256 ));
257 }
258 let tokens = &m.mac.tokens;
259 let var_name: LitStr = syn::parse2(tokens.clone())
260 .map_err(|e| format!("env!() argument must be a string literal: {e}"))?;
261 let val = std::env::var(var_name.value())
262 .map_err(|e| format!("env!(\"{}\") at compile time: {e}", var_name.value()))?;
263 Ok(md_tmpl_core::Value::Str(val))
264 }
265 syn::Expr::Path(p) => {
268 if p.path.is_ident("true") {
269 Ok(md_tmpl_core::Value::Bool(true))
270 } else if p.path.is_ident("false") {
271 Ok(md_tmpl_core::Value::Bool(false))
272 } else {
273 Err(format!(
274 "env value must be a literal or env!(\"VAR\"), got path: {}",
275 quote! { #expr }
276 ))
277 }
278 }
279 _ => Err(format!(
280 "env value must be a literal or env!(\"VAR\"), got: {}",
281 quote! { #expr }
282 )),
283 }
284}
285
286fn err_tokens(span: proc_macro2::Span, rel_path: &str, e: &str) -> TokenStream {
288 let msg = format!("template '{rel_path}': {e}");
289 syn::Error::new(span, msg).to_compile_error().into()
290}
291
292#[proc_macro]
338pub fn include_template(input: TokenStream) -> TokenStream {
339 let parsed = parse_macro_input!(input as IncludeTemplateInput);
340 let rel_path = parsed.path.value();
341
342 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
344 .env
345 .iter()
346 .map(|(k, expr)| {
347 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
348 (k.clone(), val)
349 })
350 .collect();
351 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
352 .iter()
353 .map(|(k, v)| (k.as_str(), v.clone()))
354 .collect();
355
356 let (full_path, ast) = match load_and_compile(&rel_path, &env_refs) {
357 Ok(v) => v,
358 Err(e) => return err_tokens(parsed.path.span(), &rel_path, &e),
359 };
360 let CompiledTemplateAst {
361 frontmatter: fm,
362 segments,
363 inline_templates,
364 source_hash,
365 } = ast;
366 let path_str = full_path.to_string_lossy().to_string();
367
368 let mod_ident = match parsed.custom_name {
370 Some(ident) => ident,
371 None => make_module_ident(&stem_from_path(&rel_path)),
372 };
373
374 let crate_path = parsed
376 .crate_path
377 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
378
379 with_crate_path(crate_path.clone(), || {
380 let segments_tokens = segments.iter().map(codegen_segment);
382 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
383 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
384 let v_tokens = codegen_compiled_inline_template(v);
385 quote! { (#k, #v_tokens) }
386 });
387 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
388 d.default_value.as_ref().map(|v| {
389 let name = &d.name;
390 let val_tokens = codegen_value(v);
391 quote! { (#name, #val_tokens) }
392 })
393 });
394 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
395 let val_tokens = codegen_value(v);
396 quote! { (#k, #val_tokens) }
397 });
398
399 let struct_name = parsed
401 .struct_name
402 .unwrap_or_else(|| format_ident!("Params"));
403 let source = StructGenSource::Module {
404 doc_path: &rel_path,
405 };
406 let struct_tokens = generate_struct_tokens(&fm, &struct_name, &source);
407
408 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
410
411 let name_token = if let Some(n) = &fm.name {
412 quote! { Some(#n) }
413 } else {
414 quote! { None }
415 };
416 let desc_token = if let Some(d) = &fm.description {
417 quote! { Some(#d) }
418 } else {
419 quote! { None }
420 };
421
422 let expanded = quote! {
423 pub mod #mod_ident {
424 const _: &str = include_str!(#path_str);
425
426 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
427 #crate_path::__private::LazyLock::new(|| {
428 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
429 segments: &[#(#segments_tokens),*],
430 declared_variables: &[#(#decls_tokens),*],
431 inline_templates: &[#(#inline_templates_tokens),*],
432 source_hash: #source_hash,
433 consts: &[#(#consts_tokens),*],
434 imported_consts: &[#(#imported_consts_tokens),*],
435 name: #name_token,
436 description: #desc_token,
437 })
438 });
439
440 pub fn template() -> &'static #crate_path::Template {
442 &*__TEMPLATE
443 }
444
445 #struct_tokens
446 #(#type_alias_tokens)*
447 }
448 };
449 expanded.into()
450 })
451}
452
453#[proc_macro]
489pub fn template(input: TokenStream) -> TokenStream {
490 let parsed = parse_macro_input!(input as InlineTemplateInput);
491 let source = parsed.source.value();
492 let mod_ident = parsed.name;
493
494 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
495 let base_dir = std::path::Path::new(&manifest_dir);
496
497 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
499 .env
500 .iter()
501 .map(|(k, expr)| {
502 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
503 (k.clone(), val)
504 })
505 .collect();
506 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
507 .iter()
508 .map(|(k, v)| (k.as_str(), v.clone()))
509 .collect();
510
511 let ast = match compile::compile_template_to_ast(&source, base_dir, &env_refs) {
512 Ok(v) => v,
513 Err(e) => {
514 let msg = format!("inline template: {e}");
515 return syn::Error::new(parsed.source.span(), msg)
516 .to_compile_error()
517 .into();
518 }
519 };
520 let CompiledTemplateAst {
521 frontmatter: fm,
522 segments,
523 inline_templates,
524 source_hash,
525 } = ast;
526
527 let crate_path = parsed
529 .crate_path
530 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
531
532 with_crate_path(crate_path.clone(), || {
533 let segments_tokens = segments.iter().map(codegen_segment);
535 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
536 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
537 let v_tokens = codegen_compiled_inline_template(v);
538 quote! { (#k, #v_tokens) }
539 });
540 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
541 d.default_value.as_ref().map(|v| {
542 let name = &d.name;
543 let val_tokens = codegen_value(v);
544 quote! { (#name, #val_tokens) }
545 })
546 });
547 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
548 let val_tokens = codegen_value(v);
549 quote! { (#k, #val_tokens) }
550 });
551
552 let struct_name = parsed
554 .struct_name
555 .clone()
556 .unwrap_or_else(|| format_ident!("Params"));
557 let source = StructGenSource::Module {
558 doc_path: "<inline>",
559 };
560 let struct_tokens = generate_struct_tokens(&fm, &struct_name, &source);
561
562 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
564
565 let name_token = if let Some(n) = &fm.name {
566 quote! { Some(#n) }
567 } else {
568 quote! { None }
569 };
570 let desc_token = if let Some(d) = &fm.description {
571 quote! { Some(#d) }
572 } else {
573 quote! { None }
574 };
575
576 let expanded = quote! {
577 pub mod #mod_ident {
578 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
579 #crate_path::__private::LazyLock::new(|| {
580 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
581 segments: &[#(#segments_tokens),*],
582 declared_variables: &[#(#decls_tokens),*],
583 inline_templates: &[#(#inline_templates_tokens),*],
584 source_hash: #source_hash,
585 consts: &[#(#consts_tokens),*],
586 imported_consts: &[#(#imported_consts_tokens),*],
587 name: #name_token,
588 description: #desc_token,
589 })
590 });
591
592 pub fn template() -> &'static #crate_path::Template {
594 &*__TEMPLATE
595 }
596
597 #struct_tokens
598 #(#type_alias_tokens)*
599 }
600 };
601 expanded.into()
602 })
603}