Skip to main content

typed_handlebars/
lib.rs

1//! Compile-time checked [Handlebars](https://handlebarsjs.com/) templates for Rust.
2//!
3//! Your `.hbs` files are turned into Rust when the crate is built, so there is no parsing, no
4//! template registry and no lookups at run time. The types a template needs are generated from
5//! what the template itself says, so there is nothing to declare, derive or implement.
6//!
7//! See the [project README](https://github.com/paultuckey/typed-handlebars#readme) for the goals,
8//! the full table of supported Handlebars constructs, and worked examples of partials and nesting.
9//!
10//! # Example
11//!
12//! Given `templates/button.hbs`:
13//!
14//! ```handlebars
15//! <button id="btn{{ btn_id }}">{{ btn_name }}</button>
16//! ```
17//!
18//! [`directory!`] turns each file into a module holding a `Vars` — every variable the template
19//! uses, named. It is an ordinary struct, so you write it as a literal:
20//!
21//! ```
22//! mod templates {
23//!     // The README uses "templates/"; this crate keeps its doc fixtures here.
24//!     typed_handlebars::directory!("doc-templates/");
25//! }
26//!
27//! assert_eq!(
28//!     templates::button::Vars { btn_id: 42, btn_name: "Save" }.render(),
29//!     r#"<button id="btn42">Save</button>"#
30//! );
31//! ```
32//!
33//! Nothing depends on argument order, your IDE offers the names, and the compiler checks them: a
34//! misspelled field names the one you meant, and a variable added to the `.hbs` breaks every call
35//! site rather than quietly rendering as nothing.
36//!
37//! When you do not have every variable, `builder()` sets the ones you do have and leaves the rest
38//! empty — as an undefined variable is in Handlebars:
39//!
40//! ```
41//! # mod templates { typed_handlebars::directory!("doc-templates/"); }
42//! assert_eq!(
43//!     templates::button::builder().btn_id(42).render(),
44//!     r#"<button id="btn42"></button>"#
45//! );
46//! ```
47//!
48//! # Entry points
49//!
50//! - [`directory!`] — a module per `.hbs` file in a folder, mirroring the directory layout.
51//!   Resolves `{{> partials}}` against that tree.
52//! - [`file!`] — a single template file.
53//! - [`str!`] — a template written inline, for a one-liner or a test. No directory, so no partials.
54//!
55//! A mistake in a template is reported against the `.hbs` file with a line and column, in
56//! Handlebars terms; anything outside the supported subset is a compile error naming the
57//! construct, never a silent difference in output.
58//!
59//! # Rendering
60//!
61//! `render()` returns a `String`, and `render_to` writes into any [`fmt::Write`](core::fmt::Write)
62//! sink, so a buffer you already have needs no throwaway `String`:
63//!
64//! ```
65//! # mod templates { typed_handlebars::directory!("doc-templates/"); }
66//! use core::fmt::Write;
67//!
68//! let mut page = String::from("<div>");
69//! templates::button::Vars { btn_id: 42, btn_name: "Save" }
70//!     .render_to(&mut page)
71//!     .unwrap();
72//! page.push_str("</div>");
73//! assert_eq!(page, r#"<div><button id="btn42">Save</button></div>"#);
74//! ```
75//!
76//! `{{ name }}` is HTML-escaped and `{{{ name }}}` is not, as Handlebars specifies. Markup you
77//! have already rendered goes in `{{{ }}}` — which is how one template's output is nested inside
78//! another, exactly as handlebars.js passes a rendered fragment in as a variable.
79//!
80//! A variable can be an `Option`, and `None` writes nothing — as null and undefined do in
81//! handlebars.js — so a nullable column needs no unwrapping on the way in:
82//!
83//! ```
84//! # mod templates { typed_handlebars::directory!("doc-templates/"); }
85//! let missing: Option<&str> = None;
86//! assert_eq!(
87//!     templates::button::Vars { btn_id: 42, btn_name: missing }.render(),
88//!     r#"<button id="btn42"></button>"#
89//! );
90//! ```
91//!
92//! # Items in this crate
93//!
94//! Apart from the three macros, everything here — [`Empty`], [`Absent`], [`Render`],
95//! [`RenderExt`], [`Escaped`], [`Shown`], [`Truthy`], [`Length`], [`Set`] and [`IsSet`] — is
96//! runtime support that generated code calls into. It is public because the generated code names
97//! it, not because you need to: there is nothing here for you to implement.
98
99// This crate contains no unsafe code, and generated code never emits any.
100#![forbid(unsafe_code)]
101// Every public item here is named by generated code, so a consumer denying `missing_docs` sees
102// these in their own docs — they are documented for that reader, not for this one.
103#![warn(missing_docs)]
104
105// Generated code names this crate absolutely, as `::typed_handlebars`, so that one emitted path
106// works everywhere: in a consumer's crate, in this crate's own unit tests, and in the doctests
107// above — which rustdoc compiles as separate crates depending on this one.
108extern crate self as typed_handlebars;
109
110/// Generates a module per `.hbs` file in a directory, mirroring the directory layout.
111///
112/// The path is relative to the crate root (the directory holding `Cargo.toml`). Every `.hbs` file
113/// beneath it becomes a module named after the file, holding a `Vars`, a `builder()`, and whatever
114/// types the template implies. Subdirectories become nested modules, so `templates/admin/row.hbs`
115/// is `templates::admin::row` and two files called `row.hbs` in different folders do not collide.
116///
117/// ```
118/// mod templates {
119///     typed_handlebars::directory!("doc-templates/");
120/// }
121///
122/// // doc-templates/button.hbs and doc-templates/greeting.hbs
123/// assert_eq!(
124///     templates::button::Vars { btn_id: 42, btn_name: "Save" }.render(),
125///     r#"<button id="btn42">Save</button>"#
126/// );
127/// assert_eq!(templates::greeting::Vars { name: "King" }.render(), "<p>Hello King!</p>");
128/// ```
129///
130/// `{{> partial}}` is resolved against this tree at compile time. Editing any template — or any
131/// partial it includes — rebuilds the code generated from it.
132///
133/// One broken template reports itself and the others still compile.
134#[doc(inline)]
135pub use typed_handlebars_macros::typed_handlebars_directory as directory;
136
137/// Generates a module for a single `.hbs` file.
138///
139/// The path is relative to the crate root. Partials are resolved against the file's own directory.
140///
141/// ```
142/// mod button {
143///     typed_handlebars::file!("doc-templates/button.hbs");
144/// }
145///
146/// assert_eq!(
147///     button::button::Vars { btn_id: 42, btn_name: "Save" }.render(),
148///     r#"<button id="btn42">Save</button>"#
149/// );
150/// ```
151///
152/// Reach for [`directory!`] unless you want one specific file; it keeps the module layout and the
153/// folder layout the same thing.
154#[doc(inline)]
155pub use typed_handlebars_macros::typed_handlebars_file as file;
156
157/// Generates a module from a template written inline, given a name and the template text.
158///
159/// Useful for a one-liner or a test. There is no directory to resolve against, so `{{> partial}}`
160/// is a compile error here — use [`directory!`] or [`file!`] for templates that include others.
161///
162/// ```
163/// mod templates {
164///     typed_handlebars::str!("greeting", "<p>Hello {{ name }}!</p>");
165/// }
166///
167/// assert_eq!(templates::greeting::Vars { name: "King" }.render(), "<p>Hello King!</p>");
168/// ```
169///
170/// The generated types come from the template just as they do for a file, so a list still
171/// generates its item type:
172///
173/// ```
174/// mod templates {
175///     typed_handlebars::str!("list", "{{#each rows}}<li>{{ name }}</li>{{/each}}");
176/// }
177///
178/// let rows = vec![
179///     templates::list::RowsItem { name: "King" },
180///     templates::list::RowsItem { name: "Tubby" },
181/// ];
182/// assert_eq!(templates::list::Vars { rows }.render(), "<li>King</li><li>Tubby</li>");
183/// ```
184#[doc(inline)]
185pub use typed_handlebars_macros::typed_handlebars_str as str;
186
187/// Names the *frame*: the type whose methods a template's helper calls resolve on.
188///
189/// Handlebars gives a template two things — the data, and a *data frame* of ambient state passed
190/// at render time (`options.data` in handlebars.js, which is where a `{{ t "…" }}` helper reads
191/// its locale from). `Vars` is the data. This names the frame, and a helper is one of its methods:
192///
193/// ```
194/// pub struct Ctx {
195///     greeting: &'static str,
196/// }
197///
198/// impl Ctx {
199///     pub fn t(&self, key: &str) -> String {
200///         format!("{} {}", self.greeting, key)
201///     }
202/// }
203///
204/// mod templates {
205///     typed_handlebars::register_helper!(crate::Ctx);
206///     typed_handlebars::str!("hello", "<p>{{ t \"world\" }}</p>");
207/// }
208///
209/// // Spelled out, so the items above sit at the crate root that `crate::Ctx` names.
210/// fn main() {
211///     let ctx = Ctx { greeting: "Hello" };
212///     assert_eq!(templates::hello::Vars.render(&ctx), "<p>Hello world</p>");
213/// }
214/// ```
215///
216/// Put it in the same module as the templates. Only templates that call a helper take the frame,
217/// so adding `{{ t "…" }}` to one is what makes its call sites ask for it.
218///
219/// Every argument reaches the helper as a `&str`. A quoted string or a number is passed as the
220/// text the template spelled — `{{ money 123 }}` calls `money("123")` — and anything else is a
221/// variable, written out the way `{{{ … }}}` would write it and handed over as that text.
222///
223/// Nothing is checked here: a proc macro sees tokens rather than types, so a missing or misspelled
224/// helper is caught by the generated call, as `no method named `t` found for struct `Ctx``.
225#[doc(inline)]
226pub use typed_handlebars_macros::typed_handlebars_register_helper as register_helper;
227
228/// A variable that was never given a value.
229///
230/// Handlebars treats an undefined variable as empty, and so does this: `Empty` writes nothing when
231/// displayed, and stands in for a list with no items. Generated code uses it; you should never need
232/// to name it.
233pub struct Empty;
234
235impl core::fmt::Display for Empty {
236    fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
237        Ok(())
238    }
239}
240
241impl<T> AsRef<[T]> for Empty {
242    fn as_ref(&self) -> &[T] {
243        &[]
244    }
245}
246
247/// A list variable that was never given a value.
248///
249/// [`Empty`] would do, but a list has to name its item type or nothing can infer it, so this is
250/// `Empty` with the item type written down. Generated code uses it; you should never need to name
251/// it.
252///
253/// Absent is not the same as empty, and `{{ rows.length }}` is the one place the difference shows:
254/// a list that was never set counts as nothing, where a list with no items in it counts `0`. That
255/// is what handlebars.js does with an undefined value against an empty array.
256pub struct Absent<T>(core::marker::PhantomData<T>);
257
258impl<T> Absent<T> {
259    /// Creates the absent list.
260    pub fn new() -> Self {
261        Absent(core::marker::PhantomData)
262    }
263}
264
265impl<T> Default for Absent<T> {
266    fn default() -> Self {
267        Self::new()
268    }
269}
270
271impl<T> AsRef<[T]> for Absent<T> {
272    fn as_ref(&self) -> &[T] {
273        &[]
274    }
275}
276
277/// How many items `{{ rows.length }}` reports.
278///
279/// This follows handlebars.js, where `length` is an ordinary property lookup and a JS array carries
280/// one. Only lists have it here: a `String` deliberately does not, because JS counts UTF-16 code
281/// units and Rust would count either bytes or `char`s — all three disagree on the same text, and a
282/// quietly different number is exactly what this crate promises never to produce.
283///
284/// [`Count`](Length::Count) is an associated type rather than a plain `usize` so that a list which
285/// was never set can report nothing at all, as an undefined value does in handlebars.js, while a
286/// list with no items in it reports `0`.
287#[diagnostic::on_unimplemented(
288    message = "`{Self}` has no `.length` for a template to count",
289    label = "this value is not a list",
290    // Doubled braces: this attribute reads `{…}` as a placeholder, as `format!` does.
291    note = "`{{{{ x.length }}}}` counts a list — a `Vec`, a slice or an array. A `String` has no \
292            `.length` here: JS counts UTF-16 code units and Rust counts bytes or `char`s, so any \
293            answer would silently disagree with handlebars.js"
294)]
295pub trait Length {
296    /// What the count renders as: a number, or nothing at all when the list was never set.
297    type Count: core::fmt::Display + Truthy;
298
299    /// How many items this holds.
300    fn length(&self) -> Self::Count;
301}
302
303impl<T> Length for [T] {
304    type Count = usize;
305    fn length(&self) -> usize {
306        self.len()
307    }
308}
309
310impl<T, const N: usize> Length for [T; N] {
311    type Count = usize;
312    fn length(&self) -> usize {
313        N
314    }
315}
316
317impl<T> Length for Vec<T> {
318    type Count = usize;
319    fn length(&self) -> usize {
320        self.len()
321    }
322}
323
324impl<T: Length + ?Sized> Length for &T {
325    type Count = T::Count;
326    fn length(&self) -> T::Count {
327        (**self).length()
328    }
329}
330
331/// A variable that was never set is absent, and absent has no count — `{{ rows.length }}` writes
332/// nothing, rather than `0`, exactly as it does for an undefined value in handlebars.js.
333impl Length for Empty {
334    type Count = Empty;
335    fn length(&self) -> Empty {
336        Empty
337    }
338}
339
340impl<T> Length for Absent<T> {
341    type Count = Empty;
342    fn length(&self) -> Empty {
343        Empty
344    }
345}
346
347/// How a value is written by `{{ }}` and `{{{ }}}`.
348///
349/// Anything that implements [`Display`](core::fmt::Display) is written as it displays. `Option` is
350/// the exception, and the reason this trait exists rather than a plain `Display` bound: handlebars.js
351/// writes nothing at all for a value that is null or undefined, so `None` writes nothing here too —
352/// exactly as [`Empty`] does for a variable that was never set.
353///
354/// `K` says *which* of those routes a value took. It is a marker type, filled in by inference and
355/// never written by hand: `Option<T>` cannot go through `Display`, and everything else cannot go
356/// through the `Option` impl, so exactly one route ever fits. It has to be a type parameter rather
357/// than one blanket impl with a special case inside, because Rust's coherence rules forbid a crate
358/// from writing both `impl<T: Display> Render for T` and `impl<T> Render for Option<T>`.
359///
360/// Every type you would reasonably pass already implements this — there is nothing here for you to
361/// write.
362// Left to itself, a value that cannot be written reports a missing `Render<ViaDisplay>`, naming a
363// marker the caller never wrote and a trait they have no reason to know. The template asked for
364// something printable, so that is what the error says.
365#[diagnostic::on_unimplemented(
366    message = "`{Self}` cannot be written out by a template",
367    label = "this value has no text to write",
368    note = "a template writes anything that implements `std::fmt::Display`, or an `Option` of \
369            such a type — where `None` writes nothing, as it does in handlebars.js"
370)]
371pub trait Render<K> {
372    /// Writes this value into `out`, unescaped.
373    fn render_to<W: core::fmt::Write + ?Sized>(&self, out: &mut W) -> core::fmt::Result;
374}
375
376/// [`Render`] marker: written as it displays.
377pub struct ViaDisplay;
378
379/// [`Render`] marker: written when `Some`, nothing when `None`.
380pub struct ViaOption;
381
382/// [`Render`] marker: as [`ViaOption`], for an `Option` passed by reference.
383pub struct ViaOptionRef;
384
385impl<T: core::fmt::Display + ?Sized> Render<ViaDisplay> for T {
386    fn render_to<W: core::fmt::Write + ?Sized>(&self, out: &mut W) -> core::fmt::Result {
387        write!(out, "{}", self)
388    }
389}
390
391/// `None` is absent, and absent writes nothing — as null and undefined do in handlebars.js.
392impl<T: core::fmt::Display> Render<ViaOption> for Option<T> {
393    fn render_to<W: core::fmt::Write + ?Sized>(&self, out: &mut W) -> core::fmt::Result {
394        match self {
395            Some(value) => write!(out, "{}", value),
396            None => Ok(()),
397        }
398    }
399}
400
401/// Borrowing the `Option` rather than passing it in is the common case when the value lives in a
402/// struct the caller still owns, so it renders the same way.
403impl<T: core::fmt::Display> Render<ViaOptionRef> for &Option<T> {
404    fn render_to<W: core::fmt::Write + ?Sized>(&self, out: &mut W) -> core::fmt::Result {
405        match self {
406            Some(value) => write!(out, "{}", value),
407            None => Ok(()),
408        }
409    }
410}
411
412/// Turns a value into something `write!` can take, escaped or not.
413///
414/// Generated code calls these as methods — `value.escaped()` rather than `escaped(value)` — because
415/// method lookup steps through references for us. A loop hands its body an `&Item` while a field is
416/// a plain value, and both have to reach the same [`Render`] impl.
417pub trait RenderExt<K>: Render<K> {
418    /// Wraps this value for `{{{ }}}`: written exactly as given.
419    fn shown(&self) -> Shown<'_, Self, K> {
420        Shown(self, core::marker::PhantomData)
421    }
422
423    /// Wraps this value for `{{ }}`: written HTML-escaped.
424    fn escaped(&self) -> Escaped<'_, Self, K> {
425        Escaped(self, core::marker::PhantomData)
426    }
427}
428
429impl<T: Render<K> + ?Sized, K> RenderExt<K> for T {}
430
431/// The wrapper produced by [`RenderExt::shown`].
432pub struct Shown<'a, T: ?Sized, K>(&'a T, core::marker::PhantomData<K>);
433
434impl<T: Render<K> + ?Sized, K> core::fmt::Display for Shown<'_, T, K> {
435    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
436        self.0.render_to(f)
437    }
438}
439
440/// The HTML-escaping wrapper produced by [`RenderExt::escaped`].
441pub struct Escaped<'a, T: ?Sized, K>(&'a T, core::marker::PhantomData<K>);
442
443impl<T: Render<K> + ?Sized, K> core::fmt::Display for Escaped<'_, T, K> {
444    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
445        self.0.render_to(&mut EscapeWriter(f))
446    }
447}
448
449/// Escapes as it forwards, so a large value never lands in a temporary buffer.
450struct EscapeWriter<'a, W: ?Sized>(&'a mut W);
451
452impl<W: core::fmt::Write + ?Sized> core::fmt::Write for EscapeWriter<'_, W> {
453    fn write_str(&mut self, text: &str) -> core::fmt::Result {
454        // The same set handlebars.js escapes, so output matches character for character.
455        let mut written = 0;
456        for (index, character) in text.char_indices() {
457            let replacement = match character {
458                '&' => "&amp;",
459                '<' => "&lt;",
460                '>' => "&gt;",
461                '"' => "&quot;",
462                '\'' => "&#x27;",
463                '`' => "&#x60;",
464                '=' => "&#x3D;",
465                _ => continue,
466            };
467            self.0.write_str(&text[written..index])?;
468            self.0.write_str(replacement)?;
469            written = index + character.len_utf8();
470        }
471        self.0.write_str(&text[written..])
472    }
473}
474
475/// Whether a value counts as true in `{{#if}}` and `{{#unless}}`.
476///
477/// This follows handlebars.js: absent, `false`, an empty string, zero and an empty list are all
478/// falsy; everything else is truthy. Every type you would reasonably pass already implements it —
479/// there is nothing here for you to write.
480pub trait Truthy {
481    /// Whether `{{#if}}` should render its block for this value.
482    fn is_truthy(&self) -> bool;
483}
484
485impl Truthy for bool {
486    fn is_truthy(&self) -> bool {
487        *self
488    }
489}
490
491/// A variable that was never set is absent, and absent is falsy.
492impl Truthy for Empty {
493    fn is_truthy(&self) -> bool {
494        false
495    }
496}
497
498/// A list that was never set is absent, and so is falsy — as an empty list is.
499impl<T> Truthy for Absent<T> {
500    fn is_truthy(&self) -> bool {
501        false
502    }
503}
504
505impl Truthy for str {
506    fn is_truthy(&self) -> bool {
507        !self.is_empty()
508    }
509}
510
511impl Truthy for String {
512    fn is_truthy(&self) -> bool {
513        !self.is_empty()
514    }
515}
516
517/// `None` is absent; `Some` is present whatever it wraps, as in handlebars.js.
518impl<T> Truthy for Option<T> {
519    fn is_truthy(&self) -> bool {
520        self.is_some()
521    }
522}
523
524impl<T> Truthy for [T] {
525    fn is_truthy(&self) -> bool {
526        !self.is_empty()
527    }
528}
529
530impl<T, const N: usize> Truthy for [T; N] {
531    fn is_truthy(&self) -> bool {
532        N != 0
533    }
534}
535
536impl<T> Truthy for Vec<T> {
537    fn is_truthy(&self) -> bool {
538        !self.is_empty()
539    }
540}
541
542impl<T: Truthy + ?Sized> Truthy for &T {
543    fn is_truthy(&self) -> bool {
544        (**self).is_truthy()
545    }
546}
547
548macro_rules! truthy_if_nonzero {
549    ($($ty:ty),* $(,)?) => {
550        $(
551            impl Truthy for $ty {
552                fn is_truthy(&self) -> bool {
553                    *self != 0 as $ty
554                }
555            }
556        )*
557    };
558}
559
560truthy_if_nonzero!(
561    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
562);
563
564/// A value a builder has been given.
565///
566/// Generated code uses this; you should never need to name it.
567pub struct Set<T>(pub T);
568
569/// Supplies the value held in a builder slot.
570///
571/// Every generated builder starts with each slot held by a `<template>_unset_<variable>` marker,
572/// which resolves to whatever absent means for that variable — nothing to display, a list with no
573/// items, a false condition. Setting a variable swaps the slot for [`Set`]. Nothing here needs
574/// naming from outside generated code.
575pub trait IsSet {
576    /// The type of the value in this slot.
577    type Value;
578
579    /// Unwraps the value.
580    fn into_value(self) -> Self::Value;
581}
582
583impl<T> IsSet for Set<T> {
584    type Value = T;
585
586    fn into_value(self) -> T {
587        self.0
588    }
589}