Ramhorns Templating Engine

repository·master·Indexed 43 days ago

https://github.com/maciejhirsz/ramhorns

A fast, pure-Rust implementation of the Mustache template engine designed for runtime template processing. It features a `Content` derive macro for rendering native Rust data structures, support for partials, and optional zero-copy CommonMark rendering via the `#[md]` attribute.

Tokens
4.8K
Snippets
19
Records
24
Agent score
52%

What's inside Ramhorns

  1. Mustache syntax and features in Ramhorns

    master

    Ramhorns implements the Mustache template syntax with several key features:

    • Variables: Use {{variable}} to render common types like &str, String, bool, and numbers.
    • Unescaped Printing: Use {{{triple-brace}}} or {{&ampersand}} to print content without HTML escaping.
    • Sections: Use {{#foo}} ... {{/foo}} to render sections (useful for iterating over lists or conditional rendering).
    • Inverse Sections: Use {{^foo}} ... {{/foo}} to render content when a section is empty or false.
    • Partials: Use {{>file.html}} to include partial templates.
    • Markdown Rendering: Fields marked with the #[md] attribute support zero-copy CommonMark rendering.
  2. Render templates using the `Content` derive macro

    master

    Ramhorns provides a Content derive macro that allows you to render templates directly from native Rust data structures. This avoids the need for intermediate HashMaps or temporary allocations. You can use various types like &str, String, bool, and numbers within your structs. For collections, Vec<T> or slices like &[T] are supported.

    To use it:

    1. Derive Content on your data structures.
    2. Create a Template using Template::new(source_string).
    3. Call .render(&data_struct) on the template instance.
    use ramhorns::{Template, Content};
    
    #[derive(Content)]
    struct Post<'a> {
        title: &'a str,
        teaser: &'a str,
    }
    
    #[derive(Content)]
    struct Blog<'a> {
        title: String,
        posts: Vec<Post<'a>>,
    }
    
    let source = "<h1>{{title}}</h1>{{#posts}}<article><h2>{{title}}</h2><p>{{teaser}}</p></article>{{/posts}}";
    let tpl = Template::new(source).unwrap();
    
    let rendered = tpl.render(&Blog {
        title: "My Blog".to_string(),
        posts: vec![Post { title: "Hello", teaser: "World" }],
    });
  3. Use index-based list traversal with the `indexes` feature

    master

    When the indexes feature is enabled, you can perform specific list traversals within sections using index-based tags. This allows you to target specific elements in a list (like the first or last) or include/exclude specific indices.

    Syntax

    • Include an index: {{#-index}} (e.g., {{#-first}} or {{#-2}})
    • Exclude an index: {{^-index}} (e.g., {{^-last}} or {{^-0}})

    Supported Index Keywords

    • first: Targets the first element of the list.
    • last: Targets the last element of the list.
    • n: Any integer targets the nth element (0-indexed).
    <!-- Example of index-based tags in a template -->
    {{#items}}
      {{#-first}} This is the first item! {{/items}}
      {{^-last}} This is not the last item! {{/items}}
      {{#-2}} This is the third item (index 2). {{/items}}
    {{/items}}
  4. Manage templates and partials with the `Ramhorns` aggregator

    master

    The Ramhorns struct acts as an aggregator for Templates. It manages a collection of templates and handles the loading and resolution of partials (e.g., {{>partial}}). It is designed to load templates from the file system and can be configured with a custom hasher for performance or DOS resistance (e.g., using aHash).

    // Example of creating an aggregator from a folder
    let tpls: Ramhorns = Ramhorns::from_folder("./templates").unwrap();
    let rendered = tpls.get("hello.html").unwrap().render(&content);
  5. How truthiness works for different types

    master

    When using conditionals in templates, is_truthy() determines the outcome:

    TypeTruthy Condition
    ()Always false
    str / String!self.is_empty()
    boolThe value itself
    Integersself != 0
    Floatsself.abs() > f32::EPSILON
    Option<T>self.is_some()
    Result<T, U>self.is_ok()
    Vec<T> / [T]!self.is_empty()
    HashMap / BTreeMap!self.is_empty()
  6. Implement the Content trait for template context

    master

    The Content trait is used to define how data structures can be used as context within Ramhorns templates. It provides methods for rendering variables (escaped or unescaped), handling conditional sections, and accessing fields by name or hash.

    While you can implement this trait manually for custom types, most users should use the #[derive(Content)] attribute from the ramhorns-derive crate to automatically implement it for their structs and enums.

    #[derive(ramhorns_derive::Content)]
    struct MyContext {
        name: String,
        is_active: bool,
        items: Vec<String>,
    }
  7. Configure field rendering with #[ramhorns] attributes

    master

    You can fine-tune how specific fields are rendered or how the entire struct is processed using the #[ramhorns(...)] attribute. These attributes can be applied to the struct itself or to individual fields.

    Struct-level attributes

    • rename_all = "<CASE>": Changes the naming convention for all fields when they are accessed in templates. Supported cases:
      • UPPERCASE
      • PascalCase
      • camelCase
      • sake_case (Note: this is the literal string used for snake_case)
      • SCREAMING_SNAKE_CASE
      • kebab-case
      • SCREAMING-KEBAB-CASE
    • callback = "<PATH>": Sets a custom function to handle rendering for all fields in the struct.

    Field-level attributes

    • skip: Excludes the field from being rendered.
    • md: Uses ::ramhorns::encoding::encode_cmark as a callback to render the field content as Markdown.
    • flatten: Instead of rendering the field directly, it flattens the field's own content into the current context. This is useful for nested structs.
    • rename = "<STRING>": Overrides the name of the field used in the template.
    • callback = "<PATH>": Specifies a custom function to handle rendering for this specific field. The callback should accept the field value and an encoder.
    #[derive(ramhorns::Content)]
    #[ramhorns(rename_all = "camelCase")]
    struct User {
        #[ramhorns(rename = "full_name")]
        name: String,
        
        #[ramhorns(md)]
        bio: String,
    
        #[ramhorns(skip)]
        internal_id: u32,
    
        #[ramhorns(flatten)]
        metadata: Metadata,
    }
  8. Render a Template to a String

    master

    The simplest way to render a template is using render(&self, content: &C), which returns a String. This method automatically handles HTML escaping and estimates the required buffer capacity to minimize reallocations.

    // Assuming 'content' implements the Content trait
    let rendered = tpl.render(&content);
  9. Use the Content trait to render data

    master

    The Content trait provides several methods for the template engine to interact with your data:

    • is_truthy(): Determines if a value is considered 'true' for conditional rendering. For example, () is always false, while non-empty strings or non-zero numbers are true.
    • render_escaped(): Renders the value with HTML escaping (e.g., < becomes &lt;).
    • render_unescaped(): Renders the value without any escaping.
    • render_section(): Renders a template section if the current content is truthy.
    • render_inverse(): Renders a template section if the current content is NOT truthy.
    • render_field_escaped() / render_field_unescaped(): Accesses a specific field within a map-like structure by its name or hash and renders it.
    • render_field_section() / render_field_inverse(): Accesses a field and renders it as a conditional section.
  10. Create a new Template

    master

    Use Template::new(source) to create a preprocessed template from a string.

    • If you pass a &str, the Template will borrow the source text (using the 'tpl lifetime).
    • If you pass a String, the Template will take ownership of the string, resulting in a 'static lifetime.
    // Borrowing a string slice
    let source = "Hello {{name}}!";
    let tpl = Template::new(source).unwrap();
    
    // Taking ownership of a String
    let source_string = String::from("Hello {{name}}!");
    let tpl: Template<'static> = Template::new(source_string).unwrap();