1use alloc::{
2 string::{String, ToString},
3 sync::Arc,
4 vec::Vec,
5};
6#[cfg(feature = "std")]
7use std::path::{Path, PathBuf};
8
9use crate::{
10 compat::{HashMap, HashSet},
11 compiled::{self, CompiledInlineTemplate, Segment},
12 context::Context,
13 error::TemplateError,
14 frontmatter::{self, Frontmatter},
15 types::VarDecl,
16 value::Value,
17};
18
19mod analysis;
20mod render_methods;
21#[cfg(not(feature = "std"))]
22use self::analysis::hash_source_no_std;
23use self::analysis::{
24 check_bare_enum_access, check_internal_key_access, check_name_collisions,
25 check_static_enum_in_conditions, check_undeclared_variables, check_unused_params,
26 collect_enum_type_keys, inject_enum_type_constants,
27};
28
29#[non_exhaustive]
47#[derive(Debug, Clone, Copy, Default)]
48pub struct CompileOptions<'a> {
49 pub allow_unused: bool,
54 #[cfg(feature = "std")]
58 pub base_dir: Option<&'a std::path::Path>,
59 pub env: &'a [(&'a str, crate::Value)],
63 #[cfg(not(feature = "std"))]
65 _phantom: core::marker::PhantomData<&'a ()>,
66}
67
68#[cfg(feature = "std")]
69impl<'a> CompileOptions<'a> {
70 #[must_use]
72 pub fn base_dir(mut self, dir: &'a std::path::Path) -> Self {
73 self.base_dir = Some(dir);
74 self
75 }
76}
77
78impl<'a> CompileOptions<'a> {
79 #[must_use]
81 pub fn allow_unused(mut self, allow: bool) -> Self {
82 self.allow_unused = allow;
83 self
84 }
85
86 #[must_use]
88 pub fn env(mut self, pairs: &'a [(&'a str, crate::Value)]) -> Self {
89 self.env = pairs;
90 self
91 }
92}
93
94pub struct Template {
100 body: String,
102 name: Option<String>,
104 description: Option<String>,
106 segments: Arc<[Segment]>,
108 declared_variables: Arc<[VarDecl]>,
110 #[cfg(feature = "std")]
112 base_dir: Option<PathBuf>,
113 inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
115 source_hash: u64,
116 max_include_depth: usize,
117 has_defaults: bool,
119 consts: Arc<HashMap<String, crate::value::Value>>,
121 imported_consts: Arc<HashMap<String, crate::value::Value>>,
123 estimated_capacity: usize,
125 #[cfg(feature = "std")]
131 checked_type_ids: std::sync::Mutex<Vec<core::any::TypeId>>,
132}
133
134impl core::fmt::Debug for Template {
135 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136 f.debug_struct("Template")
137 .field("body", &self.body)
138 .field("name", &self.name)
139 .field("description", &self.description)
140 .field("segments", &self.segments)
141 .field("declared_variables", &self.declared_variables)
142 .field("source_hash", &self.source_hash)
143 .finish_non_exhaustive()
144 }
145}
146
147impl Clone for Template {
148 fn clone(&self) -> Self {
149 Self {
150 body: self.body.clone(),
151 name: self.name.clone(),
152 description: self.description.clone(),
153 segments: self.segments.clone(),
154 declared_variables: self.declared_variables.clone(),
155 #[cfg(feature = "std")]
156 base_dir: self.base_dir.clone(),
157 inline_templates: self.inline_templates.clone(),
158 source_hash: self.source_hash,
159 max_include_depth: self.max_include_depth,
160 has_defaults: self.has_defaults,
161 consts: self.consts.clone(),
162 imported_consts: self.imported_consts.clone(),
163 estimated_capacity: self.estimated_capacity,
164 #[cfg(feature = "std")]
167 checked_type_ids: std::sync::Mutex::new(
168 self.checked_type_ids
169 .lock()
170 .unwrap_or_else(std::sync::PoisonError::into_inner)
171 .clone(),
172 ),
173 }
174 }
175}
176
177#[cfg(feature = "std")]
181pub(crate) struct CachedTemplateData {
182 pub segments: Arc<[Segment]>,
184 pub declared_variables: Arc<[VarDecl]>,
186 pub base_dir: Option<PathBuf>,
188 pub inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
190 pub source_hash: u64,
192 pub consts: Arc<HashMap<String, crate::value::Value>>,
194 pub imported_consts: Arc<HashMap<String, crate::value::Value>>,
196 pub name: Option<String>,
198 pub description: Option<String>,
200}
201
202#[doc(hidden)]
206pub struct PrecompiledTemplateData<'a> {
207 pub segments: &'a [Segment],
209 pub declared_variables: &'a [VarDecl],
211 pub inline_templates: &'a [(&'a str, CompiledInlineTemplate)],
213 pub source_hash: u64,
215 pub consts: &'a [(&'a str, crate::value::Value)],
217 pub imported_consts: &'a [(&'a str, crate::value::Value)],
219 pub name: Option<&'a str>,
221 pub description: Option<&'a str>,
223}
224
225impl Template {
226 #[cfg(feature = "std")]
232 pub fn from_file(path: &Path) -> Result<Self, TemplateError> {
233 let source = std::fs::read_to_string(path)?;
234 let (tmpl, _fm) =
235 Self::compile_from_source(&source, Some(path.parent().unwrap_or(Path::new("."))))?;
236 Ok(tmpl)
237 }
238
239 pub fn from_source(source: &str) -> Result<Self, TemplateError> {
245 #[cfg(feature = "std")]
246 let (tmpl, _fm) = Self::compile_from_source(source, None)?;
247 #[cfg(not(feature = "std"))]
248 let (tmpl, _fm) = Self::compile_from_source_no_std(source)?;
249 Ok(tmpl)
250 }
251
252 pub fn compile(
277 source: &str,
278 options: CompileOptions<'_>,
279 ) -> Result<(Self, Frontmatter), TemplateError> {
280 #[cfg(feature = "std")]
281 return Self::compile_inner(source, options.base_dir, options.allow_unused, options.env);
282 #[cfg(not(feature = "std"))]
283 return Self::compile_inner_no_std(source, options.allow_unused, options.env);
284 }
285
286 #[cfg(feature = "std")]
307 pub fn compile_file(
308 path: &Path,
309 options: CompileOptions<'_>,
310 ) -> Result<(Self, Frontmatter), TemplateError> {
311 let source = std::fs::read_to_string(path)?;
312 let base_dir = options.base_dir.or_else(|| path.parent());
313 Self::compile_inner(&source, base_dir, options.allow_unused, options.env)
314 }
315
316 #[cfg(feature = "std")]
318 fn compile_from_source(
319 source: &str,
320 base_dir: Option<&Path>,
321 ) -> Result<(Self, Frontmatter), TemplateError> {
322 Self::compile_inner(source, base_dir, false, &[])
323 }
324
325 #[cfg(feature = "std")]
330 fn compile_inner(
331 source: &str,
332 base_dir: Option<&Path>,
333 force_allow_unused: bool,
334 env_values: &[(&str, Value)],
335 ) -> Result<(Self, Frontmatter), TemplateError> {
336 let source_hash = crate::cache::hash_source(source);
337 let (fm, body) = if let Some(dir) = base_dir {
338 frontmatter::parse_frontmatter_with_base_dir(source, dir, env_values)?
339 } else {
340 frontmatter::parse_frontmatter_with_env(source, env_values)?
341 };
342 let body = body.to_string();
343 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
344
345 let referenced = compiled::collect_referenced_params(&segments);
347 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
348 check_unused_params(
349 &fm.declarations,
350 &referenced,
351 force_allow_unused || fm.allow_unused,
352 )?;
353 check_name_collisions(&fm, &inline_templates, &segments)?;
354 let enum_keys = collect_enum_type_keys(&fm);
355 check_bare_enum_access(&segments, &enum_keys)?;
356 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
357 check_internal_key_access(&segments)?;
358
359 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
360 let mut consts: HashMap<String, Value> = fm
361 .consts
362 .iter()
363 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
364 .collect();
365 for d in &fm.env {
367 if let Some(ref v) = d.default_value {
368 consts.entry(d.name.clone()).or_insert_with(|| v.clone());
369 }
370 }
371 inject_enum_type_constants(&fm.type_aliases, &mut consts);
373 let segments: Arc<[Segment]> = Arc::from(segments);
374 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
375 let tmpl = Self {
376 body,
377 name: fm.name.clone(),
378 description: fm.description.clone(),
379 segments,
380 declared_variables: Arc::from(fm.declarations.clone()),
381 base_dir: base_dir.map(Path::to_path_buf),
382 inline_templates: Arc::new(inline_templates),
383 source_hash,
384 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
385 has_defaults,
386 consts: Arc::new(consts),
387 imported_consts: Arc::new(fm.imported_consts.clone()),
388 estimated_capacity,
389 checked_type_ids: std::sync::Mutex::new(Vec::new()),
390 };
391 Ok((tmpl, fm))
392 }
393
394 #[cfg(not(feature = "std"))]
396 fn compile_from_source_no_std(source: &str) -> Result<(Self, Frontmatter), TemplateError> {
397 Self::compile_inner_no_std(source, false, &[])
398 }
399
400 #[cfg(not(feature = "std"))]
402 fn compile_inner_no_std(
403 source: &str,
404 force_allow_unused: bool,
405 env_values: &[(&str, Value)],
406 ) -> Result<(Self, Frontmatter), TemplateError> {
407 let source_hash = hash_source_no_std(source);
408 let (fm, body) = frontmatter::parse_frontmatter_with_env(source, env_values)?;
409 let body = body.to_string();
410 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
411
412 let referenced = compiled::collect_referenced_params(&segments);
413 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
414 check_unused_params(
415 &fm.declarations,
416 &referenced,
417 force_allow_unused || fm.allow_unused,
418 )?;
419 check_name_collisions(&fm, &inline_templates, &segments)?;
420 let enum_keys = collect_enum_type_keys(&fm);
421 check_bare_enum_access(&segments, &enum_keys)?;
422 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
423 check_internal_key_access(&segments)?;
424
425 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
426 let mut consts: HashMap<String, Value> = fm
427 .consts
428 .iter()
429 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
430 .collect();
431 for d in &fm.env {
433 if let Some(ref v) = d.default_value {
434 consts.entry(d.name.clone()).or_insert_with(|| v.clone());
435 }
436 }
437 inject_enum_type_constants(&fm.type_aliases, &mut consts);
439 let segments: Arc<[Segment]> = Arc::from(segments);
440 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
441 let tmpl = Self {
442 body,
443 name: fm.name.clone(),
444 description: fm.description.clone(),
445 segments,
446 declared_variables: Arc::from(fm.declarations.clone()),
447 inline_templates: Arc::new(inline_templates),
448 source_hash,
449 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
450 has_defaults,
451 consts: Arc::new(consts),
452 imported_consts: Arc::new(fm.imported_consts.clone()),
453 estimated_capacity,
454 };
455 Ok((tmpl, fm))
456 }
457
458 #[cfg(feature = "std")]
465 pub(crate) fn from_cached(data: CachedTemplateData) -> Self {
466 let has_defaults = data
467 .declared_variables
468 .iter()
469 .any(|d| d.default_value.is_some());
470 let estimated_capacity = compiled::render::estimate_output_capacity(&data.segments);
471 Self {
472 body: String::new(),
473 name: data.name,
474 description: data.description,
475 segments: data.segments,
476 declared_variables: data.declared_variables,
477 base_dir: data.base_dir,
478 inline_templates: data.inline_templates,
479 source_hash: data.source_hash,
480 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
481 has_defaults,
482 consts: data.consts,
483 imported_consts: data.imported_consts,
484 estimated_capacity,
485 checked_type_ids: std::sync::Mutex::new(Vec::new()),
486 }
487 }
488
489 #[doc(hidden)]
491 #[must_use]
492 pub fn from_precompiled(data: &PrecompiledTemplateData<'_>) -> Self {
493 let inline_map = data
494 .inline_templates
495 .iter()
496 .map(|(k, v)| (k.to_string(), v.clone()))
497 .collect();
498 let const_map = data
499 .consts
500 .iter()
501 .map(|(k, v)| (k.to_string(), v.clone()))
502 .collect();
503 let imported_const_map = data
504 .imported_consts
505 .iter()
506 .map(|(k, v)| (k.to_string(), v.clone()))
507 .collect();
508 let has_defaults = data
509 .declared_variables
510 .iter()
511 .any(|d| d.default_value.is_some());
512 let segments: Arc<[Segment]> = Arc::from(data.segments);
513 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
514 Self {
515 body: String::new(),
516 name: data.name.map(String::from),
517 description: data.description.map(String::from),
518 segments,
519 declared_variables: Arc::from(data.declared_variables),
520 #[cfg(feature = "std")]
521 base_dir: None,
522 inline_templates: Arc::new(inline_map),
523 source_hash: data.source_hash,
524 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
525 has_defaults,
526 consts: Arc::new(const_map),
527 imported_consts: Arc::new(imported_const_map),
528 estimated_capacity,
529 #[cfg(feature = "std")]
530 checked_type_ids: std::sync::Mutex::new(Vec::new()),
531 }
532 }
533
534 fn validate_context(&self, ctx: &Context, allow_extra: bool) -> Result<(), TemplateError> {
547 let mut missing = Vec::new();
548 let mut mismatch: Option<(String, crate::types::TypeCheckError)> = None;
549 for decl in self.declared_variables.iter() {
550 match ctx.get(&decl.name) {
551 None => {
552 if decl.default_value.is_none() {
554 missing.push(decl.name.as_str());
555 }
556 }
557 Some(value) => {
558 if mismatch.is_none()
559 && let Err(e) = decl.var_type.check(value)
560 {
561 mismatch = Some((decl.name.clone(), e));
562 }
563 }
564 }
565 }
566 if !missing.is_empty() {
568 return Err(TemplateError::MissingParams(
569 missing.into_iter().map(String::from).collect(),
570 ));
571 }
572 if let Some((name, check_err)) = mismatch {
573 let detail = if check_err.path.is_empty() {
574 String::new()
575 } else {
576 format!(" (at .{})", check_err.path)
577 };
578 return Err(TemplateError::TypeMismatch {
579 name: format!("{name}{detail}"),
580 expected: check_err.expected,
581 actual: check_err.actual,
582 actual_value: check_err.actual_value,
583 });
584 }
585 if !allow_extra {
587 let mut declared: HashSet<&str> = self
588 .declared_variables
589 .iter()
590 .map(|d| d.name.as_str())
591 .collect();
592 for name in self.consts.keys() {
593 declared.insert(name.as_str());
594 }
595 let extra: Vec<String> = ctx
596 .values
597 .keys()
598 .filter(|k| !declared.contains(k.as_str()))
599 .cloned()
600 .collect();
601 if !extra.is_empty() {
602 return Err(TemplateError::ExtraParams(extra));
603 }
604 }
605 Ok(())
606 }
607
608 #[must_use]
610 pub fn defaults(&self) -> HashMap<String, crate::value::Value> {
611 self.declared_variables
612 .iter()
613 .filter_map(|d| {
614 d.default_value
615 .as_ref()
616 .map(|v| (d.name.clone(), v.clone()))
617 })
618 .collect()
619 }
620
621 #[must_use]
623 pub fn default(&self, name: &str) -> Option<&crate::value::Value> {
624 self.declared_variables
625 .iter()
626 .find(|d| d.name == name)
627 .and_then(|d| d.default_value.as_ref())
628 }
629
630 #[must_use]
649 pub fn defaults_context(&self) -> Context {
650 let defaults = self.defaults();
651 let mut ctx = Context::with_capacity(defaults.len());
652 for (k, v) in defaults {
653 ctx.set(k, v);
654 }
655 ctx
656 }
657
658 #[must_use]
662 pub fn body(&self) -> &str {
663 &self.body
664 }
665
666 #[must_use]
668 pub fn name(&self) -> Option<&str> {
669 self.name.as_deref()
670 }
671
672 #[must_use]
674 pub fn description(&self) -> Option<&str> {
675 self.description.as_deref()
676 }
677
678 pub fn set_max_include_depth(&mut self, depth: usize) {
680 self.max_include_depth = depth;
681 }
682
683 #[must_use]
685 pub fn with_max_include_depth(mut self, depth: usize) -> Self {
686 self.max_include_depth = depth;
687 self
688 }
689
690 #[must_use]
695 pub fn declarations(&self) -> &[VarDecl] {
696 &self.declared_variables
697 }
698
699 pub(crate) fn segments(&self) -> &[crate::compiled::Segment] {
700 &self.segments
701 }
702
703 #[cfg(feature = "std")]
705 #[must_use]
706 pub fn base_dir(&self) -> Option<&Path> {
707 self.base_dir.as_deref()
708 }
709
710 #[must_use]
734 pub fn consts(&self) -> Arc<HashMap<String, Value>> {
735 self.consts.clone()
736 }
737
738 #[must_use]
741 pub fn consts_ref(&self) -> &HashMap<String, Value> {
742 &self.consts
743 }
744
745 #[must_use]
750 pub fn imported_consts(&self) -> Arc<HashMap<String, Value>> {
751 self.imported_consts.clone()
752 }
753
754 #[must_use]
757 pub fn imported_consts_ref(&self) -> &HashMap<String, Value> {
758 &self.imported_consts
759 }
760
761 pub(crate) fn inline_templates(&self) -> &HashMap<String, CompiledInlineTemplate> {
762 &self.inline_templates
763 }
764
765 #[must_use]
772 pub fn source_hash(&self) -> u64 {
773 self.source_hash
774 }
775
776 pub fn validate_declarations(&self, expected: &[VarDecl]) -> Result<(), TemplateError> {
791 let current: HashMap<&str, &crate::types::VarType> = self
792 .declared_variables
793 .iter()
794 .map(|d| (d.name.as_str(), &d.var_type))
795 .collect();
796 let expected_map: HashMap<&str, &crate::types::VarType> = expected
797 .iter()
798 .map(|d| (d.name.as_str(), &d.var_type))
799 .collect();
800
801 let current_names: HashSet<&str> = current.keys().copied().collect();
802 let expected_names: HashSet<&str> = expected_map.keys().copied().collect();
803
804 let missing: Vec<&str> = expected_names.difference(¤t_names).copied().collect();
805 let extra: Vec<&str> = current_names.difference(&expected_names).copied().collect();
806
807 let retyped: Vec<String> = current_names
809 .intersection(&expected_names)
810 .filter_map(|name| {
811 let cur_type = current[name];
812 let exp_type = expected_map[name];
813 if cur_type == exp_type {
814 None
815 } else {
816 Some(format!("{name}: {exp_type} → {cur_type}"))
817 }
818 })
819 .collect();
820
821 if missing.is_empty() && extra.is_empty() && retyped.is_empty() {
822 return Ok(());
823 }
824
825 let mut parts = Vec::new();
826 if !missing.is_empty() {
827 parts.push(format!("removed: {}", missing.join(", ")));
828 }
829 if !extra.is_empty() {
830 parts.push(format!("added: {}", extra.join(", ")));
831 }
832 if !retyped.is_empty() {
833 parts.push(format!("retyped: {}", retyped.join(", ")));
834 }
835
836 Err(TemplateError::DeclarationsMutated {
837 details: parts.join("; "),
838 })
839 }
840}
841
842impl PartialEq for Template {
855 fn eq(&self, other: &Self) -> bool {
856 self.source_hash == other.source_hash
857 }
858}
859
860impl Eq for Template {}
861
862#[cfg(feature = "serde")]
868impl serde::Serialize for Template {
869 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
870 serializer.serialize_str(&format!("template:{:016x}", self.source_hash))
871 }
872}
873
874#[cfg(feature = "serde")]
880impl<'de> serde::Deserialize<'de> for Template {
881 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
882 let _ = <serde::de::IgnoredAny as serde::Deserialize>::deserialize(deserializer)?;
883 Err(serde::de::Error::custom(
884 "Template cannot be deserialized; construct from source with \
885 Template::from_source() or Template::from_file()",
886 ))
887 }
888}
889
890#[cfg(feature = "std")]
898pub fn load_template(dir: &Path, name: &str) -> Result<Template, TemplateError> {
899 let path = dir.join(format!("{name}.tmpl.md"));
900 Template::from_file(&path)
901}
902
903#[cfg(all(test, feature = "std"))]
904mod adversarial_tests;
905#[cfg(all(test, feature = "std"))]
906mod collision_and_scope_tests;
907#[cfg(all(test, feature = "std"))]
908mod const_tests;
909#[cfg(all(test, feature = "std"))]
910mod error_diagnostic_tests;
911#[cfg(all(test, feature = "std"))]
912mod higher_order_tests;
913#[cfg(all(test, feature = "std"))]
914mod inline_edge_tests;
915#[cfg(all(test, feature = "std"))]
916mod render_integration_tests;
917#[cfg(all(test, feature = "std"))]
918mod shared_tests;
919#[cfg(all(test, feature = "std"))]
920mod tests;