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}