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
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 (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 (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
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 nameid:14B (bytes 18-31): null-padded idparent:i32 (byte 32): index of parent node, -1 for root siblingsfirst_child:i32 (byte 36): index of first child, -1 if nonenext_sib:i32 (byte 40): index of next sibling, -1 if nonereserved (bytes 44-63)
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.