← Blog
0.7.0 · APPS · BROWSER · DOM

DOM Builder — html_tokenizer Tokens → DOM Tree (sigil-apps cb2c525)

June 22, 2026 · sigil-apps cb2c525 · Sigil-Docs
browser dom html parser 0.7.0

apps/web/dom_builder.sg (sigil-apps cb2c525) is the T7 sprint deliverable — it converts html_tokenizer output into a full DOM tree. Token walk: StartTag/SelfClose → new node; Attr pairs → class/id fields; EndTag → close active node. A sibling-link pass builds the parent/first_child/next_sib tree at dom_tree_buf @0x780000. DOM-BUILDER-PASS: 4 nodes, 13/13 checks. 20568B arm-el0. This is the bridge that makes the pipeline real: html_tokenizer → dom_builder → style_resolver → layout_box → page_render now flows from a raw HTML byte string all the way to pixels, with no synthetic DOM.


Token walk

StartTag / SelfClose → new node
For each token in the tok_buf (at tok_buf @0x500000, 12B each: type/off/len), the token walker checks type. On StartTag (type=1) or SelfClose (type=5): allocates a new node slot in dom_tree_buf, records tag_name (the token string at src_buf[off]..src_buf[off+len]), zeroes class_name and id. Pushes the new node index onto the element stack (for EndTag matching). On SelfClose: also immediately pops the node (self-closing means no children).
Attr tokens → class / id
On Attr (type=4): reads the name token (previous token) and value token (this token). If name matches "class": copies value string into the current node's class_name field (14-char max). If name matches "id": copies value string into the current node's id field (14-char max). If name matches "href" or any other attribute: ignored (stored nowhere — the pipeline only needs class and id for style_resolver). class_name and id are at fixed offsets within the 64B node descriptor at dom_tree_buf + node_idx × 64.
EndTag → pop stack
On EndTag (type=2): pops the element stack — the current open element is closed. If the stack is empty at EndTag, it's a no-op (malformed HTML tolerance). After the token walk, the element stack is cleared (unclosed elements treated as implicitly closed).

Sibling link pass

Node descriptor format at dom_tree_buf @0x780000
Each node: 64 bytes.
tag_type:i32 (byte 0): node tag index (h1=1, p=2, img=3, a=4, div=5, span=6, …)
class_name:14B (bytes 4-17): null-padded class name
id:14B (bytes 18-31): null-padded id
parent:i32 (byte 32): index of parent node, -1 for root siblings
first_child:i32 (byte 36): index of first child, -1 if none
next_sib:i32 (byte 40): index of next sibling, -1 if none
reserved (bytes 44-63)
Parent/first_child/next_sib link pass
After the token walk produces a flat node array (no tree links yet), the link pass walks all nodes. For each node N: if it has a recorded parent (from the element stack at creation time), sets node[N].parent = parent_idx. For each parent, sets first_child to the first child seen. For consecutive siblings (same parent), sets next_sib on the previous sibling to point to the current. This second pass is O(N) over the node count and produces a fully linked tree.

DOM-BUILDER-PASS test

Test HTML: <h1 class="title">heading</h1><p class="lead">text</p><img id="logo"/><a href="/l">link</a>

Resulting DOM tree (flat siblings, no nesting in this test):

Node tag class id parent first_child next_sib
0 h1 "title" -1 -1 1
1 p "lead" -1 -1 2
2 img "logo" -1 -1 3
3 a -1 -1 -1

13 checks: 4 tag names ✅, 2 class_names (title, lead) ✅, 1 id (logo) ✅, 4 parent=-1 ✅, 4 next_sib links ✅ (nodes 0-2 have next_sib=1/2/3; node 3 has next_sib=-1). DOM-BUILDER-PASS.


Pipeline now real

Before cb2c525, style_resolver.sg and page_render.sg used synthetic DOM arrays hardcoded in the test code. With dom_builder.sg, the full pipeline from raw HTML bytes to pixels is now driven by actual HTML:

raw HTML bytes @ src_buf (0x400000)
  → html_tokenizer (de47df3)     → tok_buf @0x500000
  → dom_builder (cb2c525)        → dom_tree_buf @0x780000
  → style_resolver (6df6641)     → StyleMap @0x700000
  → layout_box (f33524f)         → layout results (border-box x/y/w/h)
  → text_layout (6158863)        → glyph quads / fill rects
  → comp_submit_layer + comp_present → render_buf @0x600000 → display

The next integration step: style_resolver.sg reads DOM nodes from dom_tree_buf instead of a hardcoded synthetic array. Then page_render.sg uses dom_builder output as its DOM input. At that point, editing the HTML bytes at src_buf produces a different rendered page — the full pipeline is end-to-end live.