erubi

package module
v0.0.0-...-a50720a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

README

go-ruby-erubi/erubi

erubi — go-ruby-erubi

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the erubi gemRails' default ERB template engine and the modern, frozen-string successor to erubis. It is a template-to-Ruby-source compiler: given an ERB template it emits the exact Ruby source that erubi's Erubi::Engine#src emits, matching the erubi gem (1.13.1, MRI 4.0.5) byte-for-byte, so a downstream Ruby evaluation renders identically to Rails.

It is the erubi backend for go-embedded-ruby (feeding actionview's rendering seam via a later rbgo binding), 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 rules, the <%% %%> literals, frozen-string handling, the chained-append optimization, the capture-block variant) is fully deterministic and needs no interpreter, so it lives here as pure Go. The final eval(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. The escape helper ::Erubi.h that the emitted source calls is likewise a host-supplied runtime helper; HTMLEscape documents and mirrors its semantics.

Features

Faithful port of erubi's lib/erubi.rb Engine#initialize and lib/erubi/capture_end.rb, validated against the erubi gem on every supported platform:

  • All tag kinds<% code %> (execute), <%= expr %> (output, auto-escaped when Escape), <%== expr %> (the inverted-escape output), <%# comment %>, and the <%% %%> literal escapes.
  • Exact trim rules — erubi's <% %> / <%- -%> leading-indent and trailing-newline handling (Trim, default on), the fiddly part, byte-matched.
  • Every optionescape / escape_html, escapefunc, trim, bufvar / outvar, bufval, freeze (the # frozen_string_literal: true comment), freeze_template_literals (the .freeze suffix), chain_appends, ensure, preamble / postamble, filename, regexp, literal_prefix / literal_postfix, src.
  • CaptureEndEngine — the <%|= ... %> ... <%| end %> block-capture variant Rails uses for form_with-style helpers, with escape_capture and yield_returns_buffer.
  • ::Erubi.h semanticsHTMLEscape (&<>"' → entities, '&#39;), the escape the emitted source calls at render time.

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) plus WebAssembly.

Install

go get github.com/go-ruby-erubi/erubi

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-erubi/erubi"
)

func main() {
	e := erubi.NewEngine("Hello <%= name %>!\n", erubi.Options{})
	fmt.Printf("%q\n", e.Src())
	// "_buf = ::String.new; _buf << 'Hello '.freeze; _buf << ( name ).to_s; _buf << '!\n'.freeze;\n_buf.to_s\n"
	//
	// Hand e.Src() to a Ruby interpreter with ::Erubi in scope:
	//   eval(src, binding)  ->  "Hello World!\n"

	// Auto-HTML-escaping <%= %> (Rails' default), via ::Erubi.h:
	esc := erubi.NewEngine("<p><%= body %></p>", erubi.Options{Escape: erubi.Bool(true)})
	fmt.Printf("%q\n", esc.Src())

	fmt.Println(erubi.HTMLEscape(`<a href="x">it's</a>`))
	// &lt;a href=&quot;x&quot;&gt;it&#39;s&lt;/a&gt;
}

Capture blocks (Erubi::CaptureEndEngine), the form Rails uses for form_with:

c := erubi.NewCaptureEndEngine("<%|= form do %><%= field %><%| end %>", erubi.Options{})
fmt.Println(c.Src())

API

type Options struct {
	// Tri-state booleans: nil = erubi default. Use erubi.Bool to set.
	Escape                 *bool // auto-escape <%= %>; invert <%= %>/<%== %>
	EscapeHTML             *bool // lower-priority alias for Escape
	Trim                   *bool // trim rules (default true)
	FreezeTemplateLiterals *bool // .freeze suffix on text chunks (default true)

	Freeze       bool // # frozen_string_literal: true magic comment
	ChainAppends bool // chain _buf << a << b
	Ensure       bool // wrap in begin/ensure restoring the buffer var

	EscapeFunc     string // default "::Erubi.h" / "__erubi.h"
	BufVar, OutVar string // buffer var name; default "_buf"
	BufVal         string // buffer initial value; default "::String.new"
	Filename       string
	Preamble       string // default "<bufvar> = <bufval>;"
	Postamble      string // default "<bufvar>.to_s\n"
	LiteralPrefix  string // default "<%"
	LiteralPostfix string // default "%>"
	Src            string // initial source string
	Regexp         *regexp.Regexp // default DefaultRegexp

	// CaptureEndEngine only:
	EscapeCapture      *bool // default = Escape
	YieldReturnsBuffer bool
}

func Bool(b bool) *bool

// NewEngine compiles a template, mirroring Erubi::Engine.new.
func NewEngine(input string, opts Options) *Engine

type Engine struct{ /* ... */ }
func (e *Engine) Src() string      // the compiled Ruby source
func (e *Engine) Filename() string
func (e *Engine) BufVar() string

// NewCaptureEndEngine mirrors Erubi::CaptureEndEngine.new (block capture).
func NewCaptureEndEngine(input string, opts Options) *CaptureEndEngine

func HTMLEscape(s string) string // ::Erubi.h (ERB::Escape#html_escape)

Tests & coverage

The suite includes a differential oracle: a wide template corpus (every tag kind, the literals, all trim situations, capture blocks, multiline/quoted/unicode bodies) is compiled both here and by the erubi gem across a matrix of option combinations, comparing Erubi::Engine#src / CaptureEndEngine#src byte-for-byte. A second oracle eval's our emitted source under Ruby and compares the rendered output to the gem's, escape on and off.

gem install erubi
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 and wasm lanes), so the cross-arch builds still validate the compiler.

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-erubi/erubi authors.

WebAssembly

Being pure Go (CGO=0), erubi is pure logic and 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, …)

Documentation

Overview

Package erubi is a pure-Go (no cgo) reimplementation of the erubi gem — Rails' default ERB template engine and the modern, frozen-string successor to erubis. It is a template-to-Ruby-source COMPILER: given an ERB template it emits the exact Ruby source that erubi's Erubi::Engine#src would emit, matching the erubi gem (1.13.1, Ruby 4.0.5) byte-for-byte, so that a downstream Ruby evaluation (e.g. go-embedded-ruby/rbgo, feeding actionview's rendering seam) produces identical output.

What it is NOT: the final eval of the compiled source against a binding needs a Ruby interpreter and is deliberately left to the consumer. This package compiles; the host evaluates. The escape helper ::Erubi.h that the emitted source calls is likewise a runtime helper the host supplies; HTMLEscape below documents and mirrors its semantics for parity and testing.

The package faithfully ports erubi's lib/erubi.rb Engine#initialize: the <% %> (execute), <%= %> (output, auto-escaped when Escape), <%== %> (the inverted-escape output), <%# %> (comment) and <%% %%> literal tags, the trim rules for <% %>/<%- -%>, the frozen-string-literal handling, the chained-append optimization, the begin/ensure wrapping, and the preamble/postamble/regexp/ literal-delimiter/escape-function overrides. CaptureEndEngine ports lib/erubi/capture_end.rb (the <%|= ... %> ... <%| end %> block-capture variant Rails uses for form_with-style helpers).

Index

Constants

This section is empty.

Variables

View Source
var CaptureEndRegexp = regexp.MustCompile(`(?s)<%(\|?={1,2}|-|#|%|\|)?(.*?)([-=])?%>([ \t]*\r?\n)?`)

CaptureEndRegexp is the scanner used by NewCaptureEndEngine, mirroring the regexp Erubi::CaptureEndEngine installs, adding the <%| , <%|= and <%|== tags.

View Source
var DefaultRegexp = regexp.MustCompile(`(?s)<%(={1,2}|-|#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?`)

DefaultRegexp is the scanner used by NewEngine, mirroring Erubi::Engine::DEFAULT_REGEXP. Ruby's /m flag (dot matches newline) maps to Go's (?s) flag.

Functions

func Bool

func Bool(b bool) *bool

Bool returns a pointer to b, for setting the tri-state Options fields.

func HTMLEscape

func HTMLEscape(s string) string

HTMLEscape mirrors ::Erubi.h (ERB::Escape#html_escape): it replaces the five HTML-significant characters with their entity references (note "'" becomes "&#39;"). The compiled source emitted by this package calls ::Erubi.h at render time; this function documents and reproduces that helper's semantics so a host binding (or a test) can supply an identical escape.

Types

type CaptureEndEngine

type CaptureEndEngine struct {
	*Engine
}

CaptureEndEngine is the compiled result of the capture-block ERB variant, mirroring Erubi::CaptureEndEngine. It supports capturing blocks via the <%|= %> and <%|== %> tags, explicitly ended with a <%| end %> block — the form Rails uses for form_with-style capture helpers. It embeds *Engine, so Src, Filename and BufVar are available directly.

func NewCaptureEndEngine

func NewCaptureEndEngine(input string, opts Options) *CaptureEndEngine

NewCaptureEndEngine compiles input into a CaptureEndEngine, mirroring Erubi::CaptureEndEngine.new. It installs CaptureEndRegexp (unless overridden by Options.Regexp) so the <%| , <%|= and <%|== tags are recognized, and honors the EscapeCapture and YieldReturnsBuffer options. It panics with *InvalidIndicatorError only if a caller-supplied Regexp yields an unknown indicator.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the compiled result for a template, mirroring Erubi::Engine. Its Src is the frozen Ruby source that, when eval'd against a binding, renders the template identically to the erubi gem.

func NewEngine

func NewEngine(input string, opts Options) *Engine

NewEngine compiles input into an Engine, mirroring Erubi::Engine.new. It panics with *InvalidIndicatorError only if a caller-supplied Options.Regexp yields an unknown tag indicator.

func (*Engine) BufVar

func (e *Engine) BufVar() string

BufVar returns the buffer variable name used by the compiled source (erubi's #bufvar).

func (*Engine) Filename

func (e *Engine) Filename() string

Filename returns the template filename, if one was given (erubi's #filename).

func (*Engine) Src

func (e *Engine) Src() string

Src returns the compiled Ruby source (erubi's Engine#src).

type InvalidIndicatorError

type InvalidIndicatorError struct{ Indicator string }

InvalidIndicatorError is what NewEngine/NewCaptureEndEngine panics with when a custom Regexp yields a tag indicator the engine does not know how to handle, mirroring the ArgumentError raised by Erubi::Engine#handle. It is only reachable with a caller-supplied Regexp; the built-in regexps never produce an unknown indicator.

func (*InvalidIndicatorError) Error

func (e *InvalidIndicatorError) Error() string

type Options

type Options struct {
	// Escape makes <%= %> auto-HTML-escape (via EscapeFunc) and inverts the
	// <%= %> / <%== %> semantics. nil falls back to EscapeHTML, then false.
	Escape *bool
	// EscapeHTML is the lower-priority alias for Escape (erubi's :escape_html).
	EscapeHTML *bool
	// Trim enables erubi's whitespace-trim rules for <% %> / <%- -%> and the
	// comment tag. nil means the erubi default, true.
	Trim *bool
	// FreezeTemplateLiterals suffixes each literal text chunk with .freeze.
	// nil means the erubi default, true.
	FreezeTemplateLiterals *bool

	// Freeze prepends a "# frozen_string_literal: true" magic comment.
	Freeze bool
	// ChainAppends chains buffer appends (_buf << a << b) for speed.
	ChainAppends bool
	// Ensure wraps the template in begin/ensure restoring the previous BufVar.
	Ensure bool

	// EscapeFunc overrides the escape function name (erubi default resolves to
	// "::Erubi.h", or "__erubi.h" with a hoisted "__erubi = ::Erubi;" when
	// Escape is set). Empty means use that default.
	EscapeFunc string
	// BufVar names the buffer variable. Empty defaults to OutVar, then "_buf".
	BufVar string
	// OutVar is the lower-priority alias for BufVar (erubi's :outvar).
	OutVar string
	// BufVal is the buffer's initial value expression. Empty defaults to
	// "::String.new".
	BufVal string
	// Filename is the template filename, reported by Engine.Filename.
	Filename string
	// Preamble overrides the emitted preamble. Empty defaults to
	// "<BufVar> = <BufVal>;".
	Preamble string
	// Postamble overrides the emitted postamble. Empty defaults to
	// "<BufVar>.to_s\n".
	Postamble string
	// LiteralPrefix is emitted for an escaped opening delimiter (the <%% tag).
	// Empty defaults to "<%".
	LiteralPrefix string
	// LiteralPostfix is emitted for an escaped closing delimiter. Empty defaults
	// to "%>".
	LiteralPostfix string
	// Src is the initial source string the compiled output is appended to.
	Src string
	// Regexp overrides the scanning regexp. nil uses DefaultRegexp (or
	// CaptureEndRegexp for NewCaptureEndEngine).
	Regexp *regexp.Regexp

	// EscapeCapture (CaptureEndEngine only) makes <%|= %> escape by default and
	// <%|== %> not. nil defaults to the resolved Escape value.
	EscapeCapture *bool
	// YieldReturnsBuffer (CaptureEndEngine only) makes <%| %> insert the buffer
	// as an expression so the block yields the buffer.
	YieldReturnsBuffer bool
}

Options configures NewEngine and NewCaptureEndEngine, mirroring the keyword arguments accepted by Erubi::Engine.new and Erubi::CaptureEndEngine.new.

The tri-state boolean options are pointers so that "unset" is distinguishable from an explicit false, exactly as a missing Ruby hash key differs from a present false value. Use Bool to set them, e.g. Options{Trim: erubi.Bool(false)}. String options treat the zero value "" as "unset" (falling back to the erubi default), matching erubi's `properties[:x] || default` idiom.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL