Skip to main content

md_tmpl_core/template/
render_methods.rs

1use alloc::string::String;
2
3use crate::{Template, compiled, context::Context, error::TemplateError, scope::Scope};
4
5impl Template {
6    /// Render the template with the given context (strict mode).
7    ///
8    /// Validates the context against frontmatter declarations:
9    /// - Missing declared parameters → error
10    /// - Type mismatches → error
11    /// - Extra undeclared parameters → error
12    ///
13    /// Use [`render_ctx_allowing_extra`](Self::render_ctx_allowing_extra) to permit
14    /// undeclared parameters (e.g. when sharing a context across templates).
15    ///
16    /// # Errors
17    ///
18    /// Returns [`TemplateError`] if validation fails or a rendering error
19    /// occurs.
20    pub fn render_ctx(&self, ctx: &Context) -> Result<String, TemplateError> {
21        self.render_inner(ctx, false)
22    }
23
24    /// Render the template, allowing extra (undeclared) parameters.
25    ///
26    /// Like [`render_ctx`](Self::render_ctx), but extra context keys that aren't
27    /// declared in frontmatter are silently ignored instead of producing
28    /// an error. Useful when forwarding a shared context to multiple
29    /// templates.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`TemplateError`] if validation fails or a rendering error
34    /// occurs.
35    pub fn render_ctx_allowing_extra(&self, ctx: &Context) -> Result<String, TemplateError> {
36        self.render_inner(ctx, true)
37    }
38
39    /// Render a template that takes no user-provided parameters.
40    ///
41    /// If the template declares parameters, those **must** all have defaults.
42    /// Calling `render_empty()` on a template with required (no-default)
43    /// parameters returns [`TemplateError::MissingParams`].
44    ///
45    /// This is more efficient than `render(&empty_struct)` (no serde overhead)
46    /// and more explicit than `render_ctx(&Context::new())`.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use md_tmpl_core::Template;
52    ///
53    /// // No params — renders as-is
54    /// let tmpl = Template::from_source(
55    ///     r#"---
56    /// params: []
57    /// ---
58    /// Hello world!"#,
59    /// )
60    /// .unwrap();
61    /// assert_eq!(tmpl.render_empty().unwrap(), "Hello world!");
62    ///
63    /// // All params have defaults
64    /// let tmpl = Template::from_source(
65    ///     r#"---
66    /// params:
67    ///   - greeting = str := "Hi"
68    /// ---
69    /// {{ greeting }}!"#,
70    /// )
71    /// .unwrap();
72    /// assert_eq!(tmpl.render_empty().unwrap(), "Hi!");
73    /// ```
74    ///
75    /// # Errors
76    ///
77    /// Returns [`TemplateError::MissingParams`] if any declared parameter
78    /// lacks a default value.
79    pub fn render_empty(&self) -> Result<String, TemplateError> {
80        let ctx = if self.has_defaults {
81            self.defaults_context()
82        } else {
83            Context::new()
84        };
85        self.render_ctx(&ctx)
86    }
87
88    /// Like [`render_empty`](Self::render_empty), but appends to an existing buffer.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`TemplateError::MissingParams`] if any declared parameter
93    /// lacks a default value.
94    pub fn render_empty_into(&self, output: &mut String) -> Result<(), TemplateError> {
95        let ctx = if self.has_defaults {
96            self.defaults_context()
97        } else {
98            Context::new()
99        };
100        self.render_ctx_into(&ctx, output)
101    }
102
103    /// Internal render path with configurable strictness.
104    fn render_inner(&self, ctx: &Context, allow_extra: bool) -> Result<String, TemplateError> {
105        let mut output = String::with_capacity(self.estimated_capacity);
106        self.render_into_inner(ctx, allow_extra, &mut output)?;
107        Ok(output)
108    }
109
110    /// Render the template directly into an existing `String` buffer.
111    ///
112    /// Unlike [`render_ctx`](Self::render_ctx), this appends to `output` without
113    /// allocating a new `String`. Useful when composing multiple template
114    /// outputs into a single buffer.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`TemplateError`] if validation fails or a rendering error
119    /// occurs. On error, `output` may contain partial results.
120    pub fn render_ctx_into(&self, ctx: &Context, output: &mut String) -> Result<(), TemplateError> {
121        self.render_into_inner(ctx, false, output)
122    }
123
124    /// Like [`render_ctx_into`](Self::render_ctx_into), but allows extra (undeclared)
125    /// parameters.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`TemplateError`] if validation fails or a rendering error
130    /// occurs.
131    pub fn render_ctx_into_allowing_extra(
132        &self,
133        ctx: &Context,
134        output: &mut String,
135    ) -> Result<(), TemplateError> {
136        self.render_into_inner(ctx, true, output)
137    }
138
139    /// Shared implementation for all render-into paths.
140    fn render_into_inner(
141        &self,
142        ctx: &Context,
143        allow_extra: bool,
144        output: &mut String,
145    ) -> Result<(), TemplateError> {
146        self.validate_context(ctx, allow_extra)?;
147        self.render_core(ctx, output)
148    }
149
150    /// Core rendering without any context validation.
151    ///
152    /// Used by both `render_into_inner` (after validation) and
153    /// `render_ctx_unchecked` (no validation at all).
154    fn render_core(&self, ctx: &Context, output: &mut String) -> Result<(), TemplateError> {
155        let ctx = self.inject_defaults(ctx);
156        let mut scope = Scope::new(&ctx)
157            .with_max_include_depth(self.max_include_depth)
158            .with_declarations(&self.declared_variables);
159        if !self.consts.is_empty() || !self.imported_consts.is_empty() {
160            scope.set_consts(&self.consts, &self.imported_consts);
161        }
162        scope.set_inline_templates(&self.inline_templates);
163        #[cfg(feature = "std")]
164        if !self.env_values.is_empty() {
165            scope.set_compile_env(self.env_values.clone());
166        }
167        #[cfg(feature = "std")]
168        return compiled::render::render_segments_into(
169            &self.segments,
170            &mut scope,
171            self.base_dir.as_deref(),
172            output,
173        );
174        #[cfg(not(feature = "std"))]
175        return compiled::render_segments_into_no_std(&self.segments, &mut scope, output);
176    }
177
178    /// Render the template **without** context validation.
179    ///
180    /// Skips the parameter presence, type, and extra-key checks that
181    /// [`render_ctx`](Self::render_ctx) performs on every call. This is a safe
182    /// operation — rendering errors (e.g. undefined variable) are still
183    /// reported via `Err` — but the upfront validation overhead is removed.
184    ///
185    /// Use this when the context is known-good (e.g. constructed from a
186    /// strongly-typed params struct, or pre-validated once at startup).
187    ///
188    /// # Errors
189    ///
190    /// Returns [`TemplateError`] if a rendering error occurs (e.g.
191    /// undefined variable, filter error).
192    pub fn render_ctx_unchecked(&self, ctx: &Context) -> Result<String, TemplateError> {
193        let mut output = String::with_capacity(self.estimated_capacity);
194        self.render_core(ctx, &mut output)?;
195        Ok(output)
196    }
197
198    /// Render into a buffer **without** context validation.
199    ///
200    /// Like [`render_ctx_unchecked`](Self::render_ctx_unchecked), but appends to
201    /// an existing buffer.
202    ///
203    /// # Errors
204    ///
205    /// Returns [`TemplateError`] if a rendering error occurs.
206    pub fn render_ctx_into_unchecked(
207        &self,
208        ctx: &Context,
209        output: &mut String,
210    ) -> Result<(), TemplateError> {
211        self.render_core(ctx, output)
212    }
213
214    /// Render the template using a [`TemplateCache`](crate::TemplateCache) for include resolution.
215    ///
216    /// Like [`render_ctx`](Self::render_ctx), but included templates are resolved
217    /// through the cache — unchanged includes are not re-read or re-compiled.
218    /// This is the recommended rendering path for hot-reload scenarios where
219    /// templates are re-rendered frequently.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`TemplateError`] if validation fails or a rendering error
224    /// occurs.
225    #[cfg(feature = "std")]
226    pub fn render_ctx_cached<S: core::hash::BuildHasher + Send + Sync>(
227        &self,
228        ctx: &Context,
229        cache: &crate::TemplateCache<S>,
230    ) -> Result<String, TemplateError> {
231        self.validate_context(ctx, false)?;
232        let ctx = self.inject_defaults(ctx);
233        let mut scope = Scope::with_cache(&ctx, cache)
234            .with_max_include_depth(self.max_include_depth)
235            .with_declarations(&self.declared_variables);
236        if !self.consts.is_empty() || !self.imported_consts.is_empty() {
237            scope.set_consts(&self.consts, &self.imported_consts);
238        }
239        scope.set_inline_templates(&self.inline_templates);
240        if !self.env_values.is_empty() {
241            scope.set_compile_env(self.env_values.clone());
242        }
243        compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
244    }
245
246    /// Render with caching, allowing extra parameters in the context.
247    ///
248    /// Like [`render_ctx_cached()`](Self::render_ctx_cached) but does not
249    /// reject parameters not declared in the template frontmatter.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`TemplateError`] if validation fails or a rendering error
254    /// occurs.
255    #[cfg(feature = "std")]
256    pub fn render_ctx_cached_allowing_extra<S: core::hash::BuildHasher + Send + Sync>(
257        &self,
258        ctx: &Context,
259        cache: &crate::TemplateCache<S>,
260    ) -> Result<String, TemplateError> {
261        self.validate_context(ctx, true)?;
262        let ctx = self.inject_defaults(ctx);
263        let mut scope = Scope::with_cache(&ctx, cache)
264            .with_max_include_depth(self.max_include_depth)
265            .with_declarations(&self.declared_variables);
266        if !self.consts.is_empty() || !self.imported_consts.is_empty() {
267            scope.set_consts(&self.consts, &self.imported_consts);
268        }
269        scope.set_inline_templates(&self.inline_templates);
270        if !self.env_values.is_empty() {
271            scope.set_compile_env(self.env_values.clone());
272        }
273        compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
274    }
275
276    /// Inject default values for any declared params not present in `ctx`.
277    ///
278    /// Returns a `Cow::Borrowed` if no defaults needed, avoiding allocation.
279    fn inject_defaults<'a>(&self, ctx: &'a Context) -> alloc::borrow::Cow<'a, Context> {
280        if !self.has_defaults {
281            return alloc::borrow::Cow::Borrowed(ctx);
282        }
283        let mut owned: Option<Context> = None;
284        for decl in self.declared_variables.iter() {
285            if let Some(ref default) = decl.default_value {
286                let effective = owned.as_ref().unwrap_or(ctx);
287                if effective.get(&decl.name).is_none() {
288                    let ctx_mut = owned.get_or_insert_with(|| ctx.clone());
289                    ctx_mut.set(decl.name.clone(), default.clone());
290                }
291            }
292        }
293        match owned {
294            Some(ctx) => alloc::borrow::Cow::Owned(ctx),
295            None => alloc::borrow::Cow::Borrowed(ctx),
296        }
297    }
298}
299
300#[cfg(feature = "serde")]
301impl Template {
302    /// Render the template from any `Serialize` struct.
303    ///
304    /// Struct fields become template variables — no manual `Context`
305    /// construction needed.
306    ///
307    /// On the **first** call with a given `T`, the context is fully validated
308    /// against frontmatter declarations. If validation passes,
309    /// `TypeId::of::<T>()` is cached so that subsequent calls with the
310    /// **same concrete type** skip validation entirely. This gives the
311    /// safety of runtime type-checking with near-zero amortized cost.
312    ///
313    /// # Errors
314    ///
315    /// Returns [`TemplateError`] if serialization fails, the value is not a
316    /// struct/map, or rendering encounters an error.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use md_tmpl_core::Template;
322    /// use serde::Serialize;
323    ///
324    /// #[derive(Serialize)]
325    /// struct Data {
326    ///     name: String,
327    ///     count: i64,
328    /// }
329    ///
330    /// let tmpl = Template::from_source(
331    ///     r#"---
332    /// params: [name = str, count = int]
333    /// ---
334    /// {{ name }} has {{ count }} items"#,
335    /// )
336    /// .unwrap();
337    /// let output = tmpl
338    ///     .render(&Data {
339    ///         name: "Alice".into(),
340    ///         count: 3,
341    ///     })
342    ///     .unwrap();
343    /// assert_eq!(output, "Alice has 3 items");
344    /// ```
345    pub fn render<T: serde::Serialize + 'static>(
346        &self,
347        value: &T,
348    ) -> Result<String, crate::error::TemplateError> {
349        let ctx = Context::from_serialize(value)?;
350        let mut output = String::with_capacity(self.estimated_capacity);
351        self.render_into_checked(core::any::TypeId::of::<T>(), &ctx, &mut output)?;
352        Ok(output)
353    }
354
355    /// Like [`render`](Self::render), but appends output into an
356    /// existing buffer.
357    ///
358    /// # Errors
359    ///
360    /// Returns [`TemplateError`] if serialization fails, the value is not a
361    /// struct/map, or rendering encounters an error.
362    pub fn render_into<T: serde::Serialize + 'static>(
363        &self,
364        value: &T,
365        output: &mut String,
366    ) -> Result<(), crate::error::TemplateError> {
367        let ctx = Context::from_serialize(value)?;
368        self.render_into_checked(core::any::TypeId::of::<T>(), &ctx, output)
369    }
370
371    /// Render-into with `TypeId`-based validation caching.
372    ///
373    /// If `type_id` has been validated before, skips `validate_context` and
374    /// goes straight to `render_core`. The cache itself requires `std` (it
375    /// uses a `Mutex`); `no_std` builds validate on every call.
376    #[cfg(feature = "std")]
377    fn render_into_checked(
378        &self,
379        type_id: core::any::TypeId,
380        ctx: &Context,
381        output: &mut String,
382    ) -> Result<(), crate::error::TemplateError> {
383        let already_checked = self
384            .checked_type_ids
385            .lock()
386            .unwrap_or_else(std::sync::PoisonError::into_inner)
387            .contains(&type_id);
388
389        if already_checked {
390            // Type has been validated before — skip straight to render.
391            return self.render_core(ctx, output);
392        }
393        // First time seeing this type — validate, then cache on success.
394        self.validate_context(ctx, false)?;
395        self.checked_type_ids
396            .lock()
397            .unwrap_or_else(std::sync::PoisonError::into_inner)
398            .push(type_id);
399        self.render_core(ctx, output)
400    }
401
402    /// `no_std` variant: no cross-call cache (that needs a `Mutex`), so the
403    /// `type_id` is unused and the context is validated on every call.
404    #[cfg(not(feature = "std"))]
405    fn render_into_checked(
406        &self,
407        _type_id: core::any::TypeId,
408        ctx: &Context,
409        output: &mut String,
410    ) -> Result<(), crate::error::TemplateError> {
411        self.validate_context(ctx, false)?;
412        self.render_core(ctx, output)
413    }
414}