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}