A sandboxed template engine that uses Ruby ERB syntax, with native implementations in Ruby, Python and Rust. Each implementation produces identical output, and none of them needs Ruby installed.
<%- cfg = JSON.parse(File.read("targets.json")) -%>
<%- cfg["targets"].each_with_index do |name, i| -%>
TARGET <%= name.upcase %> <%= format("%04X", 0x100 + i) %>
<%- end -%>
Total: <%= cfg["targets"].size %>ERB is a convenient way to write config files and code generators, but evaluating an ERB template means running arbitrary Ruby. ERBLite keeps the syntax but runs a small, interpreted subset of Ruby:
- No code execution escapes. Templates are lexed, parsed and run by a tree-walking
interpreter. No implementation uses
eval. Method calls go through fixed allowlists, and templates cannotrequire,send, reachENV, run system commands or touch globals. - The same language everywhere. Python and Rust tools can render the same templates as Ruby tools, with the same output.
- Ruby-compatible. A template that renders without error in ERBLite gives
the same output in real Ruby ERB (
trim_mode: "-"), apart from a few documented [ERBLite] differences such as 64-bit integers.
- Tags:
<% %>,<%= %>,<%# %>,<%%, plus-%>and<%-trimming. - Literals: integers, floats, strings with
#{}interpolation, symbols, arrays, hashes, ranges,%w[]. - Variables,
+= ||=and similar, multiple assignment, all the usual operators,&.. - Control flow:
if/elsif/else/unless, modifiers, ternary,case/when,while,until,for,loop,break,next. - Blocks:
each,map,select,each_with_index,inject,group_by, … and enumerator chains (map.with_index). - Several hundred built-in String/Integer/Float/Array/Hash/Range methods, plus
format. - Data:
File.read,Dir.globandrenderpartials, sandboxed to a base directory, andJSON.parse/JSON.generate/to_json. - Not included:
def, classes, lambdas, regex,begin/rescue,require, globals.
See SPEC.md for the full definition.
See examples/plugin.txt.erb for a larger example that uses JSON, partials, format and nested loops. It renders identically in all three implementations and in real Ruby ERB.
| Language | Directory | Usage |
|---|---|---|
| Python | python/ |
erblite.render(src, {"x": 1}, base_dir="cfg") |
| Ruby | ruby/ |
ERBLite.render(src, {x: 1}, base_dir: "cfg") |
| Rust | rust/ |
erblite::render(src, &vars) / Template::compile(..)?.render(..) |
conformance/: the shared case suite every implementation must pass. Expected output is generated by real Ruby ERB (ruby conformance/generate.rb).tools/difftest.rb: a differential fuzzer. It generates random expressions, uses Ruby as the oracle, and compares all three CLIs:ruby tools/difftest.rb --count 2000.
BUNDLE_GEMFILE=conformance/Gemfile bundle exec ruby conformance/generate.rb --check
(cd python && .venv/bin/pytest)
(cd ruby && bundle exec rake spec)
(cd rust && cargo test)All three packages share one version number. tools/version.rb keeps every
version location in sync.
ruby tools/version.rb 0.2.0 # bump python, ruby and rust together
git commit -am "Release 0.2.0"
git tag v0.2.0 && git push origin main v0.2.0Pushing a vX.Y.Z tag starts .github/workflows/release.yml. It checks that the
tag matches the package versions, runs the full CI suite, and builds the packages.
It then publishes to PyPI, RubyGems and crates.io using trusted publishing
(the one-time registry setup is described at the top of the workflow) and
creates a GitHub release with the built packages attached.