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
19pub(crate) mod 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")]
128 env_values: alloc::sync::Arc<[(String, Value)]>,
129 #[cfg(feature = "std")]
135 checked_type_ids: std::sync::Mutex<Vec<core::any::TypeId>>,
136}
137
138impl core::fmt::Debug for Template {
139 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
140 f.debug_struct("Template")
141 .field("body", &self.body)
142 .field("name", &self.name)
143 .field("description", &self.description)
144 .field("segments", &self.segments)
145 .field("declared_variables", &self.declared_variables)
146 .field("source_hash", &self.source_hash)
147 .finish_non_exhaustive()
148 }
149}
150
151impl Clone for Template {
152 fn clone(&self) -> Self {
153 Self {
154 body: self.body.clone(),
155 name: self.name.clone(),
156 description: self.description.clone(),
157 segments: self.segments.clone(),
158 declared_variables: self.declared_variables.clone(),
159 #[cfg(feature = "std")]
160 base_dir: self.base_dir.clone(),
161 inline_templates: self.inline_templates.clone(),
162 source_hash: self.source_hash,
163 max_include_depth: self.max_include_depth,
164 has_defaults: self.has_defaults,
165 consts: self.consts.clone(),
166 imported_consts: self.imported_consts.clone(),
167 estimated_capacity: self.estimated_capacity,
168 #[cfg(feature = "std")]
169 env_values: self.env_values.clone(),
170 #[cfg(feature = "std")]
173 checked_type_ids: std::sync::Mutex::new(
174 self.checked_type_ids
175 .lock()
176 .unwrap_or_else(std::sync::PoisonError::into_inner)
177 .clone(),
178 ),
179 }
180 }
181}
182
183#[cfg(feature = "std")]
187pub(crate) struct CachedTemplateData {
188 pub segments: Arc<[Segment]>,
190 pub declared_variables: Arc<[VarDecl]>,
192 pub base_dir: Option<PathBuf>,
194 pub inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
196 pub source_hash: u64,
198 pub consts: Arc<HashMap<String, crate::value::Value>>,
200 pub imported_consts: Arc<HashMap<String, crate::value::Value>>,
202 pub name: Option<String>,
204 pub description: Option<String>,
206}
207
208#[doc(hidden)]
212pub struct PrecompiledTemplateData<'a> {
213 pub segments: &'a [Segment],
215 pub declared_variables: &'a [VarDecl],
217 pub inline_templates: &'a [(&'a str, CompiledInlineTemplate)],
219 pub source_hash: u64,
221 pub consts: &'a [(&'a str, crate::value::Value)],
223 pub imported_consts: &'a [(&'a str, crate::value::Value)],
225 pub name: Option<&'a str>,
227 pub description: Option<&'a str>,
229}
230
231impl Template {
232 #[cfg(feature = "std")]
238 pub fn from_file(path: &Path) -> Result<Self, TemplateError> {
239 let source = std::fs::read_to_string(path)?;
240 let (tmpl, _fm) =
241 Self::compile_from_source(&source, Some(path.parent().unwrap_or(Path::new("."))))?;
242 Ok(tmpl)
243 }
244
245 pub fn from_source(source: &str) -> Result<Self, TemplateError> {
251 #[cfg(feature = "std")]
252 let (tmpl, _fm) = Self::compile_from_source(source, None)?;
253 #[cfg(not(feature = "std"))]
254 let (tmpl, _fm) = Self::compile_from_source_no_std(source)?;
255 Ok(tmpl)
256 }
257
258 pub fn compile(
283 source: &str,
284 options: CompileOptions<'_>,
285 ) -> Result<(Self, Frontmatter), TemplateError> {
286 #[cfg(feature = "std")]
287 return Self::compile_inner(source, options.base_dir, options.allow_unused, options.env);
288 #[cfg(not(feature = "std"))]
289 return Self::compile_inner_no_std(source, options.allow_unused, options.env);
290 }
291
292 #[cfg(feature = "std")]
313 pub fn compile_file(
314 path: &Path,
315 options: CompileOptions<'_>,
316 ) -> Result<(Self, Frontmatter), TemplateError> {
317 let source = std::fs::read_to_string(path)?;
318 let base_dir = options.base_dir.or_else(|| path.parent());
319 Self::compile_inner(&source, base_dir, options.allow_unused, options.env)
320 }
321
322 #[cfg(feature = "std")]
324 fn compile_from_source(
325 source: &str,
326 base_dir: Option<&Path>,
327 ) -> Result<(Self, Frontmatter), TemplateError> {
328 Self::compile_inner(source, base_dir, false, &[])
329 }
330
331 #[cfg(feature = "std")]
336 fn compile_inner(
337 source: &str,
338 base_dir: Option<&Path>,
339 force_allow_unused: bool,
340 env_values: &[(&str, Value)],
341 ) -> Result<(Self, Frontmatter), TemplateError> {
342 let source_hash = crate::cache::hash_source(source);
343 let (fm, body) = if let Some(dir) = base_dir {
344 frontmatter::parse_frontmatter_with_base_dir(source, dir, env_values)?
345 } else {
346 frontmatter::parse_frontmatter_with_env(source, env_values)?
347 };
348 let body = body.to_string();
349 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
350
351 let referenced = compiled::collect_referenced_params(&segments);
353 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
354 check_unused_params(
355 &fm.declarations,
356 &referenced,
357 force_allow_unused || fm.allow_unused,
358 )?;
359 check_name_collisions(&fm, &inline_templates, &segments)?;
360 let enum_keys = collect_enum_type_keys(&fm);
361 check_bare_enum_access(&segments, &enum_keys)?;
362 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
363 check_internal_key_access(&segments)?;
364 let label_errors =
366 compiled::validate_match_labels(&segments, &fm.declarations, &fm.type_aliases);
367 if !label_errors.is_empty() {
368 return Err(TemplateError::Syntax(label_errors.join("; ").into()));
369 }
370
371 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
372 let mut consts: HashMap<String, Value> = fm
373 .consts
374 .iter()
375 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
376 .collect();
377 for d in &fm.env {
379 if let Some(ref v) = d.default_value {
380 consts.entry(d.name.clone()).or_insert_with(|| v.clone());
381 }
382 }
383 inject_enum_type_constants(&fm.type_aliases, &mut consts);
385 let segments: Arc<[Segment]> = Arc::from(segments);
386 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
387 let env_values: alloc::sync::Arc<[(String, Value)]> = env_values
388 .iter()
389 .map(|(k, v)| (k.to_string(), v.clone()))
390 .collect();
391 let tmpl = Self {
392 body,
393 name: fm.name.clone(),
394 description: fm.description.clone(),
395 segments,
396 declared_variables: Arc::from(fm.declarations.clone()),
397 base_dir: base_dir.map(Path::to_path_buf),
398 inline_templates: Arc::new(inline_templates),
399 source_hash,
400 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
401 has_defaults,
402 consts: Arc::new(consts),
403 imported_consts: Arc::new(fm.imported_consts.clone()),
404 estimated_capacity,
405 env_values,
406 checked_type_ids: std::sync::Mutex::new(Vec::new()),
407 };
408 Ok((tmpl, fm))
409 }
410
411 #[cfg(not(feature = "std"))]
413 fn compile_from_source_no_std(source: &str) -> Result<(Self, Frontmatter), TemplateError> {
414 Self::compile_inner_no_std(source, false, &[])
415 }
416
417 #[cfg(not(feature = "std"))]
419 fn compile_inner_no_std(
420 source: &str,
421 force_allow_unused: bool,
422 env_values: &[(&str, Value)],
423 ) -> Result<(Self, Frontmatter), TemplateError> {
424 let source_hash = hash_source_no_std(source);
425 let (fm, body) = frontmatter::parse_frontmatter_with_env(source, env_values)?;
426 let body = body.to_string();
427 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
428
429 let referenced = compiled::collect_referenced_params(&segments);
430 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
431 check_unused_params(
432 &fm.declarations,
433 &referenced,
434 force_allow_unused || fm.allow_unused,
435 )?;
436 check_name_collisions(&fm, &inline_templates, &segments)?;
437 let enum_keys = collect_enum_type_keys(&fm);
438 check_bare_enum_access(&segments, &enum_keys)?;
439 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
440 check_internal_key_access(&segments)?;
441 let label_errors =
443 compiled::validate_match_labels(&segments, &fm.declarations, &fm.type_aliases);
444 if !label_errors.is_empty() {
445 return Err(TemplateError::Syntax(label_errors.join("; ").into()));
446 }
447
448 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
449 let mut consts: HashMap<String, Value> = fm
450 .consts
451 .iter()
452 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
453 .collect();
454 for d in &fm.env {
456 if let Some(ref v) = d.default_value {
457 consts.entry(d.name.clone()).or_insert_with(|| v.clone());
458 }
459 }
460 inject_enum_type_constants(&fm.type_aliases, &mut consts);
462 let segments: Arc<[Segment]> = Arc::from(segments);
463 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
464 let tmpl = Self {
465 body,
466 name: fm.name.clone(),
467 description: fm.description.clone(),
468 segments,
469 declared_variables: Arc::from(fm.declarations.clone()),
470 inline_templates: Arc::new(inline_templates),
471 source_hash,
472 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
473 has_defaults,
474 consts: Arc::new(consts),
475 imported_consts: Arc::new(fm.imported_consts.clone()),
476 estimated_capacity,
477 };
478 Ok((tmpl, fm))
479 }
480
481 #[cfg(feature = "std")]
488 pub(crate) fn from_cached(data: CachedTemplateData) -> Self {
489 let has_defaults = data
490 .declared_variables
491 .iter()
492 .any(|d| d.default_value.is_some());
493 let estimated_capacity = compiled::render::estimate_output_capacity(&data.segments);
494 Self {
495 body: String::new(),
496 name: data.name,
497 description: data.description,
498 segments: data.segments,
499 declared_variables: data.declared_variables,
500 base_dir: data.base_dir,
501 inline_templates: data.inline_templates,
502 source_hash: data.source_hash,
503 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
504 has_defaults,
505 consts: data.consts,
506 imported_consts: data.imported_consts,
507 estimated_capacity,
508 env_values: alloc::sync::Arc::from([]),
509 checked_type_ids: std::sync::Mutex::new(Vec::new()),
510 }
511 }
512
513 #[doc(hidden)]
515 #[must_use]
516 pub fn from_precompiled(data: &PrecompiledTemplateData<'_>) -> Self {
517 let inline_map = data
518 .inline_templates
519 .iter()
520 .map(|(k, v)| (k.to_string(), v.clone()))
521 .collect();
522 let const_map = data
523 .consts
524 .iter()
525 .map(|(k, v)| (k.to_string(), v.clone()))
526 .collect();
527 let imported_const_map = data
528 .imported_consts
529 .iter()
530 .map(|(k, v)| (k.to_string(), v.clone()))
531 .collect();
532 let has_defaults = data
533 .declared_variables
534 .iter()
535 .any(|d| d.default_value.is_some());
536 let segments: Arc<[Segment]> = Arc::from(data.segments);
537 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
538 Self {
539 body: String::new(),
540 name: data.name.map(String::from),
541 description: data.description.map(String::from),
542 segments,
543 declared_variables: Arc::from(data.declared_variables),
544 #[cfg(feature = "std")]
545 base_dir: None,
546 inline_templates: Arc::new(inline_map),
547 source_hash: data.source_hash,
548 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
549 has_defaults,
550 consts: Arc::new(const_map),
551 imported_consts: Arc::new(imported_const_map),
552 estimated_capacity,
553 #[cfg(feature = "std")]
554 env_values: alloc::sync::Arc::from([]),
555 #[cfg(feature = "std")]
556 checked_type_ids: std::sync::Mutex::new(Vec::new()),
557 }
558 }
559
560 fn validate_context(&self, ctx: &Context, allow_extra: bool) -> Result<(), TemplateError> {
573 let mut missing = Vec::new();
574 let mut mismatch: Option<(String, crate::types::TypeCheckError)> = None;
575 for decl in self.declared_variables.iter() {
576 match ctx.get(&decl.name) {
577 None => {
578 if decl.default_value.is_none() {
580 missing.push(decl.name.as_str());
581 }
582 }
583 Some(value) => {
584 if mismatch.is_none()
585 && let Err(e) = decl.var_type.check(value)
586 {
587 mismatch = Some((decl.name.clone(), e));
588 }
589 }
590 }
591 }
592 if !missing.is_empty() {
594 return Err(TemplateError::MissingParams(
595 missing.into_iter().map(String::from).collect(),
596 ));
597 }
598 if let Some((name, check_err)) = mismatch {
599 let detail = if check_err.path.is_empty() {
600 String::new()
601 } else {
602 format!(" (at .{})", check_err.path)
603 };
604 return Err(TemplateError::TypeMismatch {
605 name: format!("{name}{detail}"),
606 expected: check_err.expected,
607 actual: check_err.actual,
608 actual_value: check_err.actual_value,
609 });
610 }
611 if !allow_extra {
613 let mut declared: HashSet<&str> = self
614 .declared_variables
615 .iter()
616 .map(|d| d.name.as_str())
617 .collect();
618 for name in self.consts.keys() {
619 declared.insert(name.as_str());
620 }
621 let extra: Vec<String> = ctx
622 .values
623 .keys()
624 .filter(|k| !declared.contains(k.as_str()))
625 .cloned()
626 .collect();
627 if !extra.is_empty() {
628 return Err(TemplateError::ExtraParams(extra));
629 }
630 }
631 Ok(())
632 }
633
634 #[must_use]
636 pub fn defaults(&self) -> HashMap<String, crate::value::Value> {
637 self.declared_variables
638 .iter()
639 .filter_map(|d| {
640 d.default_value
641 .as_ref()
642 .map(|v| (d.name.clone(), v.clone()))
643 })
644 .collect()
645 }
646
647 #[must_use]
649 pub fn default(&self, name: &str) -> Option<&crate::value::Value> {
650 self.declared_variables
651 .iter()
652 .find(|d| d.name == name)
653 .and_then(|d| d.default_value.as_ref())
654 }
655
656 #[must_use]
675 pub fn defaults_context(&self) -> Context {
676 let defaults = self.defaults();
677 let mut ctx = Context::with_capacity(defaults.len());
678 for (k, v) in defaults {
679 ctx.set(k, v);
680 }
681 ctx
682 }
683
684 #[must_use]
688 pub fn body(&self) -> &str {
689 &self.body
690 }
691
692 #[must_use]
694 pub fn name(&self) -> Option<&str> {
695 self.name.as_deref()
696 }
697
698 #[must_use]
700 pub fn description(&self) -> Option<&str> {
701 self.description.as_deref()
702 }
703
704 pub fn set_max_include_depth(&mut self, depth: usize) {
706 self.max_include_depth = depth;
707 }
708
709 #[must_use]
711 pub fn with_max_include_depth(mut self, depth: usize) -> Self {
712 self.max_include_depth = depth;
713 self
714 }
715
716 #[must_use]
721 pub fn declarations(&self) -> &[VarDecl] {
722 &self.declared_variables
723 }
724
725 pub(crate) fn segments(&self) -> &[crate::compiled::Segment] {
726 &self.segments
727 }
728
729 #[cfg(feature = "std")]
731 #[must_use]
732 pub fn base_dir(&self) -> Option<&Path> {
733 self.base_dir.as_deref()
734 }
735
736 #[must_use]
760 pub fn consts(&self) -> Arc<HashMap<String, Value>> {
761 self.consts.clone()
762 }
763
764 #[must_use]
767 pub fn consts_ref(&self) -> &HashMap<String, Value> {
768 &self.consts
769 }
770
771 #[must_use]
776 pub fn imported_consts(&self) -> Arc<HashMap<String, Value>> {
777 self.imported_consts.clone()
778 }
779
780 #[must_use]
783 pub fn imported_consts_ref(&self) -> &HashMap<String, Value> {
784 &self.imported_consts
785 }
786
787 pub(crate) fn inline_templates(&self) -> &HashMap<String, CompiledInlineTemplate> {
788 &self.inline_templates
789 }
790
791 #[must_use]
798 pub fn source_hash(&self) -> u64 {
799 self.source_hash
800 }
801
802 pub fn validate_declarations(&self, expected: &[VarDecl]) -> Result<(), TemplateError> {
817 let current: HashMap<&str, &crate::types::VarType> = self
818 .declared_variables
819 .iter()
820 .map(|d| (d.name.as_str(), &d.var_type))
821 .collect();
822 let expected_map: HashMap<&str, &crate::types::VarType> = expected
823 .iter()
824 .map(|d| (d.name.as_str(), &d.var_type))
825 .collect();
826
827 let current_names: HashSet<&str> = current.keys().copied().collect();
828 let expected_names: HashSet<&str> = expected_map.keys().copied().collect();
829
830 let missing: Vec<&str> = expected_names.difference(¤t_names).copied().collect();
831 let extra: Vec<&str> = current_names.difference(&expected_names).copied().collect();
832
833 let retyped: Vec<String> = current_names
835 .intersection(&expected_names)
836 .filter_map(|name| {
837 let cur_type = current[name];
838 let exp_type = expected_map[name];
839 if cur_type == exp_type {
840 None
841 } else {
842 Some(format!("{name}: {exp_type} → {cur_type}"))
843 }
844 })
845 .collect();
846
847 if missing.is_empty() && extra.is_empty() && retyped.is_empty() {
848 return Ok(());
849 }
850
851 let mut parts = Vec::new();
852 if !missing.is_empty() {
853 parts.push(format!("removed: {}", missing.join(", ")));
854 }
855 if !extra.is_empty() {
856 parts.push(format!("added: {}", extra.join(", ")));
857 }
858 if !retyped.is_empty() {
859 parts.push(format!("retyped: {}", retyped.join(", ")));
860 }
861
862 Err(TemplateError::DeclarationsMutated {
863 details: parts.join("; "),
864 })
865 }
866}
867
868impl PartialEq for Template {
881 fn eq(&self, other: &Self) -> bool {
882 self.source_hash == other.source_hash
883 }
884}
885
886impl Eq for Template {}
887
888#[cfg(feature = "serde")]
894impl serde::Serialize for Template {
895 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
896 serializer.serialize_str(&format!("template:{:016x}", self.source_hash))
897 }
898}
899
900#[cfg(feature = "serde")]
906impl<'de> serde::Deserialize<'de> for Template {
907 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
908 let _ = <serde::de::IgnoredAny as serde::Deserialize>::deserialize(deserializer)?;
910 Err(serde::de::Error::custom(
911 "Template cannot be deserialized; construct from source with \
912 Template::from_source() or Template::from_file()",
913 ))
914 }
915}
916
917#[cfg(feature = "std")]
925pub fn load_template(dir: &Path, name: &str) -> Result<Template, TemplateError> {
926 let path = dir.join(format!("{name}.tmpl.md"));
927 Template::from_file(&path)
928}
929
930#[cfg(all(test, feature = "std"))]
931mod adversarial_tests;
932#[cfg(all(test, feature = "std"))]
933mod collision_and_scope_tests;
934#[cfg(all(test, feature = "std"))]
935mod const_tests;
936#[cfg(all(test, feature = "std"))]
937mod error_diagnostic_tests;
938#[cfg(all(test, feature = "std"))]
939mod higher_order_tests;
940#[cfg(all(test, feature = "std"))]
941mod inline_edge_tests;
942#[cfg(all(test, feature = "std"))]
943mod render_integration_tests;
944#[cfg(all(test, feature = "std"))]
945mod shared_tests;
946#[cfg(all(test, feature = "std"))]
947mod tests;
948
949#[cfg(all(test, feature = "std"))]
950mod doc_example_tests;