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 fn __init_template() -> #crate_path::Template {
405 #crate_path::Template::from_precompiled(&#crate_path::PrecompiledTemplateData {
406 segments: &[#(#segments_tokens),*],
407 declared_variables: &[#(#decls_tokens),*],
408 inline_templates: &[#(#inline_templates_tokens),*],
409 source_hash: #source_hash,
410 consts: &[#(#consts_tokens),*],
411 imported_consts: &[#(#imported_consts_tokens),*],
412 name: #name_token,
413 description: #desc_token,
414 })
415 }
416 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
417 #crate_path::__private::LazyLock::new(__init_template);
418
419 pub fn template() -> &'static #crate_path::Template {
421 &*__TEMPLATE
422 }
423
424 #struct_tokens
425 #(#type_alias_tokens)*
426 }
427 };
428 expanded.into()
429 })
430}
431
432#[proc_macro]
467pub fn template(input: TokenStream) -> TokenStream {
468 let parsed = parse_macro_input!(input as InlineTemplateInput);
469 let source = parsed.source.value();
470 let mod_ident = parsed.name;
471
472 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
473 let base_dir = std::path::Path::new(&manifest_dir);
474
475 let env_values: Vec<(String, md_tmpl_core::Value)> = parsed
477 .env
478 .iter()
479 .map(|(k, expr)| {
480 let val = eval_env_expr(expr).unwrap_or_else(|e| panic!("env '{k}': {e}"));
481 (k.clone(), val)
482 })
483 .collect();
484 let env_refs: Vec<(&str, md_tmpl_core::Value)> = env_values
485 .iter()
486 .map(|(k, v)| (k.as_str(), v.clone()))
487 .collect();
488
489 let ast = match compile::compile_template_to_ast(&source, base_dir, &env_refs) {
490 Ok(v) => v,
491 Err(e) => {
492 let msg = format!("inline template: {e}");
493 return syn::Error::new(parsed.source.span(), msg)
494 .to_compile_error()
495 .into();
496 }
497 };
498 let CompiledTemplateAst {
499 frontmatter: fm,
500 segments,
501 inline_templates,
502 source_hash,
503 } = ast;
504
505 let crate_path = parsed
507 .crate_path
508 .map_or_else(|| quote! { ::md_tmpl }, |p| quote! { #p });
509
510 with_crate_path(crate_path.clone(), || {
511 let segments_tokens = segments.iter().map(codegen_segment);
513 let decls_tokens = fm.declarations.iter().map(codegen_var_decl);
514 let inline_templates_tokens = inline_templates.iter().map(|(k, v)| {
515 let v_tokens = codegen_compiled_inline_template(v);
516 quote! { (#k, #v_tokens) }
517 });
518 let consts_tokens = fm.consts.iter().chain(fm.env.iter()).filter_map(|d| {
519 d.default_value.as_ref().map(|v| {
520 let name = &d.name;
521 let val_tokens = codegen_value(v);
522 quote! { (#name, #val_tokens) }
523 })
524 });
525 let imported_consts_tokens = fm.imported_consts.iter().map(|(k, v)| {
526 let val_tokens = codegen_value(v);
527 quote! { (#k, #val_tokens) }
528 });
529
530 let struct_name = parsed
532 .struct_name
533 .clone()
534 .unwrap_or_else(|| format_ident!("Params"));
535 let source = StructGenSource::Module {
536 doc_path: "<inline>",
537 };
538 let struct_tokens = generate_struct_tokens(&fm, &struct_name, &source);
539
540 let type_alias_tokens = generate_type_alias_tokens(&fm.type_aliases);
542
543 let name_token = if let Some(n) = &fm.name {
544 quote! { Some(#n) }
545 } else {
546 quote! { None }
547 };
548 let desc_token = if let Some(d) = &fm.description {
549 quote! { Some(#d) }
550 } else {
551 quote! { None }
552 };
553
554 let expanded = quote! {
555 pub mod #mod_ident {
556 fn __init_template() -> #crate_path::Template {
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 static __TEMPLATE: #crate_path::__private::LazyLock<#crate_path::Template> =
569 #crate_path::__private::LazyLock::new(__init_template);
570
571 pub fn template() -> &'static #crate_path::Template {
573 &*__TEMPLATE
574 }
575
576 #struct_tokens
577 #(#type_alias_tokens)*
578 }
579 };
580 expanded.into()
581 })
582}