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 scope::Scope,
16 types::{VarDecl, VarType},
17 value::Value,
18};
19
20#[non_exhaustive]
38#[derive(Debug, Clone, Copy, Default)]
39pub struct CompileOptions<'a> {
40 pub allow_unused: bool,
45 #[cfg(feature = "std")]
49 pub base_dir: Option<&'a std::path::Path>,
50 #[cfg(not(feature = "std"))]
52 _phantom: core::marker::PhantomData<&'a ()>,
53}
54
55#[cfg(feature = "std")]
56impl<'a> CompileOptions<'a> {
57 #[must_use]
59 pub fn base_dir(mut self, dir: &'a std::path::Path) -> Self {
60 self.base_dir = Some(dir);
61 self
62 }
63}
64
65impl CompileOptions<'_> {
66 #[must_use]
68 pub fn allow_unused(mut self, allow: bool) -> Self {
69 self.allow_unused = allow;
70 self
71 }
72}
73
74#[derive(Debug, Clone)]
80pub struct Template {
81 body: String,
83 name: Option<String>,
85 description: Option<String>,
87 segments: Arc<[Segment]>,
89 declared_variables: Arc<[VarDecl]>,
91 #[cfg(feature = "std")]
93 base_dir: Option<PathBuf>,
94 inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
96 source_hash: u64,
97 max_include_depth: usize,
98 has_defaults: bool,
100 consts: Arc<HashMap<String, crate::value::Value>>,
102 imported_consts: Arc<HashMap<String, crate::value::Value>>,
104 estimated_capacity: usize,
106}
107
108#[cfg(feature = "std")]
112pub(crate) struct CachedTemplateData {
113 pub segments: Arc<[Segment]>,
115 pub declared_variables: Arc<[VarDecl]>,
117 pub base_dir: Option<PathBuf>,
119 pub inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
121 pub source_hash: u64,
123 pub consts: Arc<HashMap<String, crate::value::Value>>,
125 pub imported_consts: Arc<HashMap<String, crate::value::Value>>,
127 pub name: Option<String>,
129 pub description: Option<String>,
131}
132
133#[doc(hidden)]
137pub struct PrecompiledTemplateData<'a> {
138 pub segments: &'a [Segment],
140 pub declared_variables: &'a [VarDecl],
142 pub inline_templates: &'a [(&'a str, CompiledInlineTemplate)],
144 pub source_hash: u64,
146 pub consts: &'a [(&'a str, crate::value::Value)],
148 pub imported_consts: &'a [(&'a str, crate::value::Value)],
150 pub name: Option<&'a str>,
152 pub description: Option<&'a str>,
154}
155
156impl Template {
157 #[cfg(feature = "std")]
163 pub fn from_file(path: &Path) -> Result<Self, TemplateError> {
164 let source = std::fs::read_to_string(path)?;
165 let (tmpl, _fm) =
166 Self::compile_from_source(&source, Some(path.parent().unwrap_or(Path::new("."))))?;
167 Ok(tmpl)
168 }
169
170 pub fn from_source(source: &str) -> Result<Self, TemplateError> {
176 #[cfg(feature = "std")]
177 let (tmpl, _fm) = Self::compile_from_source(source, None)?;
178 #[cfg(not(feature = "std"))]
179 let (tmpl, _fm) = Self::compile_from_source_no_std(source)?;
180 Ok(tmpl)
181 }
182
183 #[deprecated(
194 since = "0.2.0",
195 note = "Use `Template::compile(source, CompileOptions::default().allow_unused(true))` instead"
196 )]
197 pub fn from_source_allowing_unused(source: &str) -> Result<Self, TemplateError> {
198 let (tmpl, _fm) = Self::compile(source, CompileOptions::default().allow_unused(true))?;
199 Ok(tmpl)
200 }
201
202 #[cfg(feature = "std")]
208 #[deprecated(
209 since = "0.2.0",
210 note = "Use `Template::compile(source, CompileOptions::default().base_dir(dir))` instead"
211 )]
212 pub fn from_source_with_base_dir(source: &str, base_dir: &Path) -> Result<Self, TemplateError> {
213 let (tmpl, _fm) = Self::compile(source, CompileOptions::default().base_dir(base_dir))?;
214 Ok(tmpl)
215 }
216
217 #[deprecated(
223 since = "0.2.0",
224 note = "Use `Template::compile(source, CompileOptions::default())` which always returns Frontmatter"
225 )]
226 pub fn from_source_with_frontmatter(
227 source: &str,
228 ) -> Result<(Self, Frontmatter), TemplateError> {
229 Self::compile(source, CompileOptions::default())
230 }
231
232 #[cfg(feature = "std")]
238 #[deprecated(
239 since = "0.2.0",
240 note = "Use `Template::compile_file(path, CompileOptions::default())` which always returns Frontmatter"
241 )]
242 pub fn from_file_with_frontmatter(path: &Path) -> Result<(Self, Frontmatter), TemplateError> {
243 Self::compile_file(path, CompileOptions::default())
244 }
245
246 pub fn compile(
271 source: &str,
272 options: CompileOptions<'_>,
273 ) -> Result<(Self, Frontmatter), TemplateError> {
274 #[cfg(feature = "std")]
275 return Self::compile_inner(source, options.base_dir, options.allow_unused);
276 #[cfg(not(feature = "std"))]
277 return Self::compile_inner_no_std(source, options.allow_unused);
278 }
279
280 #[cfg(feature = "std")]
301 pub fn compile_file(
302 path: &Path,
303 options: CompileOptions<'_>,
304 ) -> Result<(Self, Frontmatter), TemplateError> {
305 let source = std::fs::read_to_string(path)?;
306 let base_dir = options.base_dir.or_else(|| path.parent());
307 Self::compile_inner(&source, base_dir, options.allow_unused)
308 }
309
310 #[cfg(feature = "std")]
312 fn compile_from_source(
313 source: &str,
314 base_dir: Option<&Path>,
315 ) -> Result<(Self, Frontmatter), TemplateError> {
316 Self::compile_inner(source, base_dir, false)
317 }
318
319 #[cfg(feature = "std")]
324 fn compile_inner(
325 source: &str,
326 base_dir: Option<&Path>,
327 force_allow_unused: bool,
328 ) -> Result<(Self, Frontmatter), TemplateError> {
329 let source_hash = crate::cache::hash_source(source);
330 let (fm, body) = if let Some(dir) = base_dir {
331 frontmatter::parse_frontmatter_with_base_dir(source, dir)?
332 } else {
333 frontmatter::parse_frontmatter(source)?
334 };
335 let body = body.to_string();
336 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
337
338 let referenced = compiled::collect_referenced_params(&segments);
340 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
341 check_unused_params(
342 &fm.declarations,
343 &referenced,
344 force_allow_unused || fm.allow_unused,
345 )?;
346 check_name_collisions(&fm, &inline_templates, &segments)?;
347 let enum_keys = collect_enum_type_keys(&fm);
348 check_bare_enum_access(&segments, &enum_keys)?;
349
350 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
351 let mut consts: HashMap<String, Value> = fm
352 .consts
353 .iter()
354 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
355 .collect();
356 inject_enum_type_constants(&fm.type_aliases, &mut consts);
358 let segments: Arc<[Segment]> = Arc::from(segments);
359 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
360 let tmpl = Self {
361 body,
362 name: fm.name.clone(),
363 description: fm.description.clone(),
364 segments,
365 declared_variables: Arc::from(fm.declarations.clone()),
366 base_dir: base_dir.map(Path::to_path_buf),
367 inline_templates: Arc::new(inline_templates),
368 source_hash,
369 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
370 has_defaults,
371 consts: Arc::new(consts),
372 imported_consts: Arc::new(fm.imported_consts.clone()),
373 estimated_capacity,
374 };
375 Ok((tmpl, fm))
376 }
377
378 #[cfg(not(feature = "std"))]
380 fn compile_from_source_no_std(source: &str) -> Result<(Self, Frontmatter), TemplateError> {
381 Self::compile_inner_no_std(source, false)
382 }
383
384 #[cfg(not(feature = "std"))]
386 fn compile_inner_no_std(
387 source: &str,
388 force_allow_unused: bool,
389 ) -> Result<(Self, Frontmatter), TemplateError> {
390 let source_hash = hash_source_no_std(source);
391 let (fm, body) = frontmatter::parse_frontmatter(source)?;
392 let body = body.to_string();
393 let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
394
395 let referenced = compiled::collect_referenced_params(&segments);
396 check_undeclared_variables(&referenced, &fm, &inline_templates)?;
397 check_unused_params(
398 &fm.declarations,
399 &referenced,
400 force_allow_unused || fm.allow_unused,
401 )?;
402 check_name_collisions(&fm, &inline_templates, &segments)?;
403 let enum_keys = collect_enum_type_keys(&fm);
404 check_bare_enum_access(&segments, &enum_keys)?;
405
406 let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
407 let mut consts: HashMap<String, Value> = fm
408 .consts
409 .iter()
410 .filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
411 .collect();
412 inject_enum_type_constants(&fm.type_aliases, &mut consts);
414 let segments: Arc<[Segment]> = Arc::from(segments);
415 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
416 let tmpl = Self {
417 body,
418 name: fm.name.clone(),
419 description: fm.description.clone(),
420 segments,
421 declared_variables: Arc::from(fm.declarations.clone()),
422 inline_templates: Arc::new(inline_templates),
423 source_hash,
424 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
425 has_defaults,
426 consts: Arc::new(consts),
427 imported_consts: Arc::new(fm.imported_consts.clone()),
428 estimated_capacity,
429 };
430 Ok((tmpl, fm))
431 }
432
433 #[cfg(feature = "std")]
440 pub(crate) fn from_cached(data: CachedTemplateData) -> Self {
441 let has_defaults = data
442 .declared_variables
443 .iter()
444 .any(|d| d.default_value.is_some());
445 let estimated_capacity = compiled::render::estimate_output_capacity(&data.segments);
446 Self {
447 body: String::new(),
448 name: data.name,
449 description: data.description,
450 segments: data.segments,
451 declared_variables: data.declared_variables,
452 base_dir: data.base_dir,
453 inline_templates: data.inline_templates,
454 source_hash: data.source_hash,
455 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
456 has_defaults,
457 consts: data.consts,
458 imported_consts: data.imported_consts,
459 estimated_capacity,
460 }
461 }
462
463 #[doc(hidden)]
465 #[must_use]
466 pub fn from_precompiled(data: &PrecompiledTemplateData<'_>) -> Self {
467 let inline_map = data
468 .inline_templates
469 .iter()
470 .map(|(k, v)| (k.to_string(), v.clone()))
471 .collect();
472 let const_map = data
473 .consts
474 .iter()
475 .map(|(k, v)| (k.to_string(), v.clone()))
476 .collect();
477 let imported_const_map = data
478 .imported_consts
479 .iter()
480 .map(|(k, v)| (k.to_string(), v.clone()))
481 .collect();
482 let has_defaults = data
483 .declared_variables
484 .iter()
485 .any(|d| d.default_value.is_some());
486 let segments: Arc<[Segment]> = Arc::from(data.segments);
487 let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
488 Self {
489 body: String::new(),
490 name: data.name.map(String::from),
491 description: data.description.map(String::from),
492 segments,
493 declared_variables: Arc::from(data.declared_variables),
494 #[cfg(feature = "std")]
495 base_dir: None,
496 inline_templates: Arc::new(inline_map),
497 source_hash: data.source_hash,
498 max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
499 has_defaults,
500 consts: Arc::new(const_map),
501 imported_consts: Arc::new(imported_const_map),
502 estimated_capacity,
503 }
504 }
505
506 fn validate_context(&self, ctx: &Context, allow_extra: bool) -> Result<(), TemplateError> {
519 let mut missing = Vec::new();
520 let mut mismatch: Option<(String, crate::types::TypeCheckError)> = None;
521 for decl in self.declared_variables.iter() {
522 match ctx.get(&decl.name) {
523 None => {
524 if decl.default_value.is_none() {
526 missing.push(decl.name.as_str());
527 }
528 }
529 Some(value) => {
530 if mismatch.is_none()
531 && let Err(e) = decl.var_type.check(value)
532 {
533 mismatch = Some((decl.name.clone(), e));
534 }
535 }
536 }
537 }
538 if !missing.is_empty() {
540 return Err(TemplateError::MissingParams(
541 missing.into_iter().map(String::from).collect(),
542 ));
543 }
544 if let Some((name, check_err)) = mismatch {
545 let detail = if check_err.path.is_empty() {
546 String::new()
547 } else {
548 format!(" (at .{})", check_err.path)
549 };
550 return Err(TemplateError::TypeMismatch {
551 name: format!("{name}{detail}"),
552 expected: check_err.expected,
553 actual: check_err.actual,
554 actual_value: check_err.actual_value,
555 });
556 }
557 if !allow_extra {
559 let mut declared: HashSet<&str> = self
560 .declared_variables
561 .iter()
562 .map(|d| d.name.as_str())
563 .collect();
564 for name in self.consts.keys() {
565 declared.insert(name.as_str());
566 }
567 let extra: Vec<String> = ctx
568 .values
569 .keys()
570 .filter(|k| !declared.contains(k.as_str()))
571 .cloned()
572 .collect();
573 if !extra.is_empty() {
574 return Err(TemplateError::ExtraParams(extra));
575 }
576 }
577 Ok(())
578 }
579
580 #[must_use]
582 pub fn defaults(&self) -> HashMap<String, crate::value::Value> {
583 self.declared_variables
584 .iter()
585 .filter_map(|d| {
586 d.default_value
587 .as_ref()
588 .map(|v| (d.name.clone(), v.clone()))
589 })
590 .collect()
591 }
592
593 #[must_use]
595 pub fn default(&self, name: &str) -> Option<&crate::value::Value> {
596 self.declared_variables
597 .iter()
598 .find(|d| d.name == name)
599 .and_then(|d| d.default_value.as_ref())
600 }
601
602 #[must_use]
621 pub fn defaults_context(&self) -> Context {
622 let defaults = self.defaults();
623 let mut ctx = Context::with_capacity(defaults.len());
624 for (k, v) in defaults {
625 ctx.set(k, v);
626 }
627 ctx
628 }
629
630 #[must_use]
634 pub fn body(&self) -> &str {
635 &self.body
636 }
637
638 #[must_use]
640 pub fn name(&self) -> Option<&str> {
641 self.name.as_deref()
642 }
643
644 #[must_use]
646 pub fn description(&self) -> Option<&str> {
647 self.description.as_deref()
648 }
649
650 pub fn set_max_include_depth(&mut self, depth: usize) {
652 self.max_include_depth = depth;
653 }
654
655 #[must_use]
657 pub fn with_max_include_depth(mut self, depth: usize) -> Self {
658 self.max_include_depth = depth;
659 self
660 }
661
662 #[must_use]
667 pub fn declarations(&self) -> &[VarDecl] {
668 &self.declared_variables
669 }
670
671 pub(crate) fn segments(&self) -> &[crate::compiled::Segment] {
672 &self.segments
673 }
674
675 #[cfg(feature = "std")]
677 #[must_use]
678 pub fn base_dir(&self) -> Option<&Path> {
679 self.base_dir.as_deref()
680 }
681
682 #[must_use]
706 pub fn consts(&self) -> Arc<HashMap<String, Value>> {
707 self.consts.clone()
708 }
709
710 #[must_use]
713 pub fn consts_ref(&self) -> &HashMap<String, Value> {
714 &self.consts
715 }
716
717 #[must_use]
722 pub fn imported_consts(&self) -> Arc<HashMap<String, Value>> {
723 self.imported_consts.clone()
724 }
725
726 #[must_use]
729 pub fn imported_consts_ref(&self) -> &HashMap<String, Value> {
730 &self.imported_consts
731 }
732
733 pub(crate) fn inline_templates(&self) -> &HashMap<String, CompiledInlineTemplate> {
734 &self.inline_templates
735 }
736
737 #[must_use]
744 pub fn source_hash(&self) -> u64 {
745 self.source_hash
746 }
747
748 pub fn validate_declarations(&self, expected: &[VarDecl]) -> Result<(), TemplateError> {
763 let current: HashMap<&str, &crate::types::VarType> = self
764 .declared_variables
765 .iter()
766 .map(|d| (d.name.as_str(), &d.var_type))
767 .collect();
768 let expected_map: HashMap<&str, &crate::types::VarType> = expected
769 .iter()
770 .map(|d| (d.name.as_str(), &d.var_type))
771 .collect();
772
773 let current_names: HashSet<&str> = current.keys().copied().collect();
774 let expected_names: HashSet<&str> = expected_map.keys().copied().collect();
775
776 let missing: Vec<&str> = expected_names.difference(¤t_names).copied().collect();
777 let extra: Vec<&str> = current_names.difference(&expected_names).copied().collect();
778
779 let retyped: Vec<String> = current_names
781 .intersection(&expected_names)
782 .filter_map(|name| {
783 let cur_type = current[name];
784 let exp_type = expected_map[name];
785 if cur_type == exp_type {
786 None
787 } else {
788 Some(format!("{name}: {exp_type} → {cur_type}"))
789 }
790 })
791 .collect();
792
793 if missing.is_empty() && extra.is_empty() && retyped.is_empty() {
794 return Ok(());
795 }
796
797 let mut parts = Vec::new();
798 if !missing.is_empty() {
799 parts.push(format!("removed: {}", missing.join(", ")));
800 }
801 if !extra.is_empty() {
802 parts.push(format!("added: {}", extra.join(", ")));
803 }
804 if !retyped.is_empty() {
805 parts.push(format!("retyped: {}", retyped.join(", ")));
806 }
807
808 Err(TemplateError::DeclarationsMutated {
809 details: parts.join("; "),
810 })
811 }
812
813 pub fn render_ctx(&self, ctx: &Context) -> Result<String, TemplateError> {
828 self.render_inner(ctx, false)
829 }
830
831 pub fn render_ctx_allowing_extra(&self, ctx: &Context) -> Result<String, TemplateError> {
843 self.render_inner(ctx, true)
844 }
845
846 pub fn render_empty(&self) -> Result<String, TemplateError> {
887 let ctx = if self.has_defaults {
888 self.defaults_context()
889 } else {
890 Context::new()
891 };
892 self.render_ctx(&ctx)
893 }
894
895 pub fn render_empty_into(&self, output: &mut String) -> Result<(), TemplateError> {
902 let ctx = if self.has_defaults {
903 self.defaults_context()
904 } else {
905 Context::new()
906 };
907 self.render_ctx_into(&ctx, output)
908 }
909
910 fn render_inner(&self, ctx: &Context, allow_extra: bool) -> Result<String, TemplateError> {
912 let mut output = String::with_capacity(self.estimated_capacity);
913 self.render_into_inner(ctx, allow_extra, &mut output)?;
914 Ok(output)
915 }
916
917 pub fn render_ctx_into(&self, ctx: &Context, output: &mut String) -> Result<(), TemplateError> {
928 self.render_into_inner(ctx, false, output)
929 }
930
931 pub fn render_ctx_into_allowing_extra(
939 &self,
940 ctx: &Context,
941 output: &mut String,
942 ) -> Result<(), TemplateError> {
943 self.render_into_inner(ctx, true, output)
944 }
945
946 fn render_into_inner(
948 &self,
949 ctx: &Context,
950 allow_extra: bool,
951 output: &mut String,
952 ) -> Result<(), TemplateError> {
953 self.validate_context(ctx, allow_extra)?;
954 self.render_core(ctx, output)
955 }
956
957 fn render_core(&self, ctx: &Context, output: &mut String) -> Result<(), TemplateError> {
962 let ctx = self.inject_defaults(ctx);
963 let mut scope = Scope::new(&ctx).with_max_include_depth(self.max_include_depth);
964 if !self.consts.is_empty() || !self.imported_consts.is_empty() {
966 scope.set_consts(&self.consts, &self.imported_consts);
967 }
968 scope.set_inline_templates(&self.inline_templates);
969 #[cfg(feature = "std")]
970 return compiled::render::render_segments_into(
971 &self.segments,
972 &mut scope,
973 self.base_dir.as_deref(),
974 output,
975 );
976 #[cfg(not(feature = "std"))]
977 return compiled::render_segments_into_no_std(&self.segments, &mut scope, output);
978 }
979
980 pub fn render_ctx_unchecked(&self, ctx: &Context) -> Result<String, TemplateError> {
995 let mut output = String::with_capacity(self.estimated_capacity);
996 self.render_core(ctx, &mut output)?;
997 Ok(output)
998 }
999
1000 pub fn render_ctx_into_unchecked(
1009 &self,
1010 ctx: &Context,
1011 output: &mut String,
1012 ) -> Result<(), TemplateError> {
1013 self.render_core(ctx, output)
1014 }
1015
1016 #[cfg(feature = "std")]
1028 pub fn render_ctx_cached<S: core::hash::BuildHasher + Send + Sync>(
1029 &self,
1030 ctx: &Context,
1031 cache: &crate::TemplateCache<S>,
1032 ) -> Result<String, TemplateError> {
1033 self.validate_context(ctx, false)?;
1034 let ctx = self.inject_defaults(ctx);
1035 let mut scope =
1036 Scope::with_cache(&ctx, cache).with_max_include_depth(self.max_include_depth);
1037 if !self.consts.is_empty() || !self.imported_consts.is_empty() {
1038 scope.set_consts(&self.consts, &self.imported_consts);
1039 }
1040 scope.set_inline_templates(&self.inline_templates);
1041 compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
1042 }
1043
1044 #[cfg(feature = "std")]
1054 pub fn render_ctx_cached_allowing_extra<S: core::hash::BuildHasher + Send + Sync>(
1055 &self,
1056 ctx: &Context,
1057 cache: &crate::TemplateCache<S>,
1058 ) -> Result<String, TemplateError> {
1059 self.validate_context(ctx, true)?;
1060 let ctx = self.inject_defaults(ctx);
1061 let mut scope =
1062 Scope::with_cache(&ctx, cache).with_max_include_depth(self.max_include_depth);
1063 if !self.consts.is_empty() || !self.imported_consts.is_empty() {
1064 scope.set_consts(&self.consts, &self.imported_consts);
1065 }
1066 scope.set_inline_templates(&self.inline_templates);
1067 compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
1068 }
1069
1070 fn inject_defaults<'a>(&self, ctx: &'a Context) -> alloc::borrow::Cow<'a, Context> {
1074 if !self.has_defaults {
1075 return alloc::borrow::Cow::Borrowed(ctx);
1076 }
1077 let mut owned: Option<Context> = None;
1078 for decl in self.declared_variables.iter() {
1079 if let Some(ref default) = decl.default_value {
1080 let effective = owned.as_ref().unwrap_or(ctx);
1081 if effective.get(&decl.name).is_none() {
1082 let ctx_mut = owned.get_or_insert_with(|| ctx.clone());
1083 ctx_mut.set(decl.name.clone(), default.clone());
1084 }
1085 }
1086 }
1087 match owned {
1088 Some(ctx) => alloc::borrow::Cow::Owned(ctx),
1089 None => alloc::borrow::Cow::Borrowed(ctx),
1090 }
1091 }
1092}
1093
1094#[cfg(feature = "serde")]
1095impl Template {
1096 pub fn render<T: serde::Serialize>(
1134 &self,
1135 value: &T,
1136 ) -> Result<String, crate::error::TemplateError> {
1137 let ctx = Context::from_serialize(value)?;
1138 self.render_ctx(&ctx)
1139 }
1140
1141 pub fn render_into<T: serde::Serialize>(
1149 &self,
1150 value: &T,
1151 output: &mut String,
1152 ) -> Result<(), crate::error::TemplateError> {
1153 let ctx = Context::from_serialize(value)?;
1154 self.render_ctx_into(&ctx, output)
1155 }
1156}
1157
1158impl PartialEq for Template {
1171 fn eq(&self, other: &Self) -> bool {
1172 self.source_hash == other.source_hash
1173 }
1174}
1175
1176impl Eq for Template {}
1177
1178#[cfg(feature = "serde")]
1184impl serde::Serialize for Template {
1185 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1186 serializer.serialize_str(&format!("template:{:016x}", self.source_hash))
1187 }
1188}
1189
1190#[cfg(feature = "serde")]
1196impl<'de> serde::Deserialize<'de> for Template {
1197 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1198 let _ = <serde::de::IgnoredAny as serde::Deserialize>::deserialize(deserializer)?;
1199 Err(serde::de::Error::custom(
1200 "Template cannot be deserialized; construct from source with \
1201 Template::from_source() or Template::from_file()",
1202 ))
1203 }
1204}
1205
1206#[cfg(feature = "std")]
1214pub fn load_template(dir: &Path, name: &str) -> Result<Template, TemplateError> {
1215 let path = dir.join(format!("{name}.tmpl.md"));
1216 Template::from_file(&path)
1217}
1218fn inject_enum_type_constants(
1230 type_aliases: &HashMap<String, VarType>,
1231 consts: &mut HashMap<String, Value>,
1232) {
1233 for (type_name, var_type) in type_aliases {
1234 let VarType::Enum(variants) = var_type else {
1235 continue;
1236 };
1237 if consts.contains_key(type_name) {
1239 continue;
1240 }
1241 let mut variant_map = HashMap::new();
1242 for variant in variants {
1243 if variant.fields.is_empty() {
1244 variant_map.insert(variant.name.clone(), Value::Str(variant.name.clone()));
1246 } else {
1247 let mut partial = HashMap::new();
1249 partial.insert(
1250 crate::consts::ENUM_TAG_KEY.into(),
1251 Value::Str(variant.name.clone()),
1252 );
1253 variant_map.insert(variant.name.clone(), Value::Struct(Arc::new(partial)));
1254 }
1255 }
1256 consts.insert(type_name.clone(), Value::Struct(Arc::new(variant_map)));
1257 }
1258}
1259
1260fn collect_enum_type_keys(fm: &Frontmatter) -> HashSet<String> {
1267 let mut keys = HashSet::new();
1268 for (name, ty) in &fm.type_aliases {
1270 if matches!(ty, VarType::Enum(_)) {
1271 keys.insert(name.clone());
1272 }
1273 }
1274 for key in &fm.imported_enum_type_keys {
1276 keys.insert(key.clone());
1277 }
1278 keys
1279}
1280
1281fn check_bare_enum_access(
1290 segments: &[compiled::Segment],
1291 enum_keys: &HashSet<String>,
1292) -> Result<(), TemplateError> {
1293 for seg in segments {
1294 match seg {
1295 compiled::Segment::Expr {
1296 expr: compiled::CompiledExpr::Path(path),
1297 ..
1298 } => {
1299 let parts = path.parts();
1300 if parts.len() >= 2 && is_enum_path(parts, enum_keys) {
1301 return Err(TemplateError::syntax(format!(
1302 "bare enum literal '{}' is not allowed — \
1303 use kind({}) to get the variant name as a string",
1304 path.as_str(),
1305 path.as_str(),
1306 )));
1307 }
1308 }
1309 compiled::Segment::ForLoop { body, .. } => {
1310 check_bare_enum_access(body, enum_keys)?;
1311 }
1312 compiled::Segment::If {
1313 branches,
1314 else_body,
1315 } => {
1316 for (_, branch_body) in branches {
1317 check_bare_enum_access(branch_body, enum_keys)?;
1318 }
1319 check_bare_enum_access(else_body, enum_keys)?;
1320 }
1321 compiled::Segment::Match { arms, .. } => {
1322 for (_, arm_body) in arms {
1323 check_bare_enum_access(arm_body, enum_keys)?;
1324 }
1325 }
1326 compiled::Segment::Include(inc) => {
1327 if let Some(ref inline) = inc.inline_compiled {
1328 check_bare_enum_access(&inline.segments, enum_keys)?;
1329 }
1330 }
1331 _ => {}
1332 }
1333 }
1334 Ok(())
1335}
1336
1337fn is_enum_path(parts: &[String], enum_keys: &HashSet<String>) -> bool {
1342 if enum_keys.contains(&parts[0]) {
1344 return true;
1345 }
1346 if parts.len() >= 3 {
1348 let key = format!("{}.{}", parts[0], parts[1]);
1349 if enum_keys.contains(&key) {
1350 return true;
1351 }
1352 }
1353 false
1354}
1355
1356fn check_undeclared_variables(
1362 referenced: &HashSet<String>,
1363 fm: &Frontmatter,
1364 inline_templates: &HashMap<String, CompiledInlineTemplate>,
1365) -> Result<(), TemplateError> {
1366 let mut declared: HashSet<String> = fm.params.iter().cloned().collect();
1367 for c in &fm.consts {
1368 declared.insert(c.name.clone());
1369 }
1370 for import in &fm.imports {
1371 declared.insert(import.stem.clone());
1372 }
1373 for (name, ty) in &fm.type_aliases {
1376 if matches!(ty, VarType::Enum(_)) {
1377 declared.insert(name.clone());
1378 }
1379 }
1380 for inline_name in inline_templates.keys() {
1383 declared.insert(inline_name.clone());
1384 }
1385
1386 let undeclared: Vec<&String> = referenced
1387 .iter()
1388 .filter(|v| !declared.contains(v.as_str()))
1389 .collect();
1390 if undeclared.is_empty() {
1391 return Ok(());
1392 }
1393
1394 let mut names: Vec<&str> = undeclared.iter().map(|s| s.as_str()).collect();
1395 names.sort_unstable();
1396
1397 let mut suggestions = Vec::new();
1399 for name in &names {
1400 let mut best: Option<(&str, usize)> = None;
1401 for candidate in &declared {
1402 let dist = crate::error::levenshtein_distance(name, candidate);
1403 if dist > 0 && dist <= 2 && best.is_none_or(|b| dist < b.1) {
1404 best = Some((candidate, dist));
1405 }
1406 }
1407 if let Some((suggestion, _)) = best {
1408 suggestions.push(format!("'{name}' (did you mean '{suggestion}'?)"));
1409 }
1410 }
1411 let suffix = if suggestions.is_empty() {
1412 String::new()
1413 } else {
1414 format!(". Suggestions: {}", suggestions.join(", "))
1415 };
1416 Err(TemplateError::syntax(format!(
1417 "{}{}{suffix}",
1418 crate::consts::ERR_UNDECLARED_PREFIX,
1419 names.join(", ")
1420 )))
1421}
1422
1423fn check_unused_params(
1427 declarations: &[VarDecl],
1428 referenced: &HashSet<String>,
1429 allow_unused: bool,
1430) -> Result<(), TemplateError> {
1431 if allow_unused {
1432 return Ok(());
1433 }
1434 let unused: Vec<&str> = declarations
1435 .iter()
1436 .filter(|decl| !referenced.contains(&decl.name))
1437 .map(|decl| decl.name.as_str())
1438 .collect();
1439 if unused.is_empty() {
1440 return Ok(());
1441 }
1442 Err(TemplateError::syntax(format!(
1443 "unused declared parameter(s): {}. Reference them in the template body, \
1444 in a {{# comment #}}, or remove them from the frontmatter `params:` list. \
1445 To suppress this check, add `allow_unused: true` to the frontmatter",
1446 unused.join(", ")
1447 )))
1448}
1449
1450fn check_name_collisions(
1457 fm: &Frontmatter,
1458 inline_templates: &HashMap<String, CompiledInlineTemplate>,
1459 segments: &[Segment],
1460) -> Result<(), TemplateError> {
1461 for import in &fm.imports {
1463 if inline_templates.contains_key(&import.stem) {
1464 return Err(TemplateError::syntax(format!(
1465 "import stem '{}' conflicts with inline template name",
1466 import.stem
1467 )));
1468 }
1469 }
1470
1471 let param_and_const_names: HashSet<&str> = fm
1475 .params
1476 .iter()
1477 .map(String::as_str)
1478 .chain(fm.consts.iter().map(|c| c.name.as_str()))
1479 .collect();
1480 for inline_name in inline_templates.keys() {
1481 if param_and_const_names.contains(inline_name.as_str()) {
1482 return Err(TemplateError::syntax(format!(
1483 "inline template name '{inline_name}' conflicts with a declared parameter or constant"
1484 )));
1485 }
1486 }
1487
1488 let protected_names: HashSet<&str> = fm
1490 .params
1491 .iter()
1492 .map(String::as_str)
1493 .chain(fm.consts.iter().map(|c| c.name.as_str()))
1494 .chain(fm.imports.iter().map(|i| i.stem.as_str()))
1495 .chain(inline_templates.keys().map(String::as_str))
1496 .collect();
1497 validate_for_bindings(segments, &protected_names)
1498}
1499
1500fn validate_for_bindings(
1506 segments: &[crate::compiled::Segment],
1507 protected: &HashSet<&str>,
1508) -> Result<(), TemplateError> {
1509 use crate::compiled::Segment;
1510 for seg in segments {
1511 match seg {
1512 Segment::ForLoop { binding, body, .. } => {
1513 if protected.contains(binding.as_ref()) {
1514 return Err(TemplateError::syntax(format!(
1515 "{} declared name '{binding}'",
1516 crate::consts::ERR_FOR_BINDING_SHADOWS,
1517 )));
1518 }
1519 validate_for_bindings(body, protected)?;
1520 }
1521 Segment::If {
1522 branches,
1523 else_body,
1524 } => {
1525 for (_cond, branch_body) in branches {
1526 validate_for_bindings(branch_body, protected)?;
1527 }
1528 validate_for_bindings(else_body, protected)?;
1529 }
1530 Segment::Match { arms, .. } => {
1531 for (_variants, arm_body) in arms {
1532 validate_for_bindings(arm_body, protected)?;
1533 }
1534 }
1535 _ => {}
1536 }
1537 }
1538 Ok(())
1539}
1540
1541#[cfg(not(feature = "std"))]
1545fn hash_source_no_std(source: &str) -> u64 {
1546 crate::__private::fnv1a_hash(source.as_bytes())
1547}
1548
1549#[cfg(all(test, feature = "std"))]
1550mod adversarial_tests;
1551#[cfg(all(test, feature = "std"))]
1552mod collision_and_scope_tests;
1553#[cfg(all(test, feature = "std"))]
1554mod const_tests;
1555#[cfg(all(test, feature = "std"))]
1556mod error_diagnostic_tests;
1557#[cfg(all(test, feature = "std"))]
1558mod higher_order_tests;
1559#[cfg(all(test, feature = "std"))]
1560mod inline_edge_tests;
1561#[cfg(all(test, feature = "std"))]
1562mod render_integration_tests;
1563#[cfg(all(test, feature = "std"))]
1564mod shared_tests;
1565#[cfg(all(test, feature = "std"))]
1566mod tests;