Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/go-fft/fft v0.0.0-20260620110530-0e3ca1747acb
github.com/go-images/images v0.0.0-20260620184442-aa6cd1c0beb7
github.com/go-ndarray/ndarray v0.0.0-20260620170009-555bfc31e7a3
github.com/go-ruby-erb/erb v0.0.0-20260629074717-0999ae4dd529
github.com/go-ruby-marshal/marshal v0.0.0-20260622114304-27ed1baddd9f
github.com/go-ruby-parser/parser v0.0.0-20260626192347-1bbe8b4672d0
github.com/go-ruby-regexp/regexp v0.0.0-20260628133338-5a3a8b96feac
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ndarray/ndarray v0.0.0-20260620170009-555bfc31e7a3 h1:oqUg3BkpFqPqIhLG1RXFW4gpBGWN4mq5FyOqOvjc6gg=
github.com/go-ndarray/ndarray v0.0.0-20260620170009-555bfc31e7a3/go.mod h1:CZPf0P3FjwYlphONFD0ZAjE+/eaixCZYvYhm6QqmqsU=
github.com/go-ruby-erb/erb v0.0.0-20260629074717-0999ae4dd529 h1:md/l6ifEY6S0Iy1nBk5hVt5BhnWJRqhB08gVEsVvS28=
github.com/go-ruby-erb/erb v0.0.0-20260629074717-0999ae4dd529/go.mod h1:HEJVI7jQ/BnjiSiu2Wj934QboEAgdv47RNOqvZb+nRw=
github.com/go-ruby-marshal/marshal v0.0.0-20260622114304-27ed1baddd9f h1:IIBuHgiQJbssGPhrUbagvP74febWhmKxiNk7IQq1//g=
github.com/go-ruby-marshal/marshal v0.0.0-20260622114304-27ed1baddd9f/go.mod h1:UpjSgWGcQSNt0ARWuHKwf0P55LkwZ65vuqoRPSKctZ8=
github.com/go-ruby-parser/parser v0.0.0-20260626192347-1bbe8b4672d0 h1:Q3ryvK9hPcbvX6uBw4ZpTKFIDkaGBVEtjLtxXaruP6Y=
Expand Down
1 change: 1 addition & 0 deletions internal/vm/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ func (vm *VM) bootstrap() {
vm.registerRipper() // ripper loadable shell (Ripper.sexp etc. raise); needs StandardError
vm.registerSyslog() // Syslog loadable shell (feature probe)
vm.registerCGI() // CGI.escape/unescape (real over net/url) + HTML helpers
vm.registerERB() // ERB class skeleton + ERB::Util (backed by go-ruby-erb); prelude adds the Ruby API
vm.registerMonitor() // Monitor/MonitorMixin (single-thread synchronize); needs StandardError
vm.registerZlib() // needs the exception hierarchy (Zlib::Error < StandardError)
vm.registerFiber() // needs the exception hierarchy (FiberError < StandardError)
Expand Down
90 changes: 90 additions & 0 deletions internal/vm/erb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
"github.com/go-embedded-ruby/ruby/internal/object"
erb "github.com/go-ruby-erb/erb"
)

// registerERB installs the native backbone of the ERB standard library: the ERB
// class with a private native compiler hook and the ERB::Util module of escaping
// helpers. The template scanner/compiler lives entirely in the pure-Go
// github.com/go-ruby-erb/erb library (mirroring MRI's ERB::Compiler byte for
// byte); the interpreter-bound pieces — initialize / result / result_with_hash —
// stay in Ruby (prelude.rb) because they need a Binding and eval. ERB::Util's
// pure functions (html_escape / url_encode and their h / u aliases) bind
// straight through to the library so the escaping output also comes from one
// authoritative source.
//
// The class and module are created here so the native hook __compile can be a
// private instance method; the prelude reopens ERB to add the public API.
func (vm *VM) registerERB() {
cERB := newClass("ERB", vm.cObject)
vm.consts["ERB"] = cERB

// __compile(template, trim_mode, eoutvar) -> src. The single bridge into the
// library: it returns the Ruby source the compiled template evaluates to
// (already carrying the "#coding:UTF-8\n" magic prefix). A nil trim_mode is
// the no-trim default; eoutvar names the output buffer. The library never
// fails on template syntax, but a malformed-option error is surfaced as an
// ArgumentError so the contract stays total on the Ruby side.
cERB.methods["__compile"] = &Method{name: "__compile", owner: cERB, native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
template := strArg(args[0])
trimMode := optStrArg(args[1])
eoutvar := strArg(args[2])
src, _, err := erbCompile(template, erb.Options{TrimMode: trimMode, EOutVar: eoutvar})
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return object.NewString(src)
}}

// ERB::Util — html_escape / url_encode (with the one-letter h / u aliases)
// bound to the library's pure functions, callable as module functions
// (ERB::Util.h(...)). MRI coerces non-strings with to_s first; the conversion
// happens here via toS so the library sees a String.
util := newClass("Util", nil)
util.isModule = true
cERB.consts["Util"] = util
udef := func(name string, fn NativeFn) { util.smethods[name] = &Method{name: name, owner: util, native: fn} }

htmlEscape := func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(erb.HTMLEscape(erbToS(vm, args[0])))
}
urlEncode := func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(erb.URLEncode(erbToS(vm, args[0])))
}
udef("html_escape", htmlEscape)
udef("h", htmlEscape)
udef("url_encode", urlEncode)
udef("u", urlEncode)
}

// erbCompile is the seam over the go-ruby-erb compiler. The library never fails
// on a well-formed template (Compile's err is reserved for genuinely malformed
// options, unreachable through this binding), so the error branch is exercised
// only by swapping this var in a fault-injection test.
var erbCompile = erb.Compile

// optStrArg returns the String value of v, or "" when v is nil — ERB's trim_mode
// keyword defaults to nil (no trimming), which the library spells as the empty
// trim string.
func optStrArg(v object.Value) string {
if _, isNil := v.(object.Nil); isNil {
return ""
}
return strArg(v)
}

// erbToS coerces an ERB::Util argument to its String form the way MRI does,
// invoking #to_s on non-strings (so nil -> "", 123 -> "123"). A String is used
// verbatim.
func erbToS(vm *VM, v object.Value) string {
if s, ok := v.(*object.String); ok {
return string(s.B)
}
return string(vm.send(v, "to_s", nil, nil).(*object.String).B)
}
29 changes: 29 additions & 0 deletions internal/vm/erb_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
"errors"
"testing"

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

// TestERBCompileError covers the otherwise-unreachable Compile-error branch of
// the native __compile hook. go-ruby-erb never fails on a well-formed template,
// so the failure is injected through the erbCompile seam; the binding must
// surface it as a Ruby ArgumentError carrying the error's message.
func TestERBCompileError(t *testing.T) {
saved := erbCompile
defer func() { erbCompile = saved }()
erbCompile = func(string, erb.Options) (string, string, error) {
return "", "", errors.New("boom")
}

got := runFS(t, `require "erb"; begin; ERB.new("x"); rescue => e; p [e.class, e.message]; end`)
if want := "[ArgumentError, \"boom\"]\n"; got != want {
t.Errorf("got=%q want=%q", got, want)
}
}
Loading
Loading