Skip to content

Architecture

The engine is a linear pipeline from bytes on the wire to pixels in an image. Each stage is an owned package with a narrow contract, so the pure-logic stages (cascade, layout, DOM) stay testable without a network or fonts.

Packages

Package Role
dom An owned DOM node tree built from golang.org/x/net/html — a thin, owned wrapper so the rest of the engine never depends on html.Node directly.
css A real CSS subset: value model, stylesheet/declaration parser, tag/class/id + descendant/child/sibling combinators + :checked/:not() selectors with specificity, var() custom properties, @media width queries, modern colour, dark-mode, a UA default stylesheet, and cascade + inheritance. Its own small tokenizer/parser, for full control and coverable pure logic.
layout The full box model — block-and-inline flow, floats + clear, flexbox, CSS grid, tables, position (relative/absolute/fixed/sticky), margin collapsing, a greedy word-wrap line-breaker — driven through a Measurer interface so geometry is unit-testable without fonts.
js JavaScript execution via goja bound to a minimal real DOM, with fetch()/XHR and read-back of real laid-out geometry (getBoundingClientRect, offset*, getComputedStyle).
paint Rasterises the box tree to *image.RGBA — AA text (real bold + italic), gradients, border-radius, box-shadow, opacity, images and SVG; also provides the real Measurer backed by go-opentype faces.
engine (root) The public API — Fetch, Render, Screenshot, RenderHTML, RenderWithLinks, RenderInfo — the settle-then-render loop, image + SVG sub-resource loading, and the anchor click hit-map.
cmd/render A single-binary CLI: render -url URL -out shot.png -w 1024 -h 768 (or -file page.html).

The Measurer seam

Layout never touches a font directly. It asks a Measurer for advance widths and line metrics. In production the measurer is backed by go-opentype faces; in tests it is a deterministic fake, so box geometry can be asserted at exact pixel positions with no font files and no anti-aliasing noise. This is why the layout and cascade packages carry very high statement coverage.

Reuse vs build

Everything reused is pure-Go and permissively licensed (BSD-2/BSD-3/MIT), so the whole engine builds with CGO_ENABLED=0 and carries no copyleft.

Reused

Module Role
go-browserhttp/browserhttp http.Client with a Chrome TLS fingerprint, cookie jar and redirect following.
golang.org/x/net/html HTML5 tokenizer + tree builder.
go-opentype/opentype + fonts Advance-width measurement and 8-bit AA glyph masks; embedded OFL sans/serif/mono faces with real bold + italic.
go-widgets/painter FillRect / clip for backgrounds onto the RGBA buffer.
go-images/images PNG/JPEG decode + resize for <img>.
dop251/goja Pure-Go ES5.1 + most-ES2015 JavaScript engine, bound to the DOM by the js package.
srwiley/oksvg + rasterx Pure-Go SVG parse + rasterisation.

Built clean-room — the DOM wrapper, the CSS cascade + inheritance + selector engine, the full box-model layout engine, the DOM binding for JS, and the paint orchestration + public API.

Why not build on a prior pure-Go browser?

The prior pure-Go browser engine opossum (renamed to mycel) was studied in depth. Its choice of permissive parse/selector/tokenize building blocks is worth emulating, but its layout core is explicitly stub-quality (its README calls float/flex layout "just stub implementations"), and its rendering backend is a Plan 9 pixel toolkit (duit) with the DOM exposed as a 9P filesystem — both dead weight for a headless engine. The single hardest piece of a browser, layout, is exactly what it cannot give us, so the layout/box/flow engine here is clean-room. The full prior-art verdict lives in the engine's SURVEY.md.

Portability & testing

Pure Go with CGO_ENABLED=0; cross-built in CI for all six of Go's 64-bit targets (amd64, arm64, riscv64, loong64, ppc64le, s390x). A ratchet coverage gate holds the pure-logic packages at their measured floor and raises it toward 100% as the engine matures; a committed golden PNG covers the offline paint path. The live-network paths are excluded from the gate because their coverage is not reproducible in CI.