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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ require (
github.com/go-ruby-dry-struct/dry-struct v0.0.0-20260702152517-9e25a927a351
github.com/go-ruby-dry-types/dry-types v0.0.0-20260702150052-94ab5100a720
github.com/go-ruby-dry-validation/dry-validation v0.0.0-20260702151810-a274955bddac
github.com/go-ruby-erb/erb v0.0.0-20260629074717-0999ae4dd529
github.com/go-ruby-erb/erb v0.0.0-20260706152133-22f94a380d76
github.com/go-ruby-erubi/erubi v0.0.0-20260706150033-cfeeadde9120
github.com/go-ruby-etcd/etcd v0.0.0-20260704112801-4417ab26c89a
github.com/go-ruby-excon/excon v0.0.0-20260706115718-5ca851d0373f
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ github.com/go-ruby-dry-types/dry-types v0.0.0-20260702150052-94ab5100a720 h1:Lc4
github.com/go-ruby-dry-types/dry-types v0.0.0-20260702150052-94ab5100a720/go.mod h1:S+2r7EA5zp4E8JyaJ88koD8gT7k/JD9i8NGj6V6jIRM=
github.com/go-ruby-dry-validation/dry-validation v0.0.0-20260702151810-a274955bddac h1:ry6nkQY0B7TQfJD34iTIEqVPgThNkcXFreD116X/o74=
github.com/go-ruby-dry-validation/dry-validation v0.0.0-20260702151810-a274955bddac/go.mod h1:LHalD7pLKBYjablQtp55tlPahU5LHCXf0v0mSzmS6zE=
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-erb/erb v0.0.0-20260706152133-22f94a380d76 h1:CenGI7B67vUppMG1+YV78+IgbmKArmokJFStknRlBxU=
github.com/go-ruby-erb/erb v0.0.0-20260706152133-22f94a380d76/go.mod h1:HEJVI7jQ/BnjiSiu2Wj934QboEAgdv47RNOqvZb+nRw=
github.com/go-ruby-erubi/erubi v0.0.0-20260706150033-cfeeadde9120 h1:+CId0Dae0fU9zVcyryFoY8l/cqTpFSEvgv9RBXiZDDU=
github.com/go-ruby-erubi/erubi v0.0.0-20260706150033-cfeeadde9120/go.mod h1:jZJcpxqgDFkKAW7K6Z/WwrWdF7BQ78dYUopimi986ls=
github.com/go-ruby-etcd/etcd v0.0.0-20260704112801-4417ab26c89a h1:aFqb/Uizc/H0UPz6uv5qWpyFVopuPlPjgXevBlV7HMo=
Expand Down
30 changes: 18 additions & 12 deletions internal/vm/sinatra_erb.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ import (
// _erbout buffer and returning the rendered String. That String becomes the Rack
// response body via sinatraResult, exactly as under MRI Sinatra + Tilt.
//
// Trim mode and the output-buffer name match Tilt::ERBTemplate's defaults, which
// Sinatra uses unchanged: trim "<>" (a code-only line's surrounding newlines are
// removed) and eoutvar "_erbout". <%= %> interpolates unescaped (Sinatra does not
// auto-escape unless :erb => { escape_html: true } is configured — a scoped
// follow-up), <% %> is control flow.
// The dialect and the output-buffer name match what Sinatra 4.x actually renders
// with: since Sinatra 2.0 Tilt maps :erb to Tilt::ErubiTemplate, so views compile
// through the erubi gem's Erubi::Engine, not classic MRI ERB. go-ruby-erb's
// ModeErubi reproduces Erubi::Engine byte-for-byte (default engine options: trim
// on, escape off), which is why the Sinatra render selects erb.ModeErubi here
// (erubi has no trim_mode, so TrimMode is not passed) while the standalone
// `require "erb"` / classic-ERB binding in erb.go stays in the default ModeERB.
// The output buffer is eoutvar "_erbout" (Tilt's default). <%= %> interpolates
// unescaped (Sinatra does not auto-escape unless :erb => { escape_html: true } is
// configured — a scoped follow-up), <% %> is control flow. Unlike classic ERB
// trim "<>", erubi keeps the trailing newline of a standalone `<%= … %>` line
// (e.g. a `<%= yield %>` alone on its line in a layout), matching MRI Sinatra.
//
// Two view render paths land: an inline-String template (needs no filesystem)
// and a :symbol template naming views/<sym>.erb (read through the File binding,
Expand All @@ -39,13 +46,8 @@ import (
// (`erb :part` composed inside a view via a helper) and the :erb escape_html /
// app-level `set :erb, layout:` engine options remain a scoped follow-up.

// sinatraTrimMode is the ERB trim mode Sinatra renders with: Tilt::ERBTemplate
// maps a nil/true :trim option (Sinatra's default) to "<>", so a line holding
// only a code tag has both its surrounding newlines trimmed.
const sinatraTrimMode = "<>"

// sinatraEOutVar names the output buffer the compiled template appends to, matching
// Tilt::ERBTemplate's default output variable.
// Tilt::ErubiTemplate's default output variable.
const sinatraEOutVar = "_erbout"

// sinatraErb implements the `erb` view helper on a request context. template is
Expand All @@ -70,7 +72,11 @@ func (vm *VM) sinatraErb(sc *SinatraCtx, args []object.Value) object.Value {
// non-nil, the yield block (used to expose the wrapped view to a layout's
// `<%= yield %>`). It returns the rendered String.
func (vm *VM) sinatraErbRender(sc *SinatraCtx, tmpl string, names []string, vals []object.Value, block *Proc) object.Value {
src, _, err := erbCompile(tmpl, erb.Options{TrimMode: sinatraTrimMode, EOutVar: sinatraEOutVar})
// ModeErubi: Sinatra 4.x renders ERB views through erubi (Tilt::ErubiTemplate),
// so go-ruby-erb compiles in erubi mode to stay byte-identical — notably a
// standalone `<%= yield %>` line keeps its trailing newline (classic ERB trim
// "<>" would drop it). The classic `require "erb"` binding stays in ModeERB.
src, _, err := erbCompile(tmpl, erb.Options{Mode: erb.ModeErubi, EOutVar: sinatraEOutVar})
if err != nil {
// go-ruby-erb never fails on a well-formed template (its err is reserved for
// genuinely malformed options, unreachable through this fixed-option call);
Expand Down
38 changes: 25 additions & 13 deletions scripts/conformance/webapp/apps/sinatra_erb_oracle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@
# ruby 4.0.5). To regenerate the golden vector, run this file with `ruby` and the
# sinatra gem installed.
#
# Trim note (a precisely-scoped go-ruby-erb LIBRARY follow-up, NOT a binding gap):
# Sinatra 4.x maps :erb to Tilt::ErubiTemplate (erubi), whereas go-ruby-erb
# mirrors MRI's ERB::Compiler. Rendered with trim "<>" (Tilt::ERBTemplate's own
# default) the two agree byte-for-byte for single-line templates and idiomatic
# multiline templates (a code-only `<% … %>` alone on its line). They diverge only
# for the unusual line that BOTH opens a code block and ends with an expression
# tag with trailing content (`<% each %>x <%= v %>`), where MRI ERB trims the
# trailing newline and erubi keeps it. This fixture stays inside the agreeing
# subset, so the diff is exact; full erubi trim-parity is tracked upstream in
# go-ruby-erb.
# Dialect note: Sinatra 4.x maps :erb to Tilt::ErubiTemplate (erubi), and rbgo now
# renders Sinatra views through go-ruby-erb's ModeErubi, which reproduces
# Erubi::Engine byte-for-byte. So this fixture exercises erubi's own whitespace
# semantics directly — including a standalone `<%= yield %>` line (route
# /l_standalone) whose trailing newline erubi keeps but classic ERB trim "<>" would
# drop. That case was previously kept inline to dodge the classic-ERB divergence;
# it is now asserted end-to-end, matching the real gem.

require "sinatra/base"
require "tmpdir"
Expand Down Expand Up @@ -93,13 +90,20 @@ class App < Sinatra::Base
)
# The default layout: surrounding markup with `<%= yield %>` interpolating the
# wrapped view and an @ivar the before-filter set (the layout renders in the same
# handler binding as the view). `yield` is kept inline (not alone on a line) so the
# fixture stays inside the ERB/erubi trim-agreement subset the oracle relies on —
# a standalone `<%= %>` line is the one documented go-ruby-erb LIBRARY divergence.
# handler binding as the view). Here `yield` is kept inline within <main>…</main>.
File.write(
File.join(LVIEWS, "layout.erb"),
"<!DOCTYPE html>\n<title>Site</title>\n<main><%= yield %></main>\n<footer>by <%= @who %></footer>\n"
)
# A layout with `<%= yield %>` STANDALONE on its own line — the erubi-specific case
# the classic-ERB oracle used to avoid. Under erubi the yield line keeps its
# trailing newline, so a blank-looking break appears between the wrapped view and
# the footer; classic ERB trim "<>" would have swallowed that newline. Asserting it
# byte-for-byte proves rbgo renders Sinatra views in erubi mode.
File.write(
File.join(LVIEWS, "standalone.erb"),
"<header>H</header>\n<%= yield %>\n<footer>F</footer>\n"
)
# A custom layout selected with `layout: :alt`, using a render local (proving the
# locals reach the layout, not just the view) around `<%= yield %>`.
File.write(
Expand Down Expand Up @@ -140,6 +144,13 @@ class LApp < Sinatra::Base
erb :card, { layout: "<wrap><%= yield %></wrap>\n" }, greeting: "In", items: %w[z]
end

# A layout whose `<%= yield %>` stands ALONE on its own line: erubi keeps that
# line's trailing newline (classic ERB trim "<>" would drop it), so a bare
# newline separates the wrapped view from the footer. The newline-fidelity proof.
get "/l_standalone" do
erb :card, { layout: :standalone }, greeting: "So", items: %w[m]
end

# A missing NAMED layout raises Errno::ENOENT (MRI eats the error only for the
# implicit default layout, not an explicitly requested one).
get "/l_missing_layout" do
Expand Down Expand Up @@ -176,6 +187,7 @@ class LApp < Sinatra::Base
[LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_inline", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }],
[LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_missing_layout", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }],
[LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_missing_view", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }],
[LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_standalone", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }],
]

REQS.each_with_index do |(app, env), i|
Expand Down
25 changes: 19 additions & 6 deletions scripts/conformance/webapp/sinatra_erb_oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import "testing"

// sinatraErbGem421Golden is the response dump of apps/sinatra_erb_oracle.rb
// captured from the REAL `sinatra` gem 4.2.1 (MRI ruby 4.0.5) with its ERB view
// engine: thirteen requests through the classic Sinatra::Base #call(env) adapter,
// each rendering an ERB view (six inline/symbol templates, then seven exercising
// the layout rule) and dumping [status, body]. This is the golden oracle for the
// engine — which is erubi (Tilt::ErubiTemplate), the erubi gem 1.13.1: fourteen
// requests through the classic Sinatra::Base #call(env) adapter, each rendering an
// ERB view (six inline/symbol templates, then eight exercising the layout rule,
// the last a standalone `<%= yield %>` line whose trailing newline erubi keeps but
// classic ERB trim "<>" drops) and dumping [status, body]. This is the golden oracle for the
// templating half of the Sinatra binding: rbgo, rendering through the bound
// go-ruby-erb compiler, must reproduce it byte-for-byte. It is a captured constant
// so the suite stays hermetic (no ruby, no gem, runs under qemu on every 64-bit
Expand Down Expand Up @@ -90,6 +92,16 @@ body<<Errno::ENOENT>>
== req 12 ==
status=200
body<<Errno::ENOENT>>
== req 13 ==
status=200
body<<<header>H</header>
<h2>So World</h2>
<ul>
<li>m</li>
</ul>

<footer>F</footer>
>>
`

// TestSinatraErbGemOracle proves the Sinatra `erb` view helper is MRI-identical
Expand All @@ -101,9 +113,10 @@ body<<Errno::ENOENT>>
// control flow, positional and options[:locals] locals, ERB trim behaviour, and
// the layout rule (default views/layout.erb with `<%= yield %>` returning the
// view, layout: false, a custom layout: :name, layout: true, an inline-String
// layout, and the Errno::ENOENT a missing named layout / view raises) — all
// rendered by rbgo (via the bound go-ruby-erb compiler, evaluated in the
// handler's binding) exactly as by the reference gem.
// layout, a layout with a standalone `<%= yield %>` line — the erubi
// newline-fidelity case — and the Errno::ENOENT a missing named layout / view
// raises) — all rendered by rbgo (via the bound go-ruby-erb compiler in erubi
// mode, evaluated in the handler's binding) exactly as by the reference gem.
func TestSinatraErbGemOracle(t *testing.T) {
if ok, detail := featureLoadable("sinatra/base"); !ok {
t.Fatalf("sinatra/base must be loadable for the ERB oracle: %s", detail)
Expand Down
Loading