Open Source Docs

Avocet

Fast HTML template engine and eDSL for Scala 3

Design

Core modules, render pipeline, and diff lifecycle


Design

Avocet is a small core engine for building and diffing HTML-like trees in Scala. The key idea is: a template is a function that writes rendering operations into a render context. Depending on the render context implementation, those operations become either:

  • an HTML string (static rendering), or

  • a compact byte-buffer encoded tree that can be diffed to infer changes (virtual-DOM-like).

Modules

  • avocet-core: DSL + optimizer + render contexts (including DiffRenderContext)

  • avocet-events: shared event/id model used by integrations

  • avocet-dom: Scala.js DOM backend that applies diffs to the browser DOM

Mermaid diagram: flowchart LR

The render pipeline

At runtime, an Avocet template is a Document[M] (usually created via the DSL as Node[M], Attr[M], Style[M]). Applying a document means calling methods on a RenderContext:

  • openNode(XmlNs, tag)

  • setAttr(XmlNs, name, value)

  • setStyle(name, value)

  • addTextNode(text)

  • closeNode(tag)

  • addMisc(value) (integration side-channel)

Mermaid diagram: flowchart TD

DiffRenderContext lifecycle (and why finalizeDocument() matters)

DiffRenderContext keeps two internal buffers:

  • lhs: the most recently rendered document (current)

  • rhs: the previous document (baseline)

Diffing is a lifecycle. The important steps are:

  1. Render by applying a Document to the render context (writes ops into lhs)

  2. Call finalizeDocument() (flip lhs for reading + reset traversal state)

  3. Call diff(performer) (read lhs/rhs and call the performer with inferred changes)

  4. Call swap() to make the current buffer become the next baseline

Mermaid diagram: sequenceDiagram

Design note: DiffRenderContext is intentionally stateful. You typically keep one instance per render-loop (single-threaded) and reuse it across updates.

addMisc as an integration hook

addMisc(value) attaches an out-of-band payload to the current element id. Integrations (eg frameworks built on Avocet) use it to collect things like:

  • event handlers (keyed by element id + event type)

  • element ids for imperative access

  • component markers / other metadata