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 #[cfg(not(feature = "std"))]
61 _phantom: core::marker::PhantomData<&'a ()>,
62}
63
64#[cfg(feature = "std")]
65impl<'a> CompileOptions<'a> {
66 #[must_use]
68 pub fn base_dir(mut self, dir: &'a std::path::Path) -> Self {
69 self.base_dir = Some(dir);
70 self
71 }
72}
73
74impl CompileOptions<'_> {
75 #[must_use]
77 pub fn allow_unused(mut self, allow: bool) -> Self {
78 self.allow_unused = allow;
79 self
80 }
81}
82
83pub struct Template {
89 body: String,
91 name: Option<String>,
93 description: Option<String>,
95 segments: Arc<[Segment]>,
97 declared_variables: Arc<[VarDecl]>,
99 #[cfg(feature = "std")]
101 base_dir: Option<PathBuf>,
102 inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
104 source_hash: u64,
105 max_include_depth: usize,
106 has_defaults: bool,
108 consts: Arc<HashMap<String, crate::value::Value>>,
110 imported_consts: Arc<HashMap<String, crate::value::Value>>,
112 estimated_capacity: usize,
114 #[cfg(feature = "std")]
120 checked_type_ids: std::sync::Mutex<Vec<core::any::TypeId>>,
121}
122
123impl core::fmt::Debug for Template {
124 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125 f.debug_struct("Template")
126 .field("body", &self.body)
127 .field("name", &self.name)
128 .field("description", &self.description)
129 .field("segments", &self.segments)
130 .field("declared_variables", &self.declared_variables)
131 .field("source_hash", &self.source_hash)
132 .finish_non_exhaustive()
133 }
134}
135
136impl Clone for Template {
137 fn clone(&self) -> Self {
138 Self {
139 body: self.body.clone(),
140 name: self.name.clone(),
141 description: self.description.clone(),
142 segments: self.segments.clone(),
143 declared_variables: self.declared_variables.clone(),
144 #[cfg(feature = "std")]
145 base_dir: self.base_dir.clone(),
146 inline_templates: self.inline_templates.clone(),
147 source_hash: self.source_hash,
148 max_include_depth: self.max_include_depth,
149 has_defaults: self.has_defaults,
150 consts: self.consts.clone(),
151 imported_consts: self.imported_consts.clone(),
152 estimated_capacity: self.estimated_capacity,
153 #[cfg(feature = "std")]
156 checked_type_ids: std::sync::Mutex::new(
157 self.checked_type_ids
158 .lock()
159 .unwrap_or_else(std::sync::PoisonError::into_inner)
160 .clone(),
161 ),
162 }
163 }
164}
165
166#[cfg(feature = "std")]
170pub(crate) struct CachedTemplateData {
171 pub segments: Arc<[Segment]>,
173 pub declared_variables: Arc<[VarDecl]>,
175 pub base_dir: Option<PathBuf>,
177 pub inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
179 pub source_hash: u64,
181 pub consts: Arc<HashMap<String, crate::value::Value>>,
183 pub imported_consts: Arc<HashMap<String, crate::value::Value>>,
185 pub name: Option<String>,
187 pub description: Option<String>,
189}
190
191#[doc(hidden)]
195pub struct PrecompiledTemplateData<'a> {
196 pub segments: &'a [Segment],
198 pub declared_variables: &'a [VarDecl],
200 pub inline_templates: &'a [(&'a str, CompiledInlineTemplate)],
202 pub source_hash: u64,
204 pub consts: &'a [(&'a str, crate::value::Value)],
206 pub imported_consts: &'a [(&'a str, crate::value::Value)],
208 pub name: Option<&'a str>,
210 pub description: Option<&'a str>,
212}
213
214impl Template {
215 #[cfg(feature = "std")]
221 pub fn from_file(path: &Path) -> Result<Self, TemplateError> {
222 let source = std::fs::read_to_string(path)?;
223 let (tmpl, _fm) =
224 Self::compile_from_source(&source, Some(path.parent().unwrap_or(Path::new("."))))?;
225 Ok(tmpl)
226 }
227
228 pub fn from_source(source: &str) -> Result<Self, TemplateError> {
234 #[cfg(feature = "std")]
235 let (tmpl, _fm) = Self::compile_from_source(source, None)?;
236 #[cfg(not(feature = "std"))]
237 let (tmpl, _fm) = Self::compile_from_source_no_std(source)?;
238 Ok(tmpl)
239 }
240
241 #[deprecated(
252 since = "0.2.0",
253 note = "Use `Template::compile(source, CompileOptions::default().allow_unused(true))` instead"
254 )]
255 pub fn from_source_allowing_unused(source: &str) -> Result<Self, TemplateError> {
256 let (tmpl, _fm) = Self::compile(source, CompileOptions::default().allow_unused(true))?;
257 Ok(tmpl)
258 }
259
260 #[cfg(feature = "std")]
266 #[deprecated(
267 since = "0.2.0",
268 note = "Use `Template::compile(source, CompileOptions::default().base_dir(dir))` instead"
269 )]
270 pub fn from_source_with_base_dir(source: &str, base_dir: &Path) -> Result<Self, TemplateError> {
271 let (tmpl, _fm) = Self::compile(source, CompileOptions::default().base_dir(base_dir))?;
272 Ok(tmpl)
273 }
274
275 #[deprecated(
281 since = "0.2.0",
282 note = "Use `Template::compile(source, CompileOptions::default())` which always returns Frontmatter"
283 )]
284 pub fn from_source_with_frontmatter(
285 source: &str,
286 ) -> Result<(Self, Frontmatter), TemplateError> {
287 Self::compile(source, CompileOptions::default())
288 }
289
290 #[cfg(feature = "std")]
296 #[deprecated(
297 since = "0.2.0",
298 note = "Use `Template::compile_file(path, CompileOptions::default())` which always returns Frontmatter"
299 )]
300 pub fn from_file_with_frontmatter(path: &Path) -> Result<(Self, Frontmatter), TemplateError> {
301 Self::compile_file(path, CompileOptions::default())
302 }
303
304 pub fn compile(
329 source: &str,
330 options: CompileOptions<'_>,
331 ) -> Result<(Self, Frontmatter), TemplateError> {
332 #[cfg(feature = "std")]
333 return Self::compile_inner(source, options.base_dir, options.allow_unused);
334 #[cfg(not(feature = "std"))]
335 return Self::compile_inner_no_std(source, options.allow_unused);
336 }
337
338 #[cfg(feature = "std")]
359 pub fn compile_file(
360 path: &Path,
361 options: CompileOptions<'_>,
362 ) -> Result<(Self, Frontmatter), TemplateError> {
363 let source = std::fs::read_to_string(path)?;
364 let base_dir = options.base_dir.or_else(|| path.parent());
365 Self::compile_inner(&source, base_dir, options.allow_unused)
366 }
367
368 #[cfg(feature = "std")]
370 fn compile_from_source(
371 source: &str,
372 base_dir: Option<&Path>,
373 ) -> Result<(Self, Frontmatter), TemplateError> {
374 Self::compile_inner(source, base_dir, false)
375 }
376
377 #[cfg(feature = "std")]
382 fn compile_inner(
383 source: &str,
384 base_dir: Option<&Path>,
385 force_allow_unused: bool,
386 ) -> Result<(Self, Frontmatter), TemplateError> {
387 let source_hash = crate::cache::hash_source(source);
388 let (fm, body) = if let Some(dir) = base_dir {
389 frontmatter::parse_frontmatter_with_base_dir(source, dir)?
390 } else {
391 frontmatter::parse_frontmatter(source)?
392 };
393 let body = body.to_string();
394 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
395
396 let referenced = compiled::collect_referenced_params(&segments);
398 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
399 check_unused_params(
400 &fm.declarations,
401 &referenced,
402 force_allow_unused || fm.allow_unused,
403 )?;
404 check_name_collisions(&fm, &inline_templates, &segments)?;
405 let enum_keys = collect_enum_type_keys(&fm);
406 check_bare_enum_access(&segments, &enum_keys)?;
407 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
408 check_internal_key_access(&segments)?;
409
410 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
411 let mut consts: HashMap<String, Value> = fm
412 .consts
413 .iter()
414 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
415 .collect();
416 inject_enum_type_constants(&fm.type_aliases, &mut consts);
418 let segments: Arc<[Segment]> = Arc::from(segments);
419 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
420 let tmpl = Self {
421 body,
422 name: fm.name.clone(),
423 description: fm.description.clone(),
424 segments,
425 declared_variables: Arc::from(fm.declarations.clone()),
426 base_dir: base_dir.map(Path::to_path_buf),
427 inline_templates: Arc::new(inline_templates),
428 source_hash,
429 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
430 has_defaults,
431 consts: Arc::new(consts),
432 imported_consts: Arc::new(fm.imported_consts.clone()),
433 estimated_capacity,
434 checked_type_ids: std::sync::Mutex::new(Vec::new()),
435 };
436 Ok((tmpl, fm))
437 }
438
439 #[cfg(not(feature = "std"))]
441 fn compile_from_source_no_std(source: &str) -> Result<(Self, Frontmatter), TemplateError> {
442 Self::compile_inner_no_std(source, false)
443 }
444
445 #[cfg(not(feature = "std"))]
447 fn compile_inner_no_std(
448 source: &str,
449 force_allow_unused: bool,
450 ) -> Result<(Self, Frontmatter), TemplateError> {
451 let source_hash = hash_source_no_std(source);
452 let (fm, body) = frontmatter::parse_frontmatter(source)?;
453 let body = body.to_string();
454 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
455
456 let referenced = compiled::collect_referenced_params(&segments);
457 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
458 check_unused_params(
459 &fm.declarations,
460 &referenced,
461 force_allow_unused || fm.allow_unused,
462 )?;
463 check_name_collisions(&fm, &inline_templates, &segments)?;
464 let enum_keys = collect_enum_type_keys(&fm);
465 check_bare_enum_access(&segments, &enum_keys)?;
466 check_static_enum_in_conditions(&segments, &fm.type_aliases)?;
467 check_internal_key_access(&segments)?;
468
469 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
470 let mut consts: HashMap<String, Value> = fm
471 .consts
472 .iter()
473 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
474 .collect();
475 inject_enum_type_constants(&fm.type_aliases, &mut consts);
477 let segments: Arc<[Segment]> = Arc::from(segments);
478 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
479 let tmpl = Self {
480 body,
481 name: fm.name.clone(),
482 description: fm.description.clone(),
483 segments,
484 declared_variables: Arc::from(fm.declarations.clone()),
485 inline_templates: Arc::new(inline_templates),
486 source_hash,
487 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
488 has_defaults,
489 consts: Arc::new(consts),
490 imported_consts: Arc::new(fm.imported_consts.clone()),
491 estimated_capacity,
492 };
493 Ok((tmpl, fm))
494 }
495
496 #[cfg(feature = "std")]
503 pub(crate) fn from_cached(data: CachedTemplateData) -> Self {
504 let has_defaults = data
505 .declared_variables
506 .iter()
507 .any(|d| d.default_value.is_some());
508 let estimated_capacity = compiled::render::estimate_output_capacity(&data.segments);
509 Self {
510 body: String::new(),
511 name: data.name,
512 description: data.description,
513 segments: data.segments,
514 declared_variables: data.declared_variables,
515 base_dir: data.base_dir,
516 inline_templates: data.inline_templates,
517 source_hash: data.source_hash,
518 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
519 has_defaults,
520 consts: data.consts,
521 imported_consts: data.imported_consts,
522 estimated_capacity,
523 checked_type_ids: std::sync::Mutex::new(Vec::new()),
524 }
525 }
526
527 #[doc(hidden)]
529 #[must_use]
530 pub fn from_precompiled(data: &PrecompiledTemplateData<'_>) -> Self {
531 let inline_map = data
532 .inline_templates
533 .iter()
534 .map(|(k, v)| (k.to_string(), v.clone()))
535 .collect();
536 let const_map = data
537 .consts
538 .iter()
539 .map(|(k, v)| (k.to_string(), v.clone()))
540 .collect();
541 let imported_const_map = data
542 .imported_consts
543 .iter()
544 .map(|(k, v)| (k.to_string(), v.clone()))
545 .collect();
546 let has_defaults = data
547 .declared_variables
548 .iter()
549 .any(|d| d.default_value.is_some());
550 let segments: Arc<[Segment]> = Arc::from(data.segments);
551 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
552 Self {
553 body: String::new(),
554 name: data.name.map(String::from),
555 description: data.description.map(String::from),
556 segments,
557 declared_variables: Arc::from(data.declared_variables),
558 #[cfg(feature = "std")]
559 base_dir: None,
560 inline_templates: Arc::new(inline_map),
561 source_hash: data.source_hash,
562 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
563 has_defaults,
564 consts: Arc::new(const_map),
565 imported_consts: Arc::new(imported_const_map),
566 estimated_capacity,
567 #[cfg(feature = "std")]
568 checked_type_ids: std::sync::Mutex::new(Vec::new()),
569 }
570 }
571
572 fn validate_context(&self, ctx: &Context, allow_extra: bool) -> Result<(), TemplateError> {
585 let mut missing = Vec::new();
586 let mut mismatch: Option<(String, crate::types::TypeCheckError)> = None;
587 for decl in self.declared_variables.iter() {
588 match ctx.get(&decl.name) {
589 None => {
590 if decl.default_value.is_none() {
592 missing.push(decl.name.as_str());
593 }
594 }
595 Some(value) => {
596 if mismatch.is_none()
597 && let Err(e) = decl.var_type.check(value)
598 {
599 mismatch = Some((decl.name.clone(), e));
600 }
601 }
602 }
603 }
604 if !missing.is_empty() {
606 return Err(TemplateError::MissingParams(
607 missing.into_iter().map(String::from).collect(),
608 ));
609 }
610 if let Some((name, check_err)) = mismatch {
611 let detail = if check_err.path.is_empty() {
612 String::new()
613 } else {
614 format!(" (at .{})", check_err.path)
615 };
616 return Err(TemplateError::TypeMismatch {
617 name: format!("{name}{detail}"),
618 expected: check_err.expected,
619 actual: check_err.actual,
620 actual_value: check_err.actual_value,
621 });
622 }
623 if !allow_extra {
625 let mut declared: HashSet<&str> = self
626 .declared_variables
627 .iter()
628 .map(|d| d.name.as_str())
629 .collect();
630 for name in self.consts.keys() {
631 declared.insert(name.as_str());
632 }
633 let extra: Vec<String> = ctx
634 .values
635 .keys()
636 .filter(|k| !declared.contains(k.as_str()))
637 .cloned()
638 .collect();
639 if !extra.is_empty() {
640 return Err(TemplateError::ExtraParams(extra));
641 }
642 }
643 Ok(())
644 }
645
646 #[must_use]
648 pub fn defaults(&self) -> HashMap<String, crate::value::Value> {
649 self.declared_variables
650 .iter()
651 .filter_map(|d| {
652 d.default_value
653 .as_ref()
654 .map(|v| (d.name.clone(), v.clone()))
655 })
656 .collect()
657 }
658
659 #[must_use]
661 pub fn default(&self, name: &str) -> Option<&crate::value::Value> {
662 self.declared_variables
663 .iter()
664 .find(|d| d.name == name)
665 .and_then(|d| d.default_value.as_ref())
666 }
667
668 #[must_use]
687 pub fn defaults_context(&self) -> Context {
688 let defaults = self.defaults();
689 let mut ctx = Context::with_capacity(defaults.len());
690 for (k, v) in defaults {
691 ctx.set(k, v);
692 }
693 ctx
694 }
695
696 #[must_use]
700 pub fn body(&self) -> &str {
701 &self.body
702 }
703
704 #[must_use]
706 pub fn name(&self) -> Option<&str> {
707 self.name.as_deref()
708 }
709
710 #[must_use]
712 pub fn description(&self) -> Option<&str> {
713 self.description.as_deref()
714 }
715
716 pub fn set_max_include_depth(&mut self, depth: usize) {
718 self.max_include_depth = depth;
719 }
720
721 #[must_use]
723 pub fn with_max_include_depth(mut self, depth: usize) -> Self {
724 self.max_include_depth = depth;
725 self
726 }
727
728 #[must_use]
733 pub fn declarations(&self) -> &[VarDecl] {
734 &self.declared_variables
735 }
736
737 pub(crate) fn segments(&self) -> &[crate::compiled::Segment] {
738 &self.segments
739 }
740
741 #[cfg(feature = "std")]
743 #[must_use]
744 pub fn base_dir(&self) -> Option<&Path> {
745 self.base_dir.as_deref()
746 }
747
748 #[must_use]
772 pub fn consts(&self) -> Arc<HashMap<String, Value>> {
773 self.consts.clone()
774 }
775
776 #[must_use]
779 pub fn consts_ref(&self) -> &HashMap<String, Value> {
780 &self.consts
781 }
782
783 #[must_use]
788 pub fn imported_consts(&self) -> Arc<HashMap<String, Value>> {
789 self.imported_consts.clone()
790 }
791
792 #[must_use]
795 pub fn imported_consts_ref(&self) -> &HashMap<String, Value> {
796 &self.imported_consts
797 }
798
799 pub(crate) fn inline_templates(&self) -> &HashMap<String, CompiledInlineTemplate> {
800 &self.inline_templates
801 }
802
803 #[must_use]
810 pub fn source_hash(&self) -> u64 {
811 self.source_hash
812 }
813
814 pub fn validate_declarations(&self, expected: &[VarDecl]) -> Result<(), TemplateError> {
829 let current: HashMap<&str, &crate::types::VarType> = self
830 .declared_variables
831 .iter()
832 .map(|d| (d.name.as_str(), &d.var_type))
833 .collect();
834 let expected_map: HashMap<&str, &crate::types::VarType> = expected
835 .iter()
836 .map(|d| (d.name.as_str(), &d.var_type))
837 .collect();
838
839 let current_names: HashSet<&str> = current.keys().copied().collect();
840 let expected_names: HashSet<&str> = expected_map.keys().copied().collect();
841
842 let missing: Vec<&str> = expected_names.difference(¤t_names).copied().collect();
843 let extra: Vec<&str> = current_names.difference(&expected_names).copied().collect();
844
845 let retyped: Vec<String> = current_names
847 .intersection(&expected_names)
848 .filter_map(|name| {
849 let cur_type = current[name];
850 let exp_type = expected_map[name];
851 if cur_type == exp_type {
852 None
853 } else {
854 Some(format!("{name}: {exp_type} → {cur_type}"))
855 }
856 })
857 .collect();
858
859 if missing.is_empty() && extra.is_empty() && retyped.is_empty() {
860 return Ok(());
861 }
862
863 let mut parts = Vec::new();
864 if !missing.is_empty() {
865 parts.push(format!("removed: {}", missing.join(", ")));
866 }
867 if !extra.is_empty() {
868 parts.push(format!("added: {}", extra.join(", ")));
869 }
870 if !retyped.is_empty() {
871 parts.push(format!("retyped: {}", retyped.join(", ")));
872 }
873
874 Err(TemplateError::DeclarationsMutated {
875 details: parts.join("; "),
876 })
877 }
878}
879
880impl PartialEq for Template {
893 fn eq(&self, other: &Self) -> bool {
894 self.source_hash == other.source_hash
895 }
896}
897
898impl Eq for Template {}
899
900#[cfg(feature = "serde")]
906impl serde::Serialize for Template {
907 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
908 serializer.serialize_str(&format!("template:{:016x}", self.source_hash))
909 }
910}
911
912#[cfg(feature = "serde")]
918impl<'de> serde::Deserialize<'de> for Template {
919 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
920 let _ = <serde::de::IgnoredAny as serde::Deserialize>::deserialize(deserializer)?;
921 Err(serde::de::Error::custom(
922 "Template cannot be deserialized; construct from source with \
923 Template::from_source() or Template::from_file()",
924 ))
925 }
926}
927
928#[cfg(feature = "std")]
936pub fn load_template(dir: &Path, name: &str) -> Result<Template, TemplateError> {
937 let path = dir.join(format!("{name}.tmpl.md"));
938 Template::from_file(&path)
939}
940
941#[cfg(all(test, feature = "std"))]
942mod adversarial_tests;
943#[cfg(all(test, feature = "std"))]
944mod collision_and_scope_tests;
945#[cfg(all(test, feature = "std"))]
946mod const_tests;
947#[cfg(all(test, feature = "std"))]
948mod error_diagnostic_tests;
949#[cfg(all(test, feature = "std"))]
950mod higher_order_tests;
951#[cfg(all(test, feature = "std"))]
952mod inline_edge_tests;
953#[cfg(all(test, feature = "std"))]
954mod render_integration_tests;
955#[cfg(all(test, feature = "std"))]
956mod shared_tests;
957#[cfg(all(test, feature = "std"))]
958mod tests;