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 mut source = std::fs::read_to_string(path)?;
240 if source.contains('\r') {
242 source = source.replace("\r\n", "\n");
243 }
244 let (tmpl, _fm) =
245 Self::compile_from_source(&source, Some(path.parent().unwrap_or(Path::new("."))))?;
246 Ok(tmpl)
247 }
248
249 pub fn from_source(source: &str) -> Result<Self, TemplateError> {
255 let source = if source.contains('\r') {
257 alloc::borrow::Cow::Owned(source.replace("\r\n", "\n"))
258 } else {
259 alloc::borrow::Cow::Borrowed(source)
260 };
261 #[cfg(feature = "std")]
262 let (tmpl, _fm) = Self::compile_from_source(&source, None)?;
263 #[cfg(not(feature = "std"))]
264 let (tmpl, _fm) = Self::compile_from_source_no_std(&source)?;
265 Ok(tmpl)
266 }
267
268 pub fn compile(
293 source: &str,
294 options: CompileOptions<'_>,
295 ) -> Result<(Self, Frontmatter), TemplateError> {
296 let source = if source.contains('\r') {
299 alloc::borrow::Cow::Owned(source.replace("\r\n", "\n"))
300 } else {
301 alloc::borrow::Cow::Borrowed(source)
302 };
303 #[cfg(feature = "std")]
304 return Self::compile_inner(&source, options.base_dir, options.allow_unused, options.env);
305 #[cfg(not(feature = "std"))]
306 return Self::compile_inner_no_std(&source, options.allow_unused, options.env);
307 }
308
309 #[cfg(feature = "std")]
330 pub fn compile_file(
331 path: &Path,
332 options: CompileOptions<'_>,
333 ) -> Result<(Self, Frontmatter), TemplateError> {
334 let mut source = std::fs::read_to_string(path)?;
335 if source.contains('\r') {
337 source = source.replace("\r\n", "\n");
338 }
339 let base_dir = options.base_dir.or_else(|| path.parent());
340 Self::compile_inner(&source, base_dir, options.allow_unused, options.env)
341 }
342
343 #[cfg(feature = "std")]
345 fn compile_from_source(
346 source: &str,
347 base_dir: Option<&Path>,
348 ) -> Result<(Self, Frontmatter), TemplateError> {
349 Self::compile_inner(source, base_dir, false, &[])
350 }
351
352 #[cfg(feature = "std")]
357 fn compile_inner(
358 source: &str,
359 base_dir: Option<&Path>,
360 force_allow_unused: bool,
361 env_values: &[(&str, Value)],
362 ) -> Result<(Self, Frontmatter), TemplateError> {
363 let source_hash = crate::cache::hash_source(source);
364 let (fm, body) = if let Some(dir) = base_dir {
365 frontmatter::parse_frontmatter_with_base_dir(source, dir, env_values)?
366 } else {
367 frontmatter::parse_frontmatter_with_env(source, env_values)?
368 };
369 let body = body.to_string();
370 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
371
372 let referenced = compiled::collect_referenced_params(&segments);
374 let case_labels = compiled::collect_unquoted_case_labels(&segments);
375 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
376 check_unused_params(
377 &fm.declarations,
378 &referenced,
379 &case_labels,
380 force_allow_unused || fm.allow_unused,
381 )?;
382 check_name_collisions(&fm, &inline_templates, &segments)?;
383 let enum_keys = collect_enum_type_keys(&fm);
384 check_bare_enum_access(&segments, &enum_keys)?;
385 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
386 check_internal_key_access(&segments)?;
387 let label_errors =
389 compiled::validate_match_labels(&segments, &fm.declarations, &fm.type_aliases);
390 if !label_errors.is_empty() {
391 return Err(TemplateError::Syntax(label_errors.join("; ").into()));
392 }
393
394 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
395 let mut consts: HashMap<String, Value> = fm
396 .consts
397 .iter()
398 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
399 .collect();
400 for d in &fm.env {
402 if let Some(ref v) = d.default_value {
403 consts.entry(d.name.clone()).or_insert_with(|| v.clone());
404 }
405 }
406 inject_enum_type_constants(&fm.type_aliases, &mut consts);
408 let segments: Arc<[Segment]> = Arc::from(segments);
409 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
410 let env_values: alloc::sync::Arc<[(String, Value)]> = env_values
411 .iter()
412 .map(|(k, v)| (k.to_string(), v.clone()))
413 .collect();
414 let tmpl = Self {
415 body,
416 name: fm.name.clone(),
417 description: fm.description.clone(),
418 segments,
419 declared_variables: Arc::from(fm.declarations.clone()),
420 base_dir: base_dir.map(Path::to_path_buf),
421 inline_templates: Arc::new(inline_templates),
422 source_hash,
423 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
424 has_defaults,
425 consts: Arc::new(consts),
426 imported_consts: Arc::new(fm.imported_consts.clone()),
427 estimated_capacity,
428 env_values,
429 checked_type_ids: std::sync::Mutex::new(Vec::new()),
430 };
431 Ok((tmpl, fm))
432 }
433
434 #[cfg(not(feature = "std"))]
436 fn compile_from_source_no_std(source: &str) -> Result<(Self, Frontmatter), TemplateError> {
437 Self::compile_inner_no_std(source, false, &[])
438 }
439
440 #[cfg(not(feature = "std"))]
442 fn compile_inner_no_std(
443 source: &str,
444 force_allow_unused: bool,
445 env_values: &[(&str, Value)],
446 ) -> Result<(Self, Frontmatter), TemplateError> {
447 let source_hash = hash_source_no_std(source);
448 let (fm, body) = frontmatter::parse_frontmatter_with_env(source, env_values)?;
449 let body = body.to_string();
450 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
451
452 let referenced = compiled::collect_referenced_params(&segments);
453 let case_labels = compiled::collect_unquoted_case_labels(&segments);
454 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
455 check_unused_params(
456 &fm.declarations,
457 &referenced,
458 &case_labels,
459 force_allow_unused || fm.allow_unused,
460 )?;
461 check_name_collisions(&fm, &inline_templates, &segments)?;
462 let enum_keys = collect_enum_type_keys(&fm);
463 check_bare_enum_access(&segments, &enum_keys)?;
464 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
465 check_internal_key_access(&segments)?;
466 let label_errors =
468 compiled::validate_match_labels(&segments, &fm.declarations, &fm.type_aliases);
469 if !label_errors.is_empty() {
470 return Err(TemplateError::Syntax(label_errors.join("; ").into()));
471 }
472
473 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
474 let mut consts: HashMap<String, Value> = fm
475 .consts
476 .iter()
477 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
478 .collect();
479 for d in &fm.env {
481 if let Some(ref v) = d.default_value {
482 consts.entry(d.name.clone()).or_insert_with(|| v.clone());
483 }
484 }
485 inject_enum_type_constants(&fm.type_aliases, &mut consts);
487 let segments: Arc<[Segment]> = Arc::from(segments);
488 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
489 let tmpl = Self {
490 body,
491 name: fm.name.clone(),
492 description: fm.description.clone(),
493 segments,
494 declared_variables: Arc::from(fm.declarations.clone()),
495 inline_templates: Arc::new(inline_templates),
496 source_hash,
497 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
498 has_defaults,
499 consts: Arc::new(consts),
500 imported_consts: Arc::new(fm.imported_consts.clone()),
501 estimated_capacity,
502 };
503 Ok((tmpl, fm))
504 }
505
506 #[cfg(feature = "std")]
513 pub(crate) fn from_cached(data: CachedTemplateData) -> Self {
514 let has_defaults = data
515 .declared_variables
516 .iter()
517 .any(|d| d.default_value.is_some());
518 let estimated_capacity = compiled::render::estimate_output_capacity(&data.segments);
519 Self {
520 body: String::new(),
521 name: data.name,
522 description: data.description,
523 segments: data.segments,
524 declared_variables: data.declared_variables,
525 base_dir: data.base_dir,
526 inline_templates: data.inline_templates,
527 source_hash: data.source_hash,
528 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
529 has_defaults,
530 consts: data.consts,
531 imported_consts: data.imported_consts,
532 estimated_capacity,
533 env_values: alloc::sync::Arc::from([]),
534 checked_type_ids: std::sync::Mutex::new(Vec::new()),
535 }
536 }
537
538 #[doc(hidden)]
540 #[must_use]
541 pub fn from_precompiled(data: &PrecompiledTemplateData<'_>) -> Self {
542 let inline_map = data
543 .inline_templates
544 .iter()
545 .map(|(k, v)| (k.to_string(), v.clone()))
546 .collect();
547 let const_map = data
548 .consts
549 .iter()
550 .map(|(k, v)| (k.to_string(), v.clone()))
551 .collect();
552 let imported_const_map = data
553 .imported_consts
554 .iter()
555 .map(|(k, v)| (k.to_string(), v.clone()))
556 .collect();
557 let has_defaults = data
558 .declared_variables
559 .iter()
560 .any(|d| d.default_value.is_some());
561 let segments: Arc<[Segment]> = Arc::from(data.segments);
562 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
563 Self {
564 body: String::new(),
565 name: data.name.map(String::from),
566 description: data.description.map(String::from),
567 segments,
568 declared_variables: Arc::from(data.declared_variables),
569 #[cfg(feature = "std")]
570 base_dir: None,
571 inline_templates: Arc::new(inline_map),
572 source_hash: data.source_hash,
573 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
574 has_defaults,
575 consts: Arc::new(const_map),
576 imported_consts: Arc::new(imported_const_map),
577 estimated_capacity,
578 #[cfg(feature = "std")]
579 env_values: alloc::sync::Arc::from([]),
580 #[cfg(feature = "std")]
581 checked_type_ids: std::sync::Mutex::new(Vec::new()),
582 }
583 }
584
585 fn validate_context(&self, ctx: &Context, allow_extra: bool) -> Result<(), TemplateError> {
598 let mut missing = Vec::new();
599 let mut mismatch: Option<(String, crate::types::TypeCheckError)> = None;
600 for decl in self.declared_variables.iter() {
601 match ctx.get(&decl.name) {
602 None => {
603 if decl.default_value.is_none() {
605 missing.push(decl.name.as_str());
606 }
607 }
608 Some(value) => {
609 if mismatch.is_none()
610 && let Err(e) = decl.var_type.check(value)
611 {
612 mismatch = Some((decl.name.clone(), e));
613 }
614 }
615 }
616 }
617 if !missing.is_empty() {
619 return Err(TemplateError::MissingParams(
620 missing.into_iter().map(String::from).collect(),
621 ));
622 }
623 if let Some((name, check_err)) = mismatch {
624 let detail = if check_err.path.is_empty() {
625 String::new()
626 } else {
627 format!(" (at .{})", check_err.path)
628 };
629 return Err(TemplateError::TypeMismatch {
630 name: format!("{name}{detail}"),
631 expected: check_err.expected,
632 actual: check_err.actual,
633 actual_value: check_err.actual_value,
634 });
635 }
636 if !allow_extra {
638 let mut declared: HashSet<&str> = self
639 .declared_variables
640 .iter()
641 .map(|d| d.name.as_str())
642 .collect();
643 for name in self.consts.keys() {
644 declared.insert(name.as_str());
645 }
646 let extra: Vec<String> = ctx
647 .values
648 .keys()
649 .filter(|k| !declared.contains(k.as_str()))
650 .cloned()
651 .collect();
652 if !extra.is_empty() {
653 return Err(TemplateError::ExtraParams(extra));
654 }
655 }
656 Ok(())
657 }
658
659 #[must_use]
661 pub fn defaults(&self) -> HashMap<String, crate::value::Value> {
662 self.declared_variables
663 .iter()
664 .filter_map(|d| {
665 d.default_value
666 .as_ref()
667 .map(|v| (d.name.clone(), v.clone()))
668 })
669 .collect()
670 }
671
672 #[must_use]
674 pub fn default(&self, name: &str) -> Option<&crate::value::Value> {
675 self.declared_variables
676 .iter()
677 .find(|d| d.name == name)
678 .and_then(|d| d.default_value.as_ref())
679 }
680
681 #[must_use]
700 pub fn defaults_context(&self) -> Context {
701 let defaults = self.defaults();
702 let mut ctx = Context::with_capacity(defaults.len());
703 for (k, v) in defaults {
704 ctx.set(k, v);
705 }
706 ctx
707 }
708
709 #[must_use]
713 pub fn body(&self) -> &str {
714 &self.body
715 }
716
717 #[must_use]
719 pub fn name(&self) -> Option<&str> {
720 self.name.as_deref()
721 }
722
723 #[must_use]
725 pub fn description(&self) -> Option<&str> {
726 self.description.as_deref()
727 }
728
729 pub fn set_max_include_depth(&mut self, depth: usize) {
731 self.max_include_depth = depth;
732 }
733
734 #[must_use]
736 pub fn with_max_include_depth(mut self, depth: usize) -> Self {
737 self.max_include_depth = depth;
738 self
739 }
740
741 #[must_use]
746 pub fn declarations(&self) -> &[VarDecl] {
747 &self.declared_variables
748 }
749
750 pub(crate) fn segments(&self) -> &[crate::compiled::Segment] {
751 &self.segments
752 }
753
754 #[cfg(feature = "std")]
756 #[must_use]
757 pub fn base_dir(&self) -> Option<&Path> {
758 self.base_dir.as_deref()
759 }
760
761 #[must_use]
785 pub fn consts(&self) -> Arc<HashMap<String, Value>> {
786 self.consts.clone()
787 }
788
789 #[must_use]
792 pub fn consts_ref(&self) -> &HashMap<String, Value> {
793 &self.consts
794 }
795
796 #[must_use]
801 pub fn imported_consts(&self) -> Arc<HashMap<String, Value>> {
802 self.imported_consts.clone()
803 }
804
805 #[must_use]
808 pub fn imported_consts_ref(&self) -> &HashMap<String, Value> {
809 &self.imported_consts
810 }
811
812 pub(crate) fn inline_templates(&self) -> &HashMap<String, CompiledInlineTemplate> {
813 &self.inline_templates
814 }
815
816 #[must_use]
823 pub fn source_hash(&self) -> u64 {
824 self.source_hash
825 }
826
827 pub fn validate_declarations(&self, expected: &[VarDecl]) -> Result<(), TemplateError> {
842 let current: HashMap<&str, &crate::types::VarType> = self
843 .declared_variables
844 .iter()
845 .map(|d| (d.name.as_str(), &d.var_type))
846 .collect();
847 let expected_map: HashMap<&str, &crate::types::VarType> = expected
848 .iter()
849 .map(|d| (d.name.as_str(), &d.var_type))
850 .collect();
851
852 let current_names: HashSet<&str> = current.keys().copied().collect();
853 let expected_names: HashSet<&str> = expected_map.keys().copied().collect();
854
855 let missing: Vec<&str> = expected_names.difference(¤t_names).copied().collect();
856 let extra: Vec<&str> = current_names.difference(&expected_names).copied().collect();
857
858 let retyped: Vec<String> = current_names
860 .intersection(&expected_names)
861 .filter_map(|name| {
862 let cur_type = current[name];
863 let exp_type = expected_map[name];
864 if cur_type == exp_type {
865 None
866 } else {
867 Some(format!("{name}: {exp_type} → {cur_type}"))
868 }
869 })
870 .collect();
871
872 if missing.is_empty() && extra.is_empty() && retyped.is_empty() {
873 return Ok(());
874 }
875
876 let mut parts = Vec::new();
877 if !missing.is_empty() {
878 parts.push(format!("removed: {}", missing.join(", ")));
879 }
880 if !extra.is_empty() {
881 parts.push(format!("added: {}", extra.join(", ")));
882 }
883 if !retyped.is_empty() {
884 parts.push(format!("retyped: {}", retyped.join(", ")));
885 }
886
887 Err(TemplateError::DeclarationsMutated {
888 details: parts.join("; "),
889 })
890 }
891}
892
893impl PartialEq for Template {
906 fn eq(&self, other: &Self) -> bool {
907 self.source_hash == other.source_hash
908 }
909}
910
911impl Eq for Template {}
912
913#[cfg(feature = "serde")]
919impl serde::Serialize for Template {
920 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
921 serializer.serialize_str(&format!("template:{:016x}", self.source_hash))
922 }
923}
924
925#[cfg(feature = "serde")]
931impl<'de> serde::Deserialize<'de> for Template {
932 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
933 let _ = <serde::de::IgnoredAny as serde::Deserialize>::deserialize(deserializer)?;
935 Err(serde::de::Error::custom(
936 "Template cannot be deserialized; construct from source with \
937 Template::from_source() or Template::from_file()",
938 ))
939 }
940}
941
942#[cfg(feature = "std")]
950pub fn load_template(dir: &Path, name: &str) -> Result<Template, TemplateError> {
951 let path = dir.join(format!("{name}.tmpl.md"));
952 Template::from_file(&path)
953}
954
955#[cfg(all(test, feature = "std"))]
956mod adversarial_tests;
957#[cfg(all(test, feature = "std"))]
958mod collision_and_scope_tests;
959#[cfg(all(test, feature = "std"))]
960mod const_tests;
961#[cfg(all(test, feature = "std"))]
962mod error_diagnostic_tests;
963#[cfg(all(test, feature = "std"))]
964mod higher_order_tests;
965#[cfg(all(test, feature = "std"))]
966mod inline_edge_tests;
967#[cfg(all(test, feature = "std"))]
968mod render_integration_tests;
969#[cfg(all(test, feature = "std"))]
970mod shared_tests;
971#[cfg(all(test, feature = "std"))]
972mod tests;
973
974#[cfg(all(test, feature = "std"))]
975mod doc_example_tests;