A pure-Go (no cgo) reimplementation of Ruby's ERB
template compiler — the deterministic, interpreter-independent core of MRI's
ERB::Compiler. It turns an ERB template string into the Ruby source that
renders it, matching MRI 4.0.5 byte-for-byte, and provides ERB::Util's
html_escape / url_encode helpers.
It is the ERB backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime.
What it is — and isn't. Compiling a template to Ruby source (tag scanning, trim modes, the
<%% %%>literals, magic-comment detection, theString#dumptext encoding) is fully deterministic and needs no interpreter, so it lives here as pure Go. The finaleval(compiled_src, binding)that produces the rendered string does need a Ruby interpreter and stays in the consumer (e.g. rbgo) — this library compiles, the host evaluates.
Faithful port of MRI's lib/erb/compiler.rb, validated against the ruby
binary on every supported platform:
- All tag kinds —
<% code %>,<%= expression %>,<%# comment %>. - Literals —
<%%→<%and the%%>escape, exactly as MRI scans them. - Every trim mode — the MRI
trim_modestring and its combinations:-—-%>strips the immediately-following newline (explicit trim);>— strips the newline after a tag that ends its line;<>— strips the newline only when the tag both starts and ends the line;%—%-prefixed lines are code lines, with%%an escaped literal%.
- Magic comments — a leading
<%# coding: … %>/frozen_string_literal: …(including the emacs-*- … -*-form) is detected and reflected in the emitted#coding:/#frozen-string-literal:prefix. - Binary-exact text encoding — literal runs are emitted via Ruby's
String#dumpon the binary string, so embedded quotes, newlines, control bytes and multi-byte UTF-8 round-trip byte-for-byte (héllo→"h\xC3\xA9llo"). ERB::Util—HTMLEscape(&<>"'→ entities,'→') andURLEncode(percent-encode all butA-Za-z0-9-_.~, upper-case hex).- erubi dialect (
Mode: ModeErubi) — reproduces the erubi gem'sErubi::Enginewhitespace/trim semantics, so consumers that render through erubi (Sinatra, Rails) get byte-identical output. erubi matches no classictrim_mode: a standalone<%= x %>\nkeeps its newline (whereas<>trims it), a line holding only a<% … %>code or<%# … %>comment tag is trimmed automatically (whereas the default keeps it and-needs explicit<%- … -%>),-%>/=%>chomps an expression's newline, and<%==HTML-escapes. Additive: the defaultModeERBis unchanged, sorequire "erb"consumers are unaffected.
CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and
green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le,
s390x).
go get github.com/go-ruby-erb/erbpackage main
import (
"fmt"
"github.com/go-ruby-erb/erb"
)
func main() {
src, magic, err := erb.Compile("Hello <%= name %>!\n", erb.Options{})
if err != nil {
panic(err)
}
fmt.Print(magic) // #coding:UTF-8
fmt.Println(src)
// #coding:UTF-8
// _erbout = +''; _erbout.<< "Hello ".freeze; _erbout.<<(( name ).to_s); _erbout.<< "!\n".freeze
// ; _erbout
//
// src already carries the #coding prefix; hand it to a Ruby interpreter:
// eval(src, binding) -> "Hello World!\n"
fmt.Println(erb.HTMLEscape(`<a href="x">it's</a>`))
// <a href="x">it's</a>
fmt.Println(erb.URLEncode("100% & more"))
// 100%25%20%26%20more
}With a trim mode and a custom buffer variable:
src, _, _ := erb.Compile("<ul>\n<% items.each do |i| -%>\n <li><%= i %></li>\n<% end -%>\n</ul>\n",
erb.Options{TrimMode: "-", EOutVar: "buf"})In erubi-compatible mode (for Sinatra/Rails-style rendering) the standalone
<%= … %> newline is kept and standalone <% … %> lines are trimmed:
// "<%= yield %>\n" -> renders "…\n" (newline kept), matching erubi — not "…"
src, _, _ := erb.Compile("<%= yield %>\n<% items.each do |i| %>\n<%= i %>\n<% end %>\n",
erb.Options{Mode: erb.ModeErubi})type Options struct {
Mode Mode // ModeERB (default, classic MRI ERB) or ModeErubi (erubi-compatible)
TrimMode string // MRI trim_mode: "", "-", ">", "<>", "%" and combinations (ModeERB only)
EOutVar string // output buffer var name; default "_erbout"
}
// Mode selects the ERB dialect Compile targets.
type Mode int
const (
ModeERB Mode = iota // classic MRI ERB (honours TrimMode)
ModeErubi // erubi gem's Erubi::Engine semantics (TrimMode ignored)
)
// Compile returns the Ruby source that, when eval'd against a binding, renders
// the template, plus the magic-encoding comment line — matching MRI's
// ERB::Compiler#compile two-value contract.
func Compile(template string, opts Options) (src, magicComment string, err error)
func HTMLEscape(s string) string // ERB::Util.html_escape / .h
func URLEncode(s string) string // ERB::Util.url_encode / .u
// Compiler mirrors MRI's ERB::Compiler for hosts that need the put/insert/
// pre/post command wiring directly (most callers use Compile).
type Compiler struct { PutCmd, InsertCmd string; PreCmd, PostCmd []string; EOutVar string }
func NewCompiler(trimMode string) *Compiler
func (c *Compiler) Compile(s string) (src, magicComment string, err error)The suite includes a differential oracle: a wide template corpus (every tag
kind, the literals, all trim modes, multiline/quoted/unicode bodies) is compiled
both here and by the system ruby, comparing the emitted Ruby source and the
rendered result byte-for-byte, plus ERB::Util against MRI. A parallel oracle
renders the erubi-mode corpus against the installed erubi gem (Erubi::Engine)
for byte-identical parity.
COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1 # 100.0%The oracle tests skip themselves where ruby is not on PATH (e.g. the qemu
arch lanes), so the cross-arch builds still validate the compiler itself.
BSD-3-Clause — see LICENSE. Copyright the go-ruby-erb/erb authors.
Being pure Go (CGO=0), this library also compiles to WebAssembly — both
GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI).
CI builds both targets on every push, alongside the six 64-bit native/qemu arches.
GOOS=js GOARCH=wasm go build ./... # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./... # WASI (wasmtime, wasmer, wasmedge, …)