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> {
226 match expr {
227 syn::Expr::Lit(lit) => match &lit.lit {
228 syn::Lit::Str(s) => Ok(md_tmpl_core::Value::Str(s.value())),
229 syn::Lit::Int(i) => {
230 let n: i64 = i
231 .base10_parse()
232 .map_err(|e| format!("invalid integer: {e}"))?;
233 Ok(md_tmpl_core::Value::Int(n))
234 }
235 syn::Lit::Float(f) => {
236 let n: f64 = f
237 .base10_parse()
238 .map_err(|e| format!("invalid float: {e}"))?;
239 Ok(md_tmpl_core::Value::Float(n))
240 }
241 syn::Lit::Bool(b) => Ok(md_tmpl_core::Value::Bool(b.value)),
242 _ => Err("env value must be a string, int, float, or bool literal".to_string()),
243 },
244 syn::Expr::Path(p) => {
247 if p.path.is_ident("true") {
248 Ok(md_tmpl_core::Value::Bool(true))
249 } else if p.path.is_ident("false") {
250 Ok(md_tmpl_core::Value::Bool(false))
251 } else {
252 Err(format!(
253 "env value must be a literal, got path: {}",
254 quote! { #expr }
255 ))
256 }
257 }
258 _ => Err(format!(
259 "env value must be a literal, got: {}",
260 quote! { #expr }
261 )),
262 }
263}
264
265fn err_tokens(span: proc_macro2::Span, rel_path: &str, e: &str) -> TokenStream {
267 let msg = format!("template '{rel_path}': {e}");
268 syn::Error::new(span, msg).to_compile_error().into()
269}
270
271#[proc_macro]
316pub fn include_template(input: TokenStream) -> TokenStream {
317 let parsed = parse_macro_input!(input as IncludeTemplateInput);
318 let rel_path = parsed.path.value();
319
320 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
322 .env
323 .iter()
324 .map(|(k, expr)| {
325 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
326 (k.clone(), val)
327 })
328 .collect();
329 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
330 .iter()
331 .map(|(k, v)| (k.as_str(), v.clone()))
332 .collect();
333
334 let (full_path, ast) = match load_and_compile(&rel_path, &env_refs) {
335 Ok(v) => v,
336 Err(e) => return err_tokens(parsed.path.span(), &rel_path, &e),
337 };
338 let CompiledTemplateAst {
339 frontmatter: fm,
340 segments,
341 inline_templates,
342 source_hash,
343 } = ast;
344 let path_str = full_path.to_string_lossy().to_string();
345
346 let mod_ident = match parsed.custom_name {
348 Some(ident) => ident,
349 None => make_module_ident(&stem_from_path(&rel_path)),
350 };
351
352 let crate_path = parsed
354 .crate_path
355 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
356
357 with_crate_path(crate_path.clone(), || {
358 let segments_tokens = segments.iter().map(codegen_segment);
360 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
361 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
362 let v_tokens = codegen_compiled_inline_template(v);
363 quote! { (#k, #v_tokens) }
364 });
365 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
366 d.default_value.as_ref().map(|v| {
367 let name = &d.name;
368 let val_tokens = codegen_value(v);
369 quote! { (#name, #val_tokens) }
370 })
371 });
372 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
373 let val_tokens = codegen_value(v);
374 quote! { (#k, #val_tokens) }
375 });
376
377 let struct_name = parsed
379 .struct_name
380 .unwrap_or_else(|| format_ident!("Params"));
381 let source = StructGenSource::Module {
382 doc_path: &rel_path,
383 };
384 let struct_tokens = generate_struct_tokens(&fm, &struct_name, &source);
385
386 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
388
389 let name_token = if let Some(n) = &fm.name {
390 quote! { Some(#n) }
391 } else {
392 quote! { None }
393 };
394 let desc_token = if let Some(d) = &fm.description {
395 quote! { Some(#d) }
396 } else {
397 quote! { None }
398 };
399
400 let expanded = quote! {
401 pub mod #mod_ident {
402 const _: &str = include_str!(#path_str);
403
404 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
405 #crate_path::__private::LazyLock::new(|| {
406 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
407 segments: &[#(#segments_tokens),*],
408 declared_variables: &[#(#decls_tokens),*],
409 inline_templates: &[#(#inline_templates_tokens),*],
410 source_hash: #source_hash,
411 consts: &[#(#consts_tokens),*],
412 imported_consts: &[#(#imported_consts_tokens),*],
413 name: #name_token,
414 description: #desc_token,
415 })
416 });
417
418 pub fn template() -> &'static #crate_path::Template {
420 &*__TEMPLATE
421 }
422
423 #struct_tokens
424 #(#type_alias_tokens)*
425 }
426 };
427 expanded.into()
428 })
429}
430
431#[proc_macro]
466pub fn template(input: TokenStream) -> TokenStream {
467 let parsed = parse_macro_input!(input as InlineTemplateInput);
468 let source = parsed.source.value();
469 let mod_ident = parsed.name;
470
471 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
472 let base_dir = std::path::Path::new(&manifest_dir);
473
474 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
476 .env
477 .iter()
478 .map(|(k, expr)| {
479 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
480 (k.clone(), val)
481 })
482 .collect();
483 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
484 .iter()
485 .map(|(k, v)| (k.as_str(), v.clone()))
486 .collect();
487
488 let ast = match compile::compile_template_to_ast(&source, base_dir, &env_refs) {
489 Ok(v) => v,
490 Err(e) => {
491 let msg = format!("inline template: {e}");
492 return syn::Error::new(parsed.source.span(), msg)
493 .to_compile_error()
494 .into();
495 }
496 };
497 let CompiledTemplateAst {
498 frontmatter: fm,
499 segments,
500 inline_templates,
501 source_hash,
502 } = ast;
503
504 let crate_path = parsed
506 .crate_path
507 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
508
509 with_crate_path(crate_path.clone(), || {
510 let segments_tokens = segments.iter().map(codegen_segment);
512 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
513 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
514 let v_tokens = codegen_compiled_inline_template(v);
515 quote! { (#k, #v_tokens) }
516 });
517 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
518 d.default_value.as_ref().map(|v| {
519 let name = &d.name;
520 let val_tokens = codegen_value(v);
521 quote! { (#name, #val_tokens) }
522 })
523 });
524 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
525 let val_tokens = codegen_value(v);
526 quote! { (#k, #val_tokens) }
527 });
528
529 let struct_name = parsed
531 .struct_name
532 .clone()
533 .unwrap_or_else(|| format_ident!("Params"));
534 let source = StructGenSource::Module {
535 doc_path: "<inline>",
536 };
537 let struct_tokens = generate_struct_tokens(&fm, &struct_name, &source);
538
539 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
541
542 let name_token = if let Some(n) = &fm.name {
543 quote! { Some(#n) }
544 } else {
545 quote! { None }
546 };
547 let desc_token = if let Some(d) = &fm.description {
548 quote! { Some(#d) }
549 } else {
550 quote! { None }
551 };
552
553 let expanded = quote! {
554 pub mod #mod_ident {
555 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
556 #crate_path::__private::LazyLock::new(|| {
557 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
558 segments: &[#(#segments_tokens),*],
559 declared_variables: &[#(#decls_tokens),*],
560 inline_templates: &[#(#inline_templates_tokens),*],
561 source_hash: #source_hash,
562 consts: &[#(#consts_tokens),*],
563 imported_consts: &[#(#imported_consts_tokens),*],
564 name: #name_token,
565 description: #desc_token,
566 })
567 });
568
569 pub fn template() -> &'static #crate_path::Template {
571 &*__TEMPLATE
572 }
573
574 #struct_tokens
575 #(#type_alias_tokens)*
576 }
577 };
578 expanded.into()
579 })
580}