philiprehberger-template
Logic-less Mustache-style template engine with safe rendering
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-template"Or install directly:
gem install philiprehberger-templateUsage
require "philiprehberger/template"
tpl = Philiprehberger::Template.new("Hello, {{name}}!")
tpl.render(name: "World")
# => "Hello, World!"Sections and Inverted Sections
# Truthy/falsy sections
tpl = Philiprehberger::Template.new("{{#show}}visible{{/show}}")
tpl.render(show: true) # => "visible"
tpl.render(show: false) # => ""
# Array iteration
tpl = Philiprehberger::Template.new("{{#items}}* {{name}}\n{{/items}}")
tpl.render(items: [{ name: "Alice" }, { name: "Bob" }])
# => "* Alice\n* Bob\n"
# Inverted sections
tpl = Philiprehberger::Template.new("{{^items}}No items found.{{/items}}")
tpl.render(items: [])
# => "No items found."
# Nested scopes (child inherits parent variables)
tpl = Philiprehberger::Template.new("{{#user}}{{greeting}}, {{name}}{{/user}}")
tpl.render(greeting: "Hi", user: { name: "Alice" })
# => "Hi, Alice"Partials
Philiprehberger::Template.register_partial("header", "<h1>{{title}}</h1>")
Philiprehberger::Template.register_partial("footer", "<footer>{{year}}</footer>")
tpl = Philiprehberger::Template.new("{{> header}}<main>{{content}}</main>{{> footer}}")
tpl.render(title: "Home", content: "Welcome!", year: 2026)
# => "<h1>Home</h1><main>Welcome!</main><footer>2026</footer>"
Philiprehberger::Template.clear_partials!Custom Delimiters
tpl = Philiprehberger::Template.new("{{name}} {{= <% %> =}} <%greeting%>")
tpl.render(name: "Alice", greeting: "Hi")
# => "Alice Hi"Filters
# Single filter
tpl = Philiprehberger::Template.new("{{name | upcase}}")
tpl.render(name: "hello")
# => "HELLO"
# Chained filters
tpl = Philiprehberger::Template.new("{{name | strip | upcase}}")
tpl.render(name: " hello ")
# => "HELLO"
# Default filter with argument
tpl = Philiprehberger::Template.new("{{name | default(Anonymous)}}")
tpl.render({})
# => "Anonymous"
# HTML escaping
tpl = Philiprehberger::Template.new("{{content | escape}}")
tpl.render(content: "<script>alert('xss')</script>")
# => "<script>alert('xss')</script>"
# Custom filters
Philiprehberger::Template::Filters.register("shout", ->(val) { "#{val}!!!" })
tpl = Philiprehberger::Template.new("{{name | shout}}")
tpl.render(name: "hello")
# => "hello!!!"Built-in filters: upcase, downcase, strip, escape, capitalize, reverse, length, default, truncate.
# Truncate filter (default limit: 30)
tpl = Philiprehberger::Template.new("{{text | truncate(10)}}")
tpl.render(text: "Hello, beautiful world")
# => "Hello, bea..."Template Compilation and Caching
# Compile once, render many times with different data
tpl = Philiprehberger::Template.compile("Hello, {{name}}!")
tpl.render(name: "Alice") # => "Hello, Alice!"
tpl.render(name: "Bob") # => "Hello, Bob!"
# Same source returns the cached template instance
tpl2 = Philiprehberger::Template.compile("Hello, {{name}}!")
tpl.equal?(tpl2) # => true
Philiprehberger::Template.clear_cache!Template Inheritance/Layouts
Philiprehberger::Template.register_layout("base", <<~LAYOUT)
<html>
<head>{{$ title}}Default Title{{/title}}</head>
<body>{{$ body}}Default Body{{/body}}</body>
</html>
LAYOUT
tpl = Philiprehberger::Template.new("{{< base}}{{$ title}}My Page{{/title}}{{$ body}}Hello!{{/body}}{{/base}}")
tpl.render({})
# Renders layout with "My Page" as title and "Hello!" as body
Philiprehberger::Template.clear_layouts!Lambda Support
tpl = Philiprehberger::Template.new("{{#bold}}text{{/bold}}")
tpl.render(bold: ->(raw) { "<b>#{raw}</b>" })
# => "<b>text</b>"
# Lambdas receive the raw (unrendered) block text
tpl = Philiprehberger::Template.new("{{#wrap}}{{name}}{{/wrap}}")
tpl.render(name: "Alice", wrap: ->(raw) { "[#{raw}]" })
# => "[{{name}}]"Comments
# Comments are stripped from rendered output
tpl = Philiprehberger::Template.new("Hello{{! This is a comment }} World")
tpl.render({})
# => "Hello World"
# Multi-line comments
tpl = Philiprehberger::Template.new("Hello{{! this is\na multi-line comment }}World")
tpl.render({})
# => "HelloWorld"Strict Mode
# Raises UndefinedVariableError for missing variables
tpl = Philiprehberger::Template.new("Hello, {{name}}!", strict: true)
tpl.render(name: "World") # => "Hello, World!"
tpl.render({}) # => raises UndefinedVariableError
# Raises UndefinedFilterError for unknown filters
tpl = Philiprehberger::Template.new("{{name | bogus}}", strict: true)
tpl.render(name: "hi") # => raises UndefinedFilterError
# Default mode renders empty string for missing variables
tpl = Philiprehberger::Template.new("Hello, {{name}}!")
tpl.render({})
# => "Hello, !"Whitespace Control
# Strip whitespace before the tag
tpl = Philiprehberger::Template.new("Hello {{~ name }}")
tpl.render(name: "World")
# => "HelloWorld"
# Strip whitespace after the tag
tpl = Philiprehberger::Template.new("{{ name ~}} there")
tpl.render(name: "Hello")
# => "Hellothere"
# Strip both sides
tpl = Philiprehberger::Template.new("Hello {{~ name ~}} World")
tpl.render(name: ", ")
# => "Hello, World"API
| Method | Description |
|---|---|
Template.new(source, strict: false) |
Compile a template string into a renderable template |
Template.from_file(path, strict: false) |
Read a file and compile its contents as a template |
Template.compile(source, strict: false) |
Compile and cache a template for repeated rendering |
Template.register_partial(name, source) |
Register a named partial template |
Template.clear_partials! |
Remove all registered partials |
Template.register_layout(name, source) |
Register a named layout template |
Template.clear_layouts! |
Remove all registered layouts |
Template.registered_partials |
List names of all registered partials |
Template.registered_layouts |
List names of all registered layouts |
Template.clear_cache! |
Clear the compiled template cache |
Template.cache |
Access the template cache instance |
Filters.register(name, callable) |
Register a custom filter |
Filters.reset_custom! |
Remove all custom filters |
#render(variables = {}) |
Render the template with the given variable hash |
#source |
Returns the original template source string |
#strict? |
Returns whether the template uses strict mode |
Thread Safety
Note: Template.register_partial, Template.register_layout, and the compilation cache are class-level shared state. If you register partials or layouts from multiple threads simultaneously, wrap the calls in a Mutex.
Development
bundle install
bundle exec rspec
bundle exec rubocopSupport
If you find this project useful: