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        return compiled::render::render_segments_into(
165            &self.segments,
166            &mut scope,
167            self.base_dir.as_deref(),
168            output,
169        );
170        #[cfg(not(feature = "std"))]
171        return compiled::render_segments_into_no_std(&self.segments, &mut scope, output);
172    }
173
174    /// Render the template **without** context validation.
175    ///
176    /// Skips the parameter presence, type, and extra-key checks that
177    /// [`render_ctx`](Self::render_ctx) performs on every call. This is a safe
178    /// operation — rendering errors (e.g. undefined variable) are still
179    /// reported via `Err` — but the upfront validation overhead is removed.
180    ///
181    /// Use this when the context is known-good (e.g. constructed from a
182    /// strongly-typed params struct, or pre-validated once at startup).
183    ///
184    /// # Errors
185    ///
186    /// Returns [`TemplateError`] if a rendering error occurs (e.g.
187    /// undefined variable, filter error).
188    pub fn render_ctx_unchecked(&self, ctx: &Context) -> Result<String, TemplateError> {
189        let mut output = String::with_capacity(self.estimated_capacity);
190        self.render_core(ctx, &mut output)?;
191        Ok(output)
192    }
193
194    /// Render into a buffer **without** context validation.
195    ///
196    /// Like [`render_ctx_unchecked`](Self::render_ctx_unchecked), but appends to
197    /// an existing buffer.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`TemplateError`] if a rendering error occurs.
202    pub fn render_ctx_into_unchecked(
203        &self,
204        ctx: &Context,
205        output: &mut String,
206    ) -> Result<(), TemplateError> {
207        self.render_core(ctx, output)
208    }
209
210    /// Render the template using a [`TemplateCache`](crate::TemplateCache) for include resolution.
211    ///
212    /// Like [`render_ctx`](Self::render_ctx), but included templates are resolved
213    /// through the cache — unchanged includes are not re-read or re-compiled.
214    /// This is the recommended rendering path for hot-reload scenarios where
215    /// templates are re-rendered frequently.
216    ///
217    /// # Errors
218    ///
219    /// Returns [`TemplateError`] if validation fails or a rendering error
220    /// occurs.
221    #[cfg(feature = "std")]
222    pub fn render_ctx_cached<S: core::hash::BuildHasher + Send + Sync>(
223        &self,
224        ctx: &Context,
225        cache: &crate::TemplateCache<S>,
226    ) -> Result<String, TemplateError> {
227        self.validate_context(ctx, false)?;
228        let ctx = self.inject_defaults(ctx);
229        let mut scope = Scope::with_cache(&ctx, cache)
230            .with_max_include_depth(self.max_include_depth)
231            .with_declarations(&self.declared_variables);
232        if !self.consts.is_empty() || !self.imported_consts.is_empty() {
233            scope.set_consts(&self.consts, &self.imported_consts);
234        }
235        scope.set_inline_templates(&self.inline_templates);
236        compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
237    }
238
239    /// Render with caching, allowing extra parameters in the context.
240    ///
241    /// Like [`render_ctx_cached()`](Self::render_ctx_cached) but does not
242    /// reject parameters not declared in the template frontmatter.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`TemplateError`] if validation fails or a rendering error
247    /// occurs.
248    #[cfg(feature = "std")]
249    pub fn render_ctx_cached_allowing_extra<S: core::hash::BuildHasher + Send + Sync>(
250        &self,
251        ctx: &Context,
252        cache: &crate::TemplateCache<S>,
253    ) -> Result<String, TemplateError> {
254        self.validate_context(ctx, true)?;
255        let ctx = self.inject_defaults(ctx);
256        let mut scope = Scope::with_cache(&ctx, cache)
257            .with_max_include_depth(self.max_include_depth)
258            .with_declarations(&self.declared_variables);
259        if !self.consts.is_empty() || !self.imported_consts.is_empty() {
260            scope.set_consts(&self.consts, &self.imported_consts);
261        }
262        scope.set_inline_templates(&self.inline_templates);
263        compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
264    }
265
266    /// Inject default values for any declared params not present in `ctx`.
267    ///
268    /// Returns a `Cow::Borrowed` if no defaults needed, avoiding allocation.
269    fn inject_defaults<'a>(&self, ctx: &'a Context) -> alloc::borrow::Cow<'a, Context> {
270        if !self.has_defaults {
271            return alloc::borrow::Cow::Borrowed(ctx);
272        }
273        let mut owned: Option<Context> = None;
274        for decl in self.declared_variables.iter() {
275            if let Some(ref default) = decl.default_value {
276                let effective = owned.as_ref().unwrap_or(ctx);
277                if effective.get(&decl.name).is_none() {
278                    let ctx_mut = owned.get_or_insert_with(|| ctx.clone());
279                    ctx_mut.set(decl.name.clone(), default.clone());
280                }
281            }
282        }
283        match owned {
284            Some(ctx) => alloc::borrow::Cow::Owned(ctx),
285            None => alloc::borrow::Cow::Borrowed(ctx),
286        }
287    }
288}
289
290#[cfg(feature = "serde")]
291impl Template {
292    /// Render the template from any `Serialize` struct.
293    ///
294    /// Struct fields become template variables — no manual `Context`
295    /// construction needed.
296    ///
297    /// On the **first** call with a given `T`, the context is fully validated
298    /// against frontmatter declarations. If validation passes,
299    /// `TypeId::of::<T>()` is cached so that subsequent calls with the
300    /// **same concrete type** skip validation entirely. This gives the
301    /// safety of runtime type-checking with near-zero amortized cost.
302    ///
303    /// # Errors
304    ///
305    /// Returns [`TemplateError`] if serialization fails, the value is not a
306    /// struct/map, or rendering encounters an error.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use md_tmpl_core::Template;
312    /// use serde::Serialize;
313    ///
314    /// #[derive(Serialize)]
315    /// struct Data {
316    ///     name: String,
317    ///     count: i64,
318    /// }
319    ///
320    /// let tmpl = Template::from_source(
321    ///     r#"---
322    /// params: [name = str, count = int]
323    /// ---
324    /// {{ name }} has {{ count }} items"#,
325    /// )
326    /// .unwrap();
327    /// let output = tmpl
328    ///     .render(&Data {
329    ///         name: "Alice".into(),
330    ///         count: 3,
331    ///     })
332    ///     .unwrap();
333    /// assert_eq!(output, "Alice has 3 items");
334    /// ```
335    pub fn render<T: serde::Serialize + 'static>(
336        &self,
337        value: &T,
338    ) -> Result<String, crate::error::TemplateError> {
339        let ctx = Context::from_serialize(value)?;
340        self.render_typed::<T>(&ctx)
341    }
342
343    /// Like [`render`](Self::render), but appends output into an
344    /// existing buffer.
345    ///
346    /// # Errors
347    ///
348    /// Returns [`TemplateError`] if serialization fails, the value is not a
349    /// struct/map, or rendering encounters an error.
350    pub fn render_into<T: serde::Serialize + 'static>(
351        &self,
352        value: &T,
353        output: &mut String,
354    ) -> Result<(), crate::error::TemplateError> {
355        let ctx = Context::from_serialize(value)?;
356        self.render_into_typed::<T>(&ctx, output)
357    }
358
359    /// Render with TypeId-based validation caching.
360    ///
361    /// If `T` has been validated before (`TypeId` is cached), skips
362    /// `validate_context` and goes straight to `render_core`.
363    fn render_typed<T: 'static>(
364        &self,
365        ctx: &Context,
366    ) -> Result<String, crate::error::TemplateError> {
367        let mut output = String::with_capacity(self.estimated_capacity);
368        self.render_into_typed::<T>(ctx, &mut output)?;
369        Ok(output)
370    }
371
372    /// Render-into with TypeId-based validation caching.
373    fn render_into_typed<T: 'static>(
374        &self,
375        ctx: &Context,
376        output: &mut String,
377    ) -> Result<(), crate::error::TemplateError> {
378        #[cfg(feature = "std")]
379        {
380            let type_id = core::any::TypeId::of::<T>();
381            let already_checked = self
382                .checked_type_ids
383                .lock()
384                .unwrap_or_else(std::sync::PoisonError::into_inner)
385                .contains(&type_id);
386
387            if already_checked {
388                // Type has been validated before — skip straight to render.
389                return self.render_core(ctx, output);
390            }
391            // First time seeing this type — validate, then cache on success.
392            self.validate_context(ctx, false)?;
393            self.checked_type_ids
394                .lock()
395                .unwrap_or_else(std::sync::PoisonError::into_inner)
396                .push(type_id);
397            self.render_core(ctx, output)
398        }
399        #[cfg(not(feature = "std"))]
400        {
401            self.validate_context(ctx, false)?;
402            self.render_core(ctx, output)
403        }
404    }
405}