nx_layout_from_dom.nx source
↩ module page · 630 lines · 35204 B
1// nx_layout_from_dom.nx -- build a LayoutBox tree from an HTML byte
2// stream by streaming nx_html_tokenizer tokens. Phase 3 fourth
3// primitive of NISHI_BROWSER_ROADMAP. THE bridge primitive between
4// Phase 1 (HTML parse) and Phase 3 (layout). Closes the gap that
5// prior layout smokes covered with hand-built fixtures.
6//
7// Algorithm: streaming token walk with a parent-stack arena.
8//
9// 1. Append a synthetic root LayoutBox of kind BLOCK at index 0.
10// (Production wraps this around a viewport-sized "html" box.
11// Phase 3b will detect the actual <html> / <body> root.)
12// 2. Maintain a stack of "current parent" LayoutBox indices.
13// Root is the bottom of the stack.
14// 3. For each token from the HTML tokenizer:
15// START_TAG -> create LayoutBox with kind from
16// nx_layout_default_display(tag name);
17// attach to current parent; push onto stack.
18// END_TAG -> pop the stack (back to parent).
19// SELF_CLOSING -> create LayoutBox; attach; do NOT push.
20// TEXT -> create LayoutBox of kind TEXT with the
21// text body offset/length recorded via
22// nx_layout_box_set_text.
23// DOCTYPE -> skip.
24// COMMENT -> skip.
25// UNKNOWN -> skip.
26// EOF -> end the walk.
27//
28// The output LayoutTree is ready for nx_layout_block to lay out
29// (provided cascade decls are computed via nx_css_apply against
30// the same source buffer first).
31//
32// What it does NOT handle yet (Phase 3b queued, explicit in header):
33// - HTML5 implicit-tag rules (auto-close on certain transitions,
34// e.g. <li><li> -> implicit </li> before second <li>; <p><div>
35// -> implicit </p>; <table> children must be <tr>; etc.)
36// - foster parenting of stray text inside <table>
37// - HTML5 raw-text content (script / style bodies are tokenized
38// as TEXT by nx_html_tokenizer's CAPABILITY_COMPLETENESS=PARTIAL;
39// CSS interpretation lives in Phase 2)
40// - foreign content (SVG, MathML namespace switches)
41// - implicit <html>/<head>/<body> insertion (we create a synthetic
42// root and let parsed tags hang off it)
43// - mismatched end tags (currently pops regardless; production
44// would walk the stack to find a matching open tag)
45//
46// Per cardinal feedback-honest-perf-verdict-no-aspirational-claims:
47// gap list is EXACT in this header.
48//
49// genealogy_id: w3c_html5_parser_tree_construction +
50// substrate_browser_phase_3_html_to_layout
51// lineage_id: nishi_browser_layout_from_dom_v1
52//
53// nx_safety_envelope:
54// intended_use: "HTML token stream -> LayoutBox tree"
55// sil_target: SIL1
56// evidence: [bounded_token_stream,
57// bounded_stack_arena,
58// no_internal_allocation_beyond_caller_arrays,
59// sealed_token_kind_dispatch]
60// verdict: NOT_YET_EVALUATED
61
62import "nx_syscalls.nx"
63import "nx_html_tokenizer.nx"
64import "nx_layout_box.nx"
65import "nx_layout_default_display.nx"
66const NX_MAGIC_65536: i64 = 65536
67
68// Parent-stack arena -- caller-supplied so the primitive owns no
69// allocation. Max depth ~64 is enough for andelinwest.com-class
70// pages; deeper trees can grow this.
71struct LayoutFromDomStack {
72 indices: *i64,
73 capacity: i64,
74 depth: i64
75}
76
77const NX_LAYOUT_FROM_DOM_STACK_BYTES: i64 = 24
78
79// ---- helpers ----
80
81func _lfd_push(stk: *LayoutFromDomStack, idx: i64) -> i64 {
82 if stk.depth >= stk.capacity { return -1 }
83 stk.indices[stk.depth] = idx
84 stk.depth = stk.depth + 1
85 return 0
86}
87
88func _lfd_pop(stk: *LayoutFromDomStack) -> i64 {
89 if stk.depth <= 1 { return -1 } // never pop the root
90 stk.depth = stk.depth - 1
91 return 0
92}
93
94func _lfd_top(stk: *LayoutFromDomStack) -> i64 {
95 if stk.depth == 0 { return -1 }
96 return stk.indices[stk.depth - 1]
97}
98
99// Kind of the box currently on top of the parent stack (-1 if none).
100func _lfd_top_kind(tree: *LayoutTree, stk: *LayoutFromDomStack) -> i64 {
101 let t: i64 = _lfd_top(stk)
102 if t < 0 { return -1 }
103 let b: *LayoutBox = (tree.boxes as *u8 + (t as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
104 return b.kind
105}
106
107// ---- public API ----
108
109// Initialize the parent-stack arena.
110func nx_layout_from_dom_stack_init(stk: *LayoutFromDomStack,
111 indices: *i64, capacity: i64) -> i64 {
112 stk.indices = indices
113 stk.capacity = capacity
114 stk.depth = 0
115 return 0
116}
117
118// Build a LayoutBox tree from an HTML byte stream. Returns the index
119// of the synthetic root LayoutBox (always 0 on success, -1 on error).
120//
121// `src` is the HTML source buffer; cursor walks it via nx_html_next_token.
122// `tree` is a pre-initialized LayoutTree (via nx_layout_tree_init).
123// `stk` is a pre-initialized LayoutFromDomStack with capacity >= max
124// nesting depth.
125func _lfd_lc(b: i64) -> i64 { if b >= 65 { if b <= 90 { return b + 32 } } return b }
126func _lfd_name_eq(src: *u8, off: i64, len: i64, lit: *u8) -> i64 {
127 var ll: i64 = 0
128 while lit[ll] != (0 as u8) { ll = ll + 1 }
129 if len != ll { return 0 }
130 var i: i64 = 0
131 while i < ll { if _lfd_lc(src[off+i] & 0xff) != _lfd_lc(lit[i] & 0xff) { return 0 } i = i + 1 }
132 return 1
133}
134// Should this element's TEXT be suppressed from layout? script/style = raw code, head/title/noscript =
135// non-body metadata. Real browsers execute/apply these, never render their source as body text.
136func _lfd_suppress_tag(src: *u8, off: i64, len: i64) -> i64 {
137 if _lfd_name_eq(src, off, len, "script\x00" as *u8) == 1 { return 1 }
138 if _lfd_name_eq(src, off, len, "style\x00" as *u8) == 1 { return 1 }
139 if _lfd_name_eq(src, off, len, "head\x00" as *u8) == 1 { return 1 }
140 if _lfd_name_eq(src, off, len, "title\x00" as *u8) == 1 { return 1 }
141 if _lfd_name_eq(src, off, len, "noscript\x00" as *u8) == 1 { return 1 }
142 return 0
143}
144// RETIRED bare-tag drop (2026-07-21): <nav>/<aside> used to be dropped WHOLESALE here, on the premise
145// "without the page's own CSS we can't arrange these". That premise is obsolete -- the engine now runs
146// the real cascade (var/clamp/grid/flex/atomic inline-block), and every modern site's PRIMARY nav is a
147// <nav> of inline-block pills, so the wholesale drop blanked real-page navigation (proven by shot).
148// Chrome suppression stays via the PRECISE signals below: role="navigation" (what MediaWiki marks its
149// TOC/menus with), chrome marker classes, and inline display:none. Rule 25: improve, never strip.
150func _lfd_chrome_tag(src: *u8, off: i64, len: i64) -> i64 {
151 return 0
152}
153// Does the tag source contain role="navigation" (ARIA standard for nav landmarks -- the TOC, language
154// lists, menus mark themselves this way even when the element is a <div>)? Scans for role= then checks
155// the quoted value == navigation. Self-contained (no DOM-query dependency in this core organ).
156func _lfd_has_role_nav(src: *u8, src_off: i64, src_len: i64) -> i64 {
157 let end: i64 = src_off + src_len
158 var i: i64 = src_off
159 while i + 5 <= end {
160 if (src[i]&0xff)==114 { if (src[i+1]&0xff)==111 { if (src[i+2]&0xff)==108 { if (src[i+3]&0xff)==101 { if (src[i+4]&0xff)==61 {
161 var j: i64 = i + 5
162 if j < end { let q: i64 = src[j]&0xff; if q==34 { j = j + 1 } else { if q==39 { j = j + 1 } } }
163 if j + 10 <= end { if _lfd_name_eq(src, j, 10, "navigation\x00" as *u8) == 1 { return 1 } }
164 } } } } }
165 i = i + 1
166 }
167 return 0
168}
169// Does src[off..off+len) contain the NUL-terminated literal `lit` (case-sensitive substring)? Used to
170// detect chrome marker CLASSES in a start tag's source (e.g. "vector-dropdown"). Self-contained.
171func _lfd_src_has_sub(src: *u8, off: i64, len: i64, lit: *u8) -> i64 {
172 var ll: i64 = 0
173 while lit[ll] != (0 as u8) { ll = ll + 1 }
174 if ll == 0 { return 1 }
175 let end: i64 = off + len
176 var i: i64 = off
177 while i + ll <= end {
178 var j: i64 = 0
179 var ok: i64 = 1
180 while j < ll { if (src[i+j]&0xff) != (lit[j]&0xff) { ok = 0; j = ll } else { j = j + 1 } }
181 if ok == 1 { return 1 }
182 i = i + 1
183 }
184 return 0
185}
186// Collapsed-DROPDOWN chrome (Vector skin language selector / tools / page menus): rendered EXPANDED by
187// our subset (we can't honor the checkbox-collapse hack), and the page's OWN fetched CSS RE-SHOWS them
188// even when our UA sheet sets display:none -- so we drop the subtree at LAYOUT level (before any CSS),
189// the same way <nav>/<aside> are dropped. Matches class tokens "vector-dropdown" (the dropdown wrapper)
190// and "interlanguage-link" (the interwiki list items) in the start tag source.
191func _lfd_has_chrome_class(src: *u8, src_off: i64, src_len: i64) -> i64 {
192 if _lfd_src_has_sub(src, src_off, src_len, "vector-dropdown\x00" as *u8) == 1 { return 1 }
193 if _lfd_src_has_sub(src, src_off, src_len, "interlanguage-link\x00" as *u8) == 1 { return 1 }
194 return 0
195}
196// Does the start tag carry an inline style="...display:none..."? We don't run a full inline-style cascade,
197// but display:none is the dominant inline use (hidden metadata: shortdescription, JS-toggled panels, ...).
198// The page MEANS it hidden in the initial paint (no JS here to reveal it), so drop the subtree at layout
199// level. Scans the start-tag source for the "display:none" / "display: none" substring.
200func _lfd_inline_hidden(src: *u8, src_off: i64, src_len: i64) -> i64 {
201 if _lfd_src_has_sub(src, src_off, src_len, "display:none\x00" as *u8) == 1 { return 1 }
202 if _lfd_src_has_sub(src, src_off, src_len, "display: none\x00" as *u8) == 1 { return 1 }
203 return 0
204}
205// A START token is chrome (a dropped subtree) if it's <nav>/<aside> OR carries role="navigation" OR a
206// collapsed-dropdown chrome class the page's CSS would re-show OR an inline style="display:none".
207func _lfd_is_chrome_start(src: *u8, name_off: i64, name_len: i64, src_off: i64, src_len: i64) -> i64 {
208 if _lfd_chrome_tag(src, name_off, name_len) == 1 { return 1 }
209 if _lfd_has_role_nav(src, src_off, src_len) == 1 { return 1 }
210 if _lfd_has_chrome_class(src, src_off, src_len) == 1 { return 1 }
211 if _lfd_inline_hidden(src, src_off, src_len) == 1 { return 1 }
212 return 0
213}
214// Find attribute `name=` inside a start tag's source span; writes the VALUE span to out[0]=off,
215// out[1]=len (out[0]=-1 when absent). The attribute name must be preceded by whitespace so
216// `data-value=` can never match `value=`. Handles double-quoted, single-quoted, and bare values.
217// Self-contained scanner in the from_dom local-helper style (no DOM-query import in this core organ).
218func _lfd_attr_val(src: *u8, src_off: i64, src_len: i64, name: *u8, out: *i64) -> i64 {
219 out[0] = 0 - 1
220 out[1] = 0
221 var nl: i64 = 0
222 while name[nl] != (0 as u8) { nl = nl + 1 }
223 let end: i64 = src_off + src_len
224 var i: i64 = src_off + 1
225 while i + nl + 1 <= end {
226 var ws: i64 = 0
227 let pc: i64 = src[i-1] & 0xff
228 if pc == 32 { ws = 1 }
229 if pc == 9 { ws = 1 }
230 if pc == 10 { ws = 1 }
231 if pc == 13 { ws = 1 }
232 if ws == 1 {
233 var j: i64 = 0
234 var ok: i64 = 1
235 while j < nl { if _lfd_lc(src[i+j] & 0xff) != _lfd_lc(name[j] & 0xff) { ok = 0; j = nl } else { j = j + 1 } }
236 if ok == 1 { if (src[i+nl] & 0xff) == 61 {
237 var v: i64 = i + nl + 1
238 if v >= end { return 0 }
239 let q: i64 = src[v] & 0xff
240 if q == 34 {
241 var e2: i64 = v + 1
242 while e2 < end { if (src[e2]&0xff) == 34 { out[0] = v + 1; out[1] = e2 - (v + 1); e2 = end } else { e2 = e2 + 1 } }
243 return 0
244 }
245 if q == 39 {
246 var e3: i64 = v + 1
247 while e3 < end { if (src[e3]&0xff) == 39 { out[0] = v + 1; out[1] = e3 - (v + 1); e3 = end } else { e3 = e3 + 1 } }
248 return 0
249 }
250 // UNQUOTED attribute value. HTML5 13.2.5.36 ends this state ONLY at tab, LF, FF, CR,
251 // space or '>'. A '/' is an ORDINARY CHARACTER here -- it is the self-closing marker
252 // only where an attribute NAME would start, i.e. straight after whitespace.
253 //
254 // 2026-08-27 -- MEASURED ON danluu.com, BY LOOKING AT THE RENDER. Every article title
255 // painted as plain black text while one link ("Patreon posts") painted blue. The site
256 // writes its hrefs unquoted -- `<a href=https://danluu.com/perf-opt/>` -- and this scan
257 // stopped at the first '/', so every href collapsed to the useless value "https:".
258 // The one link that worked was `href=#pt`, the only href on the page with no slash.
259 // A whole page of links silently became non-links, and nothing failed loudly.
260 // LF/CR/FF were ALSO missing from the terminator set, so an unquoted value followed by
261 // a newline before '>' swallowed the newline and everything after it.
262 var e4: i64 = v
263 var sc4: i64 = 1
264 while sc4 == 1 {
265 if e4 >= end { sc4 = 0 }
266 else { let c4: i64 = src[e4]&0xff
267 var term: i64 = 0
268 if c4 == 32 { term = 1 }
269 if c4 == 62 { term = 1 }
270 if c4 == 9 { term = 1 }
271 if c4 == 10 { term = 1 }
272 if c4 == 12 { term = 1 }
273 if c4 == 13 { term = 1 }
274 if term == 1 { sc4 = 0 } else { e4 = e4 + 1 } }
275 }
276 out[0] = v
277 out[1] = e4 - v
278 return 0
279 } }
280 }
281 i = i + 1
282 }
283 return 0
284}
285// Should this <input>'s value paint as its visible label? Chrome paints the value of text-like and
286// button-like inputs; hidden carries data, password must be masked (we have no masking in this pass),
287// checkbox/radio values are submission tokens ("on"), image/file have no textual face. Absent type =
288// text input = paintable.
289func _lfd_input_paintable(src: *u8, src_off: i64, src_len: i64) -> i64 {
290 let t: *i64 = sys_mmap(16) as *i64
291 _lfd_attr_val(src, src_off, src_len, "type\x00" as *u8, t)
292 if t[0] < 0 { return 1 }
293 if _lfd_name_eq(src, t[0], t[1], "hidden\x00" as *u8) == 1 { return 0 }
294 if _lfd_name_eq(src, t[0], t[1], "password\x00" as *u8) == 1 { return 0 }
295 if _lfd_name_eq(src, t[0], t[1], "checkbox\x00" as *u8) == 1 { return 0 }
296 if _lfd_name_eq(src, t[0], t[1], "radio\x00" as *u8) == 1 { return 0 }
297 if _lfd_name_eq(src, t[0], t[1], "image\x00" as *u8) == 1 { return 0 }
298 if _lfd_name_eq(src, t[0], t[1], "file\x00" as *u8) == 1 { return 0 }
299 return 1
300}
301// VOID ELEMENTS (HTML5 ยง13.1.2): no end tag EVER arrives for these, so pushing one as a container
302// skews the stack -- every later END_TAG pops one level wrong, and the whole rest of the document
303// mis-nests. hackernews' slashless <img src="y18.svg"> in its header detached the story rows from
304// their table (subtext laid at x=999, off-viewport); wikipedia dodged the bug only because MediaWiki
305// emits XHTML-style <img/> (SELF_CLOSING -> never pushed). Probe-proven both ways 2026-07-28.
306func _lfd_is_void_tag(src: *u8, off: i64, len: i64) -> i64 {
307 if _lfd_name_eq(src, off, len, "img\x00" as *u8) == 1 { return 1 }
308 if _lfd_name_eq(src, off, len, "br\x00" as *u8) == 1 { return 1 }
309 if _lfd_name_eq(src, off, len, "hr\x00" as *u8) == 1 { return 1 }
310 if _lfd_name_eq(src, off, len, "input\x00" as *u8) == 1 { return 1 }
311 if _lfd_name_eq(src, off, len, "meta\x00" as *u8) == 1 { return 1 }
312 if _lfd_name_eq(src, off, len, "link\x00" as *u8) == 1 { return 1 }
313 if _lfd_name_eq(src, off, len, "base\x00" as *u8) == 1 { return 1 }
314 if _lfd_name_eq(src, off, len, "area\x00" as *u8) == 1 { return 1 }
315 if _lfd_name_eq(src, off, len, "col\x00" as *u8) == 1 { return 1 }
316 if _lfd_name_eq(src, off, len, "embed\x00" as *u8) == 1 { return 1 }
317 if _lfd_name_eq(src, off, len, "source\x00" as *u8) == 1 { return 1 }
318 if _lfd_name_eq(src, off, len, "track\x00" as *u8) == 1 { return 1 }
319 if _lfd_name_eq(src, off, len, "wbr\x00" as *u8) == 1 { return 1 }
320 if _lfd_name_eq(src, off, len, "param\x00" as *u8) == 1 { return 1 }
321 return 0
322}
323// COLSPAN STASH (seq1159, 2026-07-28): a table cell's colspan attribute must reach the layout, but
324// LayoutBox carries no attributes. `source_node_idx` is written as -1 by every append in this builder
325// and read by nothing downstream, so a cell box with colspan=N carries N there (only when N >= 2 --
326// the -1 default remains "no span" everywhere else). The table column layout in nx_layout_block reads
327// it: a spanning cell occupies N column slots (news.ycombinator.com hides its subtext row behind
328// <td colspan=2>; positional cell counting put the subtext in the votearrow column and inflated it).
329func _lfd_colspan_stash(src: *u8, tok: *HtmlToken, tree: *LayoutTree, elem_idx: i64) -> i64 {
330 let csp: *i64 = sys_mmap(16) as *i64
331 _lfd_attr_val(src, tok.src_off, tok.src_len, "colspan\x00" as *u8, csp)
332 if csp[0] < 0 { return 0 }
333 if csp[1] <= 0 { return 0 }
334 var n: i64 = 0
335 var i: i64 = 0
336 while i < csp[1] {
337 let c: i64 = src[csp[0] + i] & 0xff
338 if c >= 48 { if c <= 57 { n = n * 10 + (c - 48) } }
339 i = i + 1
340 }
341 if n < 2 { return 0 }
342 if n > 16 { n = 16 } // a hostile colspan=9999 cannot wedge the columns
343 let b: *LayoutBox = (tree.boxes as *u8 + (elem_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
344 b.source_node_idx = n
345 return 1
346}
347// INPUT VALUE SYNTHESIS (the google-blank root, 2026-07-28): an <input>'s visible label lives in its
348// value= ATTRIBUTE, not a text node -- google's first screen is a logo image plus
349// <input type=submit value="Google Search"> etc., and a text renderer that ignores input values paints
350// a WHITE VOID (census recall 0 with the words sitting right there in the source). Synthesizing the
351// value bytes as a TEXT child box makes everything downstream work unchanged: inline-block
352// shrink-to-fit sizes the control from its text subtree, the normal text pass paints the label, OCR
353// reads it. Shared by the START_TAG and SELF_CLOSING paths.
354func _lfd_input_value_child(src: *u8, tok: *HtmlToken, tree: *LayoutTree, elem_idx: i64) -> i64 {
355 if _lfd_name_eq(src, tok.name_off, tok.name_len, "input\x00" as *u8) == 0 { return 0 }
356 if _lfd_input_paintable(src, tok.src_off, tok.src_len) == 0 { return 0 }
357 let vsp: *i64 = sys_mmap(16) as *i64
358 _lfd_attr_val(src, tok.src_off, tok.src_len, "value\x00" as *u8, vsp)
359 if vsp[0] < 0 { return 0 }
360 if vsp[1] <= 0 { return 0 }
361 let vtx: i64 = nx_layout_box_append(tree, NX_LAYOUT_BOX_TEXT, -1)
362 if vtx < 0 { return 0 }
363 if nx_layout_box_attach_child(tree, elem_idx, vtx) < 0 { return 0 }
364 nx_layout_box_set_text(tree, vtx, vsp[0], vsp[1])
365 return 1
366}
367// is this text run entirely whitespace? (inter-tag indentation/newlines -> no box, avoids huge gaps)
368func _lfd_is_blank(src: *u8, off: i64, len: i64) -> i64 {
369 var i: i64 = 0
370 while i < len {
371 let c: i64 = src[off+i] & 0xff
372 if c != 32 { if c != 9 { if c != 10 { if c != 13 { if c != 12 { return 0 } } } } }
373 i = i + 1
374 }
375 return 1
376}
377func _lfd_ws(c: i64) -> i64 { if c==32 {return 1} if c==9 {return 1} if c==10 {return 1} if c==13 {return 1} if c==12 {return 1} return 0 }
378// Is the parent's LAST child inline-level? This is the discriminator CSS needs for a whitespace-only
379// text node, and it decides whether words run together.
380//
381// 2026-08-26 -- MEASURED ON news.ycombinator.com, BY LOOKING AT THE RENDER. Its header is
382// `<a ...>Hacker News</a>
383<a href="newest">new</a>` and the story rows are
384// `<a ...>nderjung</a>
385<span class="age">4 hours ago</span>`. The whitespace between the two
386// anchors is its OWN text node, entirely whitespace, so _lfd_is_blank dropped it and the page
387// rendered "Hacker Newsnew" and "nderjung4 hours ago". Under CSS white-space:normal that run
388// collapses to a SINGLE SPACE between inline-level boxes -- it only disappears at the edges of a
389// line box. Dropping it is only correct between BLOCK siblings (source indentation), which is the
390// case _lfd_is_blank was written for; it over-applied to the inline case.
391//
392// The previous sibling is the part of the rule we can answer while streaming (the next element is
393// not parsed yet). That is enough: an inline previous sibling means we are mid-line, so the space
394// is required. First-child whitespace is still skipped, which is also what CSS does.
395func _lfd_last_kid_inline(tree: *LayoutTree, parent_idx: i64) -> i64 {
396 if parent_idx < 0 { return 0 }
397 let p: *LayoutBox = (tree.boxes as *u8 + (parent_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
398 var last: i64 = p.first_child_idx
399 if last < 0 { return 0 }
400 var walking: i64 = 1
401 while walking == 1 {
402 let c: *LayoutBox = (tree.boxes as *u8 + (last as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
403 if c.next_sibling_idx < 0 { walking = 0 } else { last = c.next_sibling_idx }
404 }
405 let lb: *LayoutBox = (tree.boxes as *u8 + (last as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
406 if lb.kind == NX_LAYOUT_BOX_INLINE { return 1 }
407 if lb.kind == NX_LAYOUT_BOX_INLINE_BLOCK { return 1 }
408 if lb.kind == NX_LAYOUT_BOX_TEXT { return 1 }
409 return 0
410}
411// Trim an EDGE whitespace run only when it contains a newline. Rationale: HTML source indentation
412// between a block tag and its text ("\n\t\tNavigation\n\t") contains newlines and MUST go -- a leading
413// '\n' makes nx_paint_text line-break, shoving glyphs into the next row (heading overlap). But the
414// lone space separating adjacent inline text and links ("...to the " + "programming") has NO newline
415// and MUST be kept, or the words run together ("theprogrammingof"). So: newline in the edge run ->
416// trim it; lone space -> keep it. This approximates CSS collapse without a scratch rewrite buffer.
417func _lfd_trim_start(src: *u8, off: i64, end: i64) -> i64 {
418 var s: i64 = off
419 var has_nl: i64 = 0
420 var keep: i64 = 1
421 while keep == 1 {
422 if s >= end { keep = 0 }
423 else {
424 let c: i64 = src[s] & 0xff
425 if _lfd_ws(c) == 1 { if c == 10 { has_nl = 1 } if c == 13 { has_nl = 1 } s = s + 1 }
426 else { keep = 0 }
427 }
428 }
429 if has_nl == 1 { return s }
430 return off
431}
432func _lfd_trim_end(src: *u8, off: i64, end: i64) -> i64 {
433 var e: i64 = end
434 var has_nl: i64 = 0
435 var keep: i64 = 1
436 while keep == 1 {
437 if e <= off { keep = 0 }
438 else {
439 let c: i64 = src[e-1] & 0xff
440 if _lfd_ws(c) == 1 { if c == 10 { has_nl = 1 } if c == 13 { has_nl = 1 } e = e - 1 }
441 else { keep = 0 }
442 }
443 }
444 if has_nl == 1 { return e }
445 return end
446}
447func nx_layout_from_dom(src: *u8, src_len: i64,
448 tree: *LayoutTree,
449 stk: *LayoutFromDomStack) -> i64 {
450 // Synthetic root.
451 let root_idx: i64 = nx_layout_box_append(tree, NX_LAYOUT_BOX_BLOCK, -1)
452 if root_idx < 0 { return -1 }
453 if _lfd_push(stk, root_idx) < 0 { return -1 }
454 // TAG-NAME MIRROR of the open-element stack (indexed by depth; pushes write, pops just shrink).
455 // Powers HTML5-lite end-tag matching below -- without names, every END popped blind, and an end
456 // tag whose element was already resync-popped popped an ANCESTOR (the hackernews mid-row detach:
457 // <center><a><div> -- resync pops the <a> for the div, then </a> popped <center>, </center> popped
458 // the td, </td> popped the row; every element after the votelinks cell escaped its table).
459 let tgo: *i64 = sys_mmap(8 * 128) as *i64
460 let tgl: *i64 = sys_mmap(8 * 128) as *i64
461
462 let cursor: *HtmlCursor = (sys_mmap(48)) as *HtmlCursor
463 nx_html_cursor_init(cursor, src, src_len)
464
465 let tok: *HtmlToken = (sys_mmap(64)) as *HtmlToken
466
467 var safety: i64 = 0
468 let MAX_TOKENS: i64 = NX_MAGIC_65536
469 var keep: i64 = 1
470 var suppress: i64 = 0 // >0 when inside <script>/<style>/<head>/<title>/<noscript>
471 var chrome_skip: i64 = 0 // >0 when inside a dropped <nav>/<aside> chrome subtree
472 while keep == 1 {
473 if safety >= MAX_TOKENS { keep = 0 }
474 else {
475 safety = safety + 1
476 let rc: i64 = nx_html_next_token(cursor, tok)
477 if rc < 0 { return -1 }
478 let k: i64 = tok.kind
479 if k == NX_HTML_TOK_EOF { keep = 0 }
480 else {
481 // chrome-skip gate: drop <nav>/<aside>/role=navigation subtrees (menus, TOC, language
482 // lists). DEPTH-counted -- a role=navigation <div> closes with a generic </div>, so we
483 // count ALL nested tags until the subtree closes; process NOTHING inside (no boxes).
484 var _proc: i64 = 1
485 if chrome_skip > 0 {
486 // void START tags never get an END -- exempt them from the depth count too
487 if k == NX_HTML_TOK_START_TAG { if _lfd_is_void_tag(src, tok.name_off, tok.name_len) == 0 { chrome_skip = chrome_skip + 1 } }
488 if k == NX_HTML_TOK_END_TAG { chrome_skip = chrome_skip - 1 }
489 _proc = 0
490 } else {
491 if k == NX_HTML_TOK_START_TAG {
492 if _lfd_is_chrome_start(src, tok.name_off, tok.name_len, tok.src_off, tok.src_len) == 1 { chrome_skip = 1; _proc = 0 }
493 }
494 }
495 if _proc == 1 {
496 if k == NX_HTML_TOK_START_TAG {
497 let kind: i64 = nx_layout_default_display(src,
498 tok.name_off, tok.name_len)
499 // HTML: a block-level element closes any open inline formatting elements. This
500 // also RESYNCS the parent stack after a malformed/unclosed inline tag, so a block
501 // never ends up (mis)nested under an inline -- which the inline-flow layout would
502 // otherwise render horizontally instead of stacking. (See nx_layout_block.)
503 if kind == NX_LAYOUT_BOX_BLOCK {
504 var _kp: i64 = 1
505 while _kp == 1 {
506 if stk.depth <= 1 { _kp = 0 }
507 else {
508 let _tk: i64 = _lfd_top_kind(tree, stk)
509 if _tk == NX_LAYOUT_BOX_INLINE { _lfd_pop(stk) }
510 else { if _tk == NX_LAYOUT_BOX_INLINE_BLOCK { _lfd_pop(stk) } else { _kp = 0 } }
511 }
512 }
513 }
514 let parent_idx: i64 = _lfd_top(stk)
515 let new_idx: i64 = nx_layout_box_append(tree, kind, -1)
516 if new_idx < 0 { return -1 }
517 if nx_layout_box_attach_child(tree, parent_idx, new_idx) < 0 {
518 return -1
519 }
520 if _lfd_push(stk, new_idx) < 0 { return -1 }
521 if stk.depth <= 128 { tgo[stk.depth - 1] = tok.name_off; tgl[stk.depth - 1] = tok.name_len }
522 _lfd_input_value_child(src, tok, tree, new_idx) // <input value=...> label (no-op for other tags)
523 _lfd_colspan_stash(src, tok, tree, new_idx) // td/th colspan -> source_node_idx (seq1159)
524 if _lfd_is_void_tag(src, tok.name_off, tok.name_len) == 1 { _lfd_pop(stk) } // void: box kept, never a container
525 if _lfd_suppress_tag(src, tok.name_off, tok.name_len) == 1 { suppress = suppress + 1 }
526 // RAW-TEXT elements (script/style/textarea/title): consume the body via the HTML5
527 // raw-text scanner so a bare `<` inside the body (e.g. JS `a<b`) is NOT tokenized as a
528 // start tag whose attribute-skip eats the real </script> -- the google-blank root
529 // (2026-07-16): that ate the closer, left `suppress` stuck >0, and every following
530 // visible text was dropped. The other 3 tokenizer consumers (extract_links/imgs,
531 // render_html) already do this; layout_from_dom was the one that forgot. After
532 // consuming, this element has no children -> pop it and undo the suppress bump.
533 if nx_html_is_raw_text_tag(src, tok.name_off, tok.name_len) == 1 {
534 let rawtok: *HtmlToken = (sys_mmap(64)) as *HtmlToken
535 nx_html_consume_raw_text(cursor, (src as i64 + tok.name_off) as *u8, tok.name_len, rawtok)
536 // CONTRACT (nx_html_tokenizer): consume leaves the cursor AT the `<` of the close
537 // tag -- the NEXT token IS the END_TAG, whose normal handling pops the element and
538 // undoes the suppress bump. The old eager pop+undo here DOUBLE-POPPED: the arriving
539 // END_TAG then popped the PARENT, so every script/style/title amputated one
540 // ancestor level -- body attached to the root on every page, a <script> inside
541 // <body> re-parented all following content to the root (probe-proven 2026-07-28:
542 // x=44 -> 0), and script-bearing containers lost their spines = the "flattening"
543 // that broke hide-subtree and link hit-tests (seq1137). Only an UNCLOSED raw-text
544 // body (consumed to EOF, no END_TAG coming) still needs the eager cleanup.
545 if cursor.pos >= cursor.src_len {
546 if suppress > 0 { suppress = suppress - 1 }
547 _lfd_pop(stk)
548 }
549 }
550 } else {
551 if k == NX_HTML_TOK_END_TAG {
552 if _lfd_suppress_tag(src, tok.name_off, tok.name_len) == 1 { if suppress > 0 { suppress = suppress - 1 } }
553 // HTML5-lite end-tag matching (13.2.6.4-lite, 2026-07-28): find the NEAREST
554 // open element with this tag name. None on the stack -> IGNORE the end tag
555 // (its element was resync-popped -- popping blind here amputated an ancestor
556 // per stray end, the hackernews mid-row detach). Found deeper -> pop THROUGH
557 // it (implied ends for the unclosed elements above it).
558 var mdep: i64 = 0 - 1
559 var si2: i64 = stk.depth - 1
560 while si2 >= 1 {
561 if mdep < 0 {
562 if tgl[si2] == tok.name_len {
563 var mm: i64 = 1
564 var mk2: i64 = 0
565 while mk2 < tok.name_len {
566 if _lfd_lc(src[tgo[si2] + mk2] & 0xff) != _lfd_lc(src[tok.name_off + mk2] & 0xff) { mm = 0; mk2 = tok.name_len } else { mk2 = mk2 + 1 }
567 }
568 if mm == 1 { mdep = si2 }
569 }
570 }
571 si2 = si2 - 1
572 }
573 if mdep >= 1 { while stk.depth > mdep { _lfd_pop(stk) } }
574 } else {
575 if k == NX_HTML_TOK_SELF_CLOSING {
576 let kind_sc: i64 = nx_layout_default_display(src,
577 tok.name_off, tok.name_len)
578 let parent_sc: i64 = _lfd_top(stk)
579 let new_sc: i64 = nx_layout_box_append(tree,
580 kind_sc, -1)
581 if new_sc < 0 { return -1 }
582 if nx_layout_box_attach_child(tree, parent_sc, new_sc) < 0 {
583 return -1
584 }
585 _lfd_input_value_child(src, tok, tree, new_sc) // <input value=...> label
586 _lfd_colspan_stash(src, tok, tree, new_sc) // colspan (seq1159)
587 // Do NOT push for self-closing.
588 } else {
589 if k == NX_HTML_TOK_TEXT { if suppress == 0 { if _lfd_is_blank(src, tok.body_off, tok.body_len) == 0 {
590 // text inside <script>/<style>/<head>/... suppressed; whitespace-only skipped.
591 let parent_t: i64 = _lfd_top(stk)
592 let new_t: i64 = nx_layout_box_append(tree,
593 NX_LAYOUT_BOX_TEXT, -1)
594 if new_t < 0 { return -1 }
595 if nx_layout_box_attach_child(tree, parent_t, new_t) < 0 {
596 return -1
597 }
598 let _t_end: i64 = tok.body_off + tok.body_len
599 let _t_s: i64 = _lfd_trim_start(src, tok.body_off, _t_end)
600 let _t_e: i64 = _lfd_trim_end(src, _t_s, _t_end)
601 nx_layout_box_set_text(tree, new_t,
602 _t_s, _t_e - _t_s)
603 // TEXT is a leaf -- no push.
604 } else {
605 // Whitespace-only run AFTER an inline box: collapse to ONE SPACE
606 // rather than deleting it (see _lfd_last_kid_inline). The byte is
607 // FORCED to 0x20 because a raw newline reaching nx_paint_text is
608 // treated as a hard break -- which is the very failure the blanket
609 // drop was introduced to avoid.
610 if tok.body_len > 0 { if _lfd_last_kid_inline(tree, _lfd_top(stk)) == 1 {
611 let parent_b: i64 = _lfd_top(stk)
612 let new_b: i64 = nx_layout_box_append(tree, NX_LAYOUT_BOX_TEXT, -1)
613 if new_b < 0 { return -1 }
614 if nx_layout_box_attach_child(tree, parent_b, new_b) < 0 { return -1 }
615 src[tok.body_off] = 32 as u8
616 nx_layout_box_set_text(tree, new_b, tok.body_off, 1)
617 } }
618 } } }
619 // DOCTYPE / COMMENT / UNKNOWN: skip
620 // (don't create boxes; don't push).
621 }
622 }
623 }
624 }
625 }
626 }
627 }
628
629 return root_idx
630}