go / haml renderer
I ported the ruby / haml renderer to Go
as the haml package. It allows Go web routes and background jobs
(eds-jobs) to render the exact same .haml files as the
Ruby web application without invoking a Ruby process.
Both renderers accept the same "dumb Haml" subset and produce the same HTML for the same inputs.
The API
The Go API is minimal, exposing Parse and Render:
import "eds/haml"
// Parse template once at startup
tmpl, err := haml.Parse(sourceCode, "views/companies/index.haml")
if err != nil {
log.Fatal(err)
}
// Render with a context map and optional partial resolver
locals := map[string]any{
"name": "Acme Corp",
"score": 9.4,
}
html, err := tmpl.Render(locals, func(name string, partialLocals map[string]any) (string, error) {
// Resolve = render "partial" calls
return renderPartial(name, partialLocals)
})
Structure-Aware AST
Like the Ruby version, the parser constructs an Abstract Syntax Tree (AST) rather than concatenating strings:
type Template struct {
path string
nodes []node
}
type node struct {
kind nodeKind
indent int
text string // static text, output expressions
tag string // %tag
classes []string // .class
id string // #id
children []node
}
This prevents invalid HTML nesting. Since indentation defines nesting, tag open/close pairings are mathematically guaranteed.
Security Model
The Go port enforces the same strict safety invariants:
- Escaping by Default:
= fieldHTML-escapes output viahtml.EscapeString. - No Raw Syntax:
!=is a parse error. The only unescaped paths are trustedhaml.SafeStringvalues (returned by the layout or CSRF helpers) and the engine's transform builtins. - No Arbitrary Code: The parser rejects ternaries, variable assignments, method calls, and constant lookups. Templates only read pre-computed data from the context.
- Secure Transforms: Rich text renders exclusively through built-in
engine transforms:
= markdown(field),= slack(field), and= search_highlight(field). The engine processes and sanitizes these values directly.