haml

package module
v0.0.0-...-455e870 Latest Latest
Warning

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

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

README

go-ruby-haml/haml

haml — go-ruby-haml

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the Ruby Haml template engine (the haml gem) — the deterministic, interpreter-independent core that turns an indentation-structured Haml template into the Ruby source that renders it, producing the same HTML the gem produces for the same locals.

It is the Haml 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 (indentation nesting, %tag/.class/#id shorthand, attribute hashes, filters, comments, the doctype) is fully deterministic and needs no interpreter, so it lives here as pure Go. The final eval(compiled_src) that runs any embedded Ruby (= expressions, - control, #{} interpolation, dynamic attribute hashes) does need a Ruby interpreter and stays in the consumer (e.g. rbgo) — this library compiles, the host evaluates. This mirrors the sibling go-ruby-erb design exactly.

Everything static — element structure, literal attributes, .class/#id shorthand, the doctype, HTML/conditional comments, and the :plain/:css/ :javascript/:escaped/:preserve filters — is resolved at compile time into literal HTML runs, so a template with no embedded Ruby renders with no interpreter at all.

Features

Validated against the haml gem (7.x, Ruby ≥ 4.0) on every supported platform:

  • Elements & shorthand%tag, .class/#id (div default), %tag.c1.c2#id, class-merge (space) and id-merge (_) between shorthand and attribute hashes.
  • Attributes — Ruby-hash %a{href: "x"}, HTML-style %a(href="x") and object references %tr[@user] / %tr[@user, :prefix]; symbol keys, hashrocket keys, data: nested-hash expansion to data-*, numeric/string/true/false/ nil literals, boolean attributes, alphabetical ordering, escaped values. Non-literal values, dynamic class/id merging and object references are resolved at eval time via ::Haml::HamlAttributes.render / ::Haml::ObjectRef.parse.
  • Content — inline text, = (HTML-escaped Ruby), !=/&= (unescaped/ force-escaped), ~ (preserve), - (control, no output), #{} interpolation (HTML-escaped by default, like the gem), the &/! plain-text escape/unescape markers, \ escape, | multiline continuation.
  • Whitespace control — the > (remove outer) and < (remove inner) whitespace-removal markers, byte-for-byte with the gem.
  • Control flow- if/elsif/else, - case/when, - begin/rescue/ensure and - … do |x| blocks nest correctly and share a single emitted end.
  • Filters:plain, :javascript, :css, :escaped, :preserve, :ruby.
  • Comments & doctype — HTML comments /, silent comments -#, conditional comments /[if IE], and the full doctype table (!!!, !!! 5, !!! 1.1, !!! Strict, !!! Frameset, !!! Mobile, !!! Basic, !!! RDFa, !!! XML) resolved per format.
  • FormatsOptions.Format selects html5 (default), xhtml (self-closing <br />, checked="checked", XML prolog) or html4.
  • Void / self-closing — the HTML5 void set (br, img, input, keygen, menuitem, …) and the explicit %tag/ marker.

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

Deferred, honestly
  • :markdown, :textile, :sass/:scss, :less, :coffee filters need an external rendering engine (a Markdown/Textile/Sass/… library) and so cannot be a pure, dependency-free compile-time transform. The host can register them; this library does not ship them. :cdata (whose exact re-indentation is format-specific) is likewise left to the host. An unrecognised filter compiles to its raw body.
  • ~ / find_and_preserve is compiled as an HTML-escaped =. In Haml 7 escaping runs before preservation, so an escaped ~ can never contain a literal <pre>/<textarea> to preserve — the two are byte-identical, which the oracle confirms. Unescaped preservation (!~) is not implemented.

Everything else in the feature list above matches the gem's rendered HTML byte-for-byte across the html5/xhtml/html4 differential corpus.

Install

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

Usage

package main

import (
	"fmt"

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

func main() {
	src, err := haml.Compile("%p= name\n", haml.Options{})
	if err != nil {
		panic(err)
	}
	fmt.Println(src)
	// _hamlout = ::String.new
	// _hamlout << "<p>"
	// _hamlout << ::Haml::Util.escape_html((name).to_s)
	// _hamlout << "</p>\n"
	// _hamlout
	//
	// Hand it to a Ruby interpreter with `name` in scope:
	//   name = "World"; eval(src)  ->  "<p>World</p>\n"

	// A fully-static template compiles to a single literal append — no interpreter needed:
	src, _ = haml.Compile(".card\n  %h1 Title\n  %p Body", haml.Options{})
	// _hamlout << "<div class=\"card\">\n<h1>Title</h1>\n<p>Body</p>\n</div>\n"
}

Render through a pluggable evaluator (the rbgo seam):

out, err := haml.Render("%p= greeting",
	map[string]string{"greeting": `"hi"`},
	func(rubySrc string, locals map[string]string) (string, error) {
		// go-embedded-ruby/rbgo binds the locals and eval's rubySrc here.
		return rbgo.Eval(rubySrc, locals)
	})

API

type Options struct {
	BufVar   string // output-buffer var name; default "_hamlout"
	EscapeFn string // Ruby escape helper for "="; default "::Haml::Util.escape_html"
	Format   string // "html5" (default), "xhtml" or "html4"
}

// Compile returns the Ruby source that, when eval'd with the template's locals
// in scope, builds and returns the rendered HTML string, matching the `haml` gem.
func Compile(template string, opts Options) (src string, err error)

// Render = Compile + a pluggable Ruby-eval seam (nil eval returns the source).
func Render(template string, locals map[string]string, eval Evaluator) (string, error)
type Evaluator func(rubySource string, locals map[string]string) (string, error)

func HTMLEscape(s string) string // Haml::Util.escape_html

// SyntaxError mirrors the gem's Haml::SyntaxError (e.g. an unterminated
// attribute list).
type SyntaxError struct{ Line, Msg string }
What the host (rbgo) provides at eval time

The compiled source references two runtime symbols the host supplies:

  • ::Haml::Util.escape_html(s) — the five-character HTML escape (overridable via Options.EscapeFn);
  • ::Haml::HamlAttributes.render(format, *hashes) — renders one or more dynamic attribute hashes, accumulating class (space) and id (_) merges across them, expanding data:, handling booleans per format and sorting alphabetically;
  • ::Haml::ObjectRef.parse([obj, prefix]) — derives the class/id of an %tag[obj] object reference.

The reference implementations used by the differential oracle live in testdata/prelude.rb.

Tests & coverage

The suite includes a differential oracle: a wide template corpus (elements, shorthand, static & dynamic attributes, filters, comments, control flow, interpolation, multiline) is rendered both by the system haml gem and by eval'ing our compiled source under the reference prelude, comparing the HTML byte-for-byte. The deterministic, ruby-free golden-source tests alone hold coverage at 100%, so the no-ruby lanes still pass the gate.

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/haml is not available (e.g. the qemu arch lanes), so the cross-arch builds still validate the compiler itself.

License

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

WebAssembly

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, …)

Documentation

Overview

Package haml is a pure-Go (no cgo) reimplementation of the Ruby Haml template engine (the `haml` gem) — the deterministic, interpreter-independent core that turns an indentation-structured Haml template into the Ruby source that, when evaluated against a set of locals, renders the same HTML the gem produces.

It mirrors the go-ruby-erb design: this package COMPILES a template to Ruby source; the final eval that runs any embedded Ruby (`=` expressions, `-` control, `#{}` interpolation, dynamic attribute hashes) needs a Ruby interpreter and is deliberately left to the consumer (go-embedded-ruby/rbgo).

Everything that is static — element structure, `.class`/`#id` shorthand, literal attribute hashes, the doctype, comments, and the `:plain`/`:css`/ `:javascript`/`:escaped`/`:preserve` filters — is resolved at compile time into literal HTML runs, so a template with no embedded Ruby renders with no interpreter at all. That deterministic core is what the ruby-free tests cover to 100%.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Compile

func Compile(template string, opts Options) (src string, err error)

Compile compiles a Haml template into the Ruby source that, when eval'd with the template's locals in scope, builds and returns the rendered HTML string — matching the `haml` gem's rendered output. err is non-nil only for genuinely malformed templates; well-formed templates always compile (the compiled Ruby may still raise at eval time, which is the host's concern).

The returned source assigns the buffer to BufVar, appends every fragment, and evaluates to the buffer as its final expression, so a host renders with `eval(src)` after binding the locals.

func HTMLEscape

func HTMLEscape(s string) string

HTMLEscape replaces the five HTML-significant characters with their entity references, matching Haml::Util.escape_html exactly (note "'" becomes "&#39;"). It is exposed so a host embedding the compiled source can provide the runtime escape helper the emitted Ruby calls.

func Render

func Render(template string, locals map[string]string, eval Evaluator) (string, error)

Render compiles template and renders it via eval, using the supplied evaluator. It is the compile+eval seam that mirrors go-ruby-erb.Render: the pure-Go side compiles to Ruby source and hands it, together with the locals, to eval — a pluggable function a host such as rbgo provides. When eval is nil, Render returns the compiled source unchanged as a convenience for callers that only need the source.

locals maps a local-variable name to the Ruby literal source for its value (e.g. "name" -> `"World"`); the evaluator is expected to bind them before eval'ing the compiled source.

func TrimTrailingNewline

func TrimTrailingNewline(s string) string

TrimTrailingNewline reports whether s ends in a newline; a small helper used by hosts that want to normalise the buffer. It is part of the public surface so the rendering contract (Haml always emits a trailing newline per line) is documented and testable.

Types

type Evaluator

type Evaluator func(rubySource string, locals map[string]string) (string, error)

Evaluator is the Ruby-eval seam: given compiled Ruby source and the locals to bind, it returns the rendered string. go-embedded-ruby/rbgo supplies a real implementation; the pure-Go tests use a tiny deterministic shim for the interpreter-free cases.

type Options

type Options struct {
	// BufVar names the output-buffer local variable the compiled source appends
	// to. When empty it defaults to "_hamlout".
	BufVar string

	// EscapeFn names the Ruby method the compiled source calls to HTML-escape an
	// interpolated `=` expression. When empty it defaults to
	// "::Haml::Util.escape_html", the helper the gem uses and that the host
	// (rbgo) provides at eval time.
	EscapeFn string

	// Format selects the output format, mirroring the gem's :format option: it
	// governs the doctype table, void/self-closing tag rendering (html5 "<br>"
	// vs xhtml "<br />") and boolean-attribute rendering (html5 bare "checked"
	// vs xhtml `checked="checked"`). Valid values are "html5" (the default when
	// empty, matching the gem), "xhtml" and "html4".
	Format string
}

Options configures Compile.

type SyntaxError

type SyntaxError struct {
	Line string
	Msg  string
}

SyntaxError reports a malformed Haml template, mirroring the gem's Haml::SyntaxError. Line carries the offending source line.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

Jump to

Keyboard shortcuts

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