builder

package module
v0.0.0-...-9662d41 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: 5 Imported by: 0

README

go-ruby-builder/builder

builder — go-ruby-builder

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's builder gemBuilder::XmlMarkup, the programmatic XML/markup generator. It emits markup byte-for-byte identical to builder 3.3.0 running on MRI (Ruby ≥ 4.0), validated by a differential oracle against the gem — without any Ruby runtime.

It is a markup backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-erb (the ERB compiler) and go-ruby-regexp (the Onigmo engine).

method_missing, without method_missing. Ruby drives Builder through method_missing: xml.person { xml.name("Alice") } turns the missing method name into an element name. Go has no method_missing, so this package exposes the same emitter through an explicit, dynamic API — Tag(name, …) — that the host drives: the rbgo binding intercepts a Ruby method_missing(name, *args, &block) and calls Tag(name, …), so element naming still comes straight from the missing Ruby method.

Features

Faithful port of Builder::XmlMarkup, validated against the gem on every supported platform:

  • Elements — nested elements via blocks, attributes (insertion-ordered, attribute-escaped), text content (character-data-escaped), and self-closing empty tags (<br/>). An empty-string body emits <a></a>, no body emits <a/> — exactly as the gem distinguishes them.
  • Tag / tag! — the general emitter, including namespace shorthand (NS("ns", "name")<ns:name>) and the gem's "cannot mix a text argument with a block" rule (a panic mirroring MRI's ArgumentError).
  • Text / text!, Append / << (verbatim, unescaped), CData / cdata! (with ]]> splitting), Comment / comment!, Instruct / instruct! (<?xml version="1.0" encoding="UTF-8"?>, ordered version/encoding/standalone), and Declare / declare! (DOCTYPE).
  • Indentation (WithIndent(2)), a starting margin (WithMargin), and an external target sink (WithTarget).
  • MRI-exact escaping — character data escapes only & < >; attribute values additionally escape "&quot;, \n&#10;, \r&#13; (a literal tab and an apostrophe are left intact); invalid XML characters become U+FFFD.

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) and three operating systems.

Install

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

Usage

package main

import (
	"fmt"

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

func main() {
	x := builder.New(builder.WithIndent(2))
	x.Instruct("", nil) // <?xml version="1.0" encoding="UTF-8"?>
	x.Block("person", builder.Of("id", 1), func(x *builder.XmlMarkup) {
		x.Tag("name", "Alice & Bob")
		x.Tag("age", 30)
		x.Comment("a note")
		x.CData("raw <chars> & such")
	})
	fmt.Print(x.Target())
	// <?xml version="1.0" encoding="UTF-8"?>
	// <person id="1">
	//   <name>Alice &amp; Bob</name>
	//   <age>30</age>
	//   <!-- a note -->
	//   <![CDATA[raw <chars> & such]]>
	// </person>
}

API

func New(opts ...Option) *XmlMarkup
func WithIndent(spaces int) Option // Builder::XmlMarkup.new(indent: n)
func WithMargin(level int) Option  // margin: n
func WithTarget(sink *strings.Builder) Option // target:

// The dynamic element emitter method_missing maps onto (the rbgo binding calls
// Tag with the element name taken from the intercepted Ruby method symbol).
func (x *XmlMarkup) Tag(name string, args ...any) string

// Typed conveniences over Tag.
func (x *XmlMarkup) Element(name string, attrs Attrs, content Value) string
func (x *XmlMarkup) Block(name string, attrs Attrs, fn func(*XmlMarkup)) string
func (x *XmlMarkup) NS(ns, name string, args ...any) string // xml.tag!(:ns, :name, …)

func (x *XmlMarkup) Text(v Value) string    // text!  (escaped)
func (x *XmlMarkup) Append(v Value) string  // <<     (verbatim)
func (x *XmlMarkup) CData(text string) string
func (x *XmlMarkup) Comment(text string) string
func (x *XmlMarkup) Instruct(directive string, attrs Attrs) string
func (x *XmlMarkup) Declare(inst string, args ...any) string
func (x *XmlMarkup) Target() string // target!

type Value = any            // string / Symbol / bool / int / *big.Int / float / Raw / …
type Symbol string          // bare name; Sym("html") for declare! identifiers
type Raw string             // verbatim markup, bypasses escaping
type Attr struct{ Key string; Value Value }
type Attrs []Attr
func Of(kv ...any) Attrs     // Of("id", 1, "class", "vip")
How element naming is exposed for rbgo

The gem's xml.person(…) reaches method_missing(:person, …), which calls tag!(:person, …). The rbgo binding does the same across the language boundary: it defines XmlMarkup#method_missing to forward (name, *args, &block) to this package's Tag(name.to_s, …), mapping each Ruby argument to a Go Value — a HashAttrs, a block → func(*XmlMarkup), everything else → text. So the Go side never guesses element names; they arrive from Ruby, unchanged.

Tests & coverage

The suite pairs deterministic, ruby-free golden vectors (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential oracle: a wide corpus (nested elements, attributes, cdata, comments, instruct, indentation, namespaces, escaping) is emitted here and by the builder gem under the system ruby, and the bytes are required to match. The oracle skips itself where ruby/the gem is absent or RUBY_VERSION < "4.0".

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-builder/builder 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 builder is a pure-Go (CGO=0), MRI-faithful reimplementation of Ruby's `builder` gem — Builder::XmlMarkup, the programmatic XML/markup generator.

Ruby drives Builder through method_missing: `xml.person { xml.name("Alice") }` turns the missing method name into an element name. Go has no method_missing, so this package exposes the same emitter through an explicit, dynamic API that a host (go-embedded-ruby / rbgo) drives:

The bytes it emits are byte-for-byte identical to builder 3.3.0 running on MRI (Ruby >= 4.0), validated by a differential oracle against the gem.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Attr

type Attr struct {
	Key   string
	Value Value // rendered with Ruby to_s then attribute-escaped; nil -> ""
}

Attr is a single XML attribute. Builder takes attributes as a Ruby Hash, whose insertion order is preserved in the emitted markup; an ordered slice of Attr reproduces that exactly and avoids Go map's random iteration order.

type Attrs

type Attrs []Attr

Attrs is an ordered attribute list.

func Of

func Of(kv ...any) Attrs

Of builds an Attrs from alternating key/value pairs, a terse form for hosts and tests: Of("id", 1, "class", "vip"). An odd trailing key is given a nil value (rendered as an empty attribute), matching Builder's nil handling.

type Option

type Option func(*XmlMarkup)

Option configures a new XmlMarkup.

func WithIndent

func WithIndent(spaces int) Option

WithIndent sets the number of spaces emitted per nesting level, matching Builder::XmlMarkup.new(indent: n). Zero (the default) emits everything on a single line with no newlines.

func WithMargin

func WithMargin(level int) Option

WithMargin sets the initial indentation level (Builder's margin: option), so the whole document is shifted right by margin*indent spaces. It has no effect unless an indent is also set.

func WithTarget

func WithTarget(sink *strings.Builder) Option

WithTarget directs output to an external *strings.Builder rather than the emitter's own buffer — the Go equivalent of Builder's target: option, which also lets one builder feed another. XmlMarkup.Target still returns the same accumulated text.

type Raw

type Raw string

Raw is markup inserted verbatim, with no escaping — the Go equivalent of Ruby's `xml << "<i>raw</i>"`. Text!/content strings are always escaped; wrap a string in Raw to bypass that.

type Symbol

type Symbol string

Symbol is a Ruby Symbol. As element content or an attribute value it renders as its bare name (Ruby's Symbol#to_s), then XML-escaped like a string.

func Sym

func Sym(name string) Symbol

Sym marks a declaration argument as an unquoted identifier (a Ruby Symbol), as in xml.declare!(:DOCTYPE, :html). It is just Symbol with a shorter name at the declaration call site.

type Value

type Value = any

Value is any Ruby value the builder accepts as element content, text, or an attribute value. The rbgo host maps its own object.Value to and from this small, fixed set; the emitter itself never touches a Ruby runtime.

The concrete shapes that carry meaning are:

nil                 -> no content (self-closing tag) / a nil attribute value
string              -> text, XML-escaped
Symbol              -> a bare name (Ruby to_s), XML-escaped like a string
bool                -> "true" / "false"
int, int64, ...     -> decimal integer
*big.Int            -> arbitrary-precision integer
float64, float32    -> Ruby Float#to_s
Raw                 -> pre-formatted markup inserted verbatim (the `<<` path)

Any other Go value is stringified with valueToString's fallback (Sprint), so the host can hand through a type it has already rendered.

type XmlMarkup

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

XmlMarkup is a Builder::XmlMarkup emitter. Create one with New; write to it with the element and special methods; read the accumulated document with XmlMarkup.Target. It is not safe for concurrent use.

func New

func New(opts ...Option) *XmlMarkup

New creates an XmlMarkup emitter. With no options it behaves like Builder::XmlMarkup.new: no indentation, double-quoted attributes, UTF-8.

func (*XmlMarkup) Append

func (x *XmlMarkup) Append(v Value) string

Append inserts markup verbatim, with no escaping — Builder's `xml << str`.

func (*XmlMarkup) Block

func (x *XmlMarkup) Block(name string, attrs Attrs, fn func(*XmlMarkup)) string

Block emits an element whose body is produced by fn — the block form, xml.name { … }. Attributes are optional.

func (*XmlMarkup) CData

func (x *XmlMarkup) CData(text string) string

CData wraps text in a CDATA section — Builder's cdata!. A literal "]]>" in the text is split as "]]]]><![CDATA[>" so it cannot terminate the section early, exactly as the gem does. The text itself is not escaped.

func (*XmlMarkup) Comment

func (x *XmlMarkup) Comment(text string) string

Comment emits an XML comment — Builder's comment!. The text is not escaped (the gem does not escape comment bodies).

func (*XmlMarkup) Declare

func (x *XmlMarkup) Declare(inst string, args ...any) string

Declare emits a markup declaration such as a DOCTYPE — Builder's declare!. Each arg is emitted per its type: a Symbol (an unquoted identifier, e.g. Sym("html")) prints bare, a string prints double-quoted, and a func (func() or func(*XmlMarkup)) is the internal-subset block, emitted between " [" and "]" with its body nested one level deeper. Other values print double-quoted via to_s.

func (*XmlMarkup) Element

func (x *XmlMarkup) Element(name string, attrs Attrs, content Value) string

Element is a typed convenience over [Tag] for the frequent name+attrs+content shape. A nil content self-closes the tag; a non-nil content (including "") is emitted as escaped text.

func (*XmlMarkup) Instruct

func (x *XmlMarkup) Instruct(directive string, attrs Attrs) string

Instruct emits a processing instruction — Builder's instruct!. Called with no directive it emits the XML declaration <?xml version="1.0" encoding="UTF-8"?>; callers may override version/encoding/standalone (and add more attrs) via opts. A non-"xml" directive emits <?directive …?> with the given attributes.

func (*XmlMarkup) NS

func (x *XmlMarkup) NS(ns, name string, args ...any) string

NS is Builder's namespace shorthand xml.tag!(:ns, :name, …): it joins the namespace prefix and local name with a colon, then behaves like [Tag].

func (*XmlMarkup) Tag

func (x *XmlMarkup) Tag(name string, args ...any) string

Tag emits an element, the general form equivalent to Ruby's xml.tag!(name, …) (and to what method_missing dispatches to for xml.<name>(…)). Arguments after the name are interpreted like Builder does:

  • an Attrs (or a lone Attr) supplies attributes; several are merged;
  • a nil is ignored (Builder only records it under explicit-nil handling, which this emitter, like the gem's default, does not enable);
  • a func(*XmlMarkup) or func() is the nested block; and
  • anything else is text content (multiple are concatenated).

Mixing a text argument with a block panics with the same message MRI raises (ArgumentError: "XmlMarkup cannot mix a text argument with a block"). With neither text nor block the tag self-closes (<name/>); an empty string counts as text, so xml.Tag("a", "") emits <a></a>, matching the gem.

The rbgo binding calls Tag with the element name taken from the intercepted Ruby method_missing symbol, so element naming comes straight from Ruby.

func (*XmlMarkup) Target

func (x *XmlMarkup) Target() string

Target returns the markup accumulated so far — Builder's target!.

func (*XmlMarkup) Text

func (x *XmlMarkup) Text(v Value) string

Text appends escaped character data — Builder's text!. Unlike a string passed to Text, [Append] inserts markup verbatim.

Jump to

Keyboard shortcuts

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