philiprehberger-template

Tests Gem Version Last updated

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-template

Usage

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>")
# => "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;"

# 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, titleize.

# Truncate filter (default limit: 30)
tpl = Philiprehberger::Template.new("{{text | truncate(10)}}")
tpl.render(text: "Hello, beautiful world")
# => "Hello, bea..."

# Titleize filter — capitalize each word
tpl = Philiprehberger::Template.new("{{title | titleize}}")
tpl.render(title: "hello world from ruby")
# => "Hello World From Ruby"

List custom filters that have been registered:

Philiprehberger::Template::Filters.register("shout", ->(val) { "#{val}!" })
Philiprehberger::Template.registered_filters
# => ["shout"]

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.registered_filters List names of all registered custom filters
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 rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT