nx_domtree.nx source
↩ module page · 840 lines · 35765 B
1// nx_domtree.nx -- a MATERIALIZED, MUTABLE DOM tree for the sovereign browser.
2//
3// The JS engine's existing DOM is a READ-ONLY byte snapshot (nx_js_eval: genv[2]=html ptr,
4// queries re-scan the bytes). To RENDER JS-built content, page scripts must MUTATE the DOM and
5// have the result laid out. This organ is that missing piece: parse HTML -> a node arena (REUSING
6// nx_html_tokenizer, no duplicate parser), MUTATE it (createElement/appendChild/textContent=/
7// setAttribute), and SERIALIZE it back to HTML for nx_layout. Rung A of general JS rendering.
8// module: nx_domtree capability: mutable-dom
9// Sovereign: imports only nx_syscalls + the gated nx_html_tokenizer. No gcc/V8/libxml. ORIGINAL.
10import "nx_syscalls.nx"
11import "nx_html_tokenizer.nx"
12const DT_MAGIC_4096: i64 = 4096
13const DT_MAGIC_65536: i64 = 65536
14
15const DT_ELEM: i64 = 1
16const DT_TEXT: i64 = 2
17const DT_DOC: i64 = 3
18const DT_NODE_BYTES: i64 = 96 // 12 i64 fields
19const DT_ATTR_BYTES: i64 = 16 // 2 i64 fields
20const DT_ATTR_CAP: i64 = 32 // max attrs per element (rung-A bound)
21const DT_MAX_NODES: i64 = 131072 // node arena capacity (mmap lazy zero-fill)
22
23struct DomNode {
24 kind: i64, // DT_ELEM / DT_TEXT / DT_DOC
25 parent: i64, // node index, or -1
26 first_child: i64, // node index, or -1
27 last_child: i64, // node index, or -1 (O(1) append)
28 next_sib: i64, // node index, or -1
29 tag: i64, // *u8 owned NUL-term lowercased tag name (DT_ELEM), else 0
30 text: i64, // *u8 owned text bytes (DT_TEXT), else 0
31 text_len: i64,
32 attrs: i64, // *DtAttr owned array, or 0
33 nattrs: i64,
34 voidf: i64, // 1 = void element (img/br/...) -> no close tag, no children
35 jswrap: i64 // cached JS element-wrapper obj ptr (0=none) -> STABLE element identity (dataPriv/events/===)
36}
37struct DtAttr {
38 name: i64, // *u8 owned NUL-term (lowercased)
39 val: i64 // *u8 owned NUL-term
40}
41struct DomTree {
42 nodes: i64, // *u8 node arena base
43 n: i64, // node count
44 cap: i64,
45 root: i64 // root node index (DT_DOC)
46}
47
48// ---- small helpers ----
49func dt_clen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
50
51func dt_ceq(a: *u8, b: *u8) -> i64 {
52 var i: i64 = 0
53 while 1 == 1 {
54 let ca: i64 = a[i] & 0xff
55 let cb: i64 = b[i] & 0xff
56 if ca != cb { return 0 }
57 if ca == 0 { return 1 }
58 i = i + 1
59 }
60 return 0
61}
62// owned NUL-terminated copy of src[off, off+len)
63func dt_dup(src: *u8, off: i64, len: i64) -> i64 {
64 var n: i64 = len
65 if n < 0 { n = 0 }
66 let d: *u8 = sys_mmap(n + 1)
67 var i: i64 = 0
68 while i < n { d[i] = src[off + i]; i = i + 1 }
69 d[n] = 0 as u8
70 return d as i64
71}
72// owned NUL-terminated LOWERCASED copy of src[off, off+len)
73func dt_dup_lower(src: *u8, off: i64, len: i64) -> i64 {
74 var n: i64 = len
75 if n < 0 { n = 0 }
76 let d: *u8 = sys_mmap(n + 1)
77 var i: i64 = 0
78 while i < n {
79 var b: i64 = src[off + i] & 0xff
80 if b >= 65 { if b <= 90 { b = b + 32 } }
81 d[i] = b as u8
82 i = i + 1
83 }
84 d[n] = 0 as u8
85 return d as i64
86}
87func dt_dup_c(s: *u8) -> i64 { return dt_dup(s, 0, dt_clen(s)) }
88func dt_dup_lower_c(s: *u8) -> i64 { return dt_dup_lower(s, 0, dt_clen(s)) }
89
90// ---- node arena ----
91func dt_node(t: *DomTree, idx: i64) -> *DomNode {
92 return ((t.nodes) + idx * DT_NODE_BYTES) as *DomNode
93}
94func dt_alloc(t: *DomTree, kind: i64) -> i64 {
95 let idx: i64 = t.n
96 t.n = t.n + 1
97 let nd: *DomNode = dt_node(t, idx)
98 nd.kind = kind
99 nd.parent = 0 - 1
100 nd.first_child = 0 - 1
101 nd.last_child = 0 - 1
102 nd.next_sib = 0 - 1
103 nd.tag = 0
104 nd.text = 0
105 nd.text_len = 0
106 nd.attrs = 0
107 nd.nattrs = 0
108 nd.voidf = 0
109 nd.jswrap = 0
110 return idx
111}
112func dt_new() -> *DomTree {
113 let t: *DomTree = sys_mmap(32) as *DomTree
114 t.nodes = (sys_mmap(DT_MAX_NODES * DT_NODE_BYTES)) as i64
115 t.n = 0
116 t.cap = DT_MAX_NODES
117 t.root = dt_alloc(t, DT_DOC)
118 return t
119}
120
121// is this lowercased tag a void element?
122func dt_is_void(tag: *u8) -> i64 {
123 if dt_ceq(tag, "img\x00" as *u8) == 1 { return 1 }
124 if dt_ceq(tag, "br\x00" as *u8) == 1 { return 1 }
125 if dt_ceq(tag, "hr\x00" as *u8) == 1 { return 1 }
126 if dt_ceq(tag, "input\x00" as *u8) == 1 { return 1 }
127 if dt_ceq(tag, "meta\x00" as *u8) == 1 { return 1 }
128 if dt_ceq(tag, "link\x00" as *u8) == 1 { return 1 }
129 if dt_ceq(tag, "area\x00" as *u8) == 1 { return 1 }
130 if dt_ceq(tag, "base\x00" as *u8) == 1 { return 1 }
131 if dt_ceq(tag, "col\x00" as *u8) == 1 { return 1 }
132 if dt_ceq(tag, "embed\x00" as *u8) == 1 { return 1 }
133 if dt_ceq(tag, "param\x00" as *u8) == 1 { return 1 }
134 if dt_ceq(tag, "source\x00" as *u8) == 1 { return 1 }
135 if dt_ceq(tag, "track\x00" as *u8) == 1 { return 1 }
136 if dt_ceq(tag, "wbr\x00" as *u8) == 1 { return 1 }
137 return 0
138}
139
140func dt_create_element_span(t: *DomTree, src: *u8, off: i64, len: i64) -> i64 {
141 let idx: i64 = dt_alloc(t, DT_ELEM)
142 let nd: *DomNode = dt_node(t, idx)
143 nd.tag = dt_dup_lower(src, off, len)
144 nd.voidf = dt_is_void(nd.tag as *u8)
145 return idx
146}
147func dt_create_element(t: *DomTree, tagc: *u8) -> i64 {
148 return dt_create_element_span(t, tagc, 0, dt_clen(tagc))
149}
150func dt_create_text(t: *DomTree, src: *u8, off: i64, len: i64) -> i64 {
151 let idx: i64 = dt_alloc(t, DT_TEXT)
152 let nd: *DomNode = dt_node(t, idx)
153 nd.text = dt_dup(src, off, len)
154 var n: i64 = len
155 if n < 0 { n = 0 }
156 nd.text_len = n
157 return idx
158}
159
160func dt_append_child(t: *DomTree, parent: i64, child: i64) -> i64 {
161 let p: *DomNode = dt_node(t, parent)
162 let c: *DomNode = dt_node(t, child)
163 c.parent = parent
164 c.next_sib = 0 - 1
165 if p.first_child == (0 - 1) {
166 p.first_child = child
167 p.last_child = child
168 } else {
169 let last: *DomNode = dt_node(t, p.last_child)
170 last.next_sib = child
171 p.last_child = child
172 }
173 return 0
174}
175// UNLINK child from parent's sibling chain (real Node.removeChild / jQuery .remove()/.detach()/.empty()).
176// Repairs first_child/last_child/prev.next_sib and orphans the child (parent=-1). No-op if not a child.
177func dt_remove_child(t: *DomTree, parent: i64, child: i64) -> i64 {
178 let p: *DomNode = dt_node(t, parent)
179 if p.first_child == child {
180 let c: *DomNode = dt_node(t, child)
181 p.first_child = c.next_sib
182 if p.last_child == child { p.last_child = c.next_sib } // was the only child -> chain now empty (-1)
183 c.parent = 0 - 1
184 c.next_sib = 0 - 1
185 return 0
186 }
187 var prev: i64 = p.first_child
188 while prev != (0 - 1) {
189 let pn: *DomNode = dt_node(t, prev)
190 if pn.next_sib == child {
191 let c: *DomNode = dt_node(t, child)
192 pn.next_sib = c.next_sib
193 if p.last_child == child { p.last_child = prev }
194 c.parent = 0 - 1
195 c.next_sib = 0 - 1
196 return 0
197 }
198 prev = pn.next_sib
199 }
200 return 0
201}
202// deep-clone the subtree rooted at `src` -> new node index (children recursively cloned + re-parented). The
203// node arena is fixed (pre-mmap'd, no realloc), so DomNode ptrs stay valid across dt_alloc during recursion.
204// Used by Element.cloneNode(true) -- jQuery's support-detects clone a populated node + inspect the copy.
205static dt_clone_depth: i64
206func dt_clone_subtree(t: *DomTree, src: i64) -> i64 {
207 if src < 0 { return dt_create_text(t, "\x00" as *u8, 0, 0) }
208 if src >= t.n { return dt_create_text(t, "\x00" as *u8, 0, 0) } // out-of-range index guard
209 if dt_clone_depth > DT_MAGIC_4096 { return dt_create_text(t, "\x00" as *u8, 0, 0) } // cycle/too-deep -> stop (never crash)
210 dt_clone_depth = dt_clone_depth + 1
211 let s: *DomNode = dt_node(t, src)
212 var idx: i64 = 0 - 1
213 if s.kind == DT_TEXT {
214 idx = dt_create_text(t, s.text as *u8, 0, s.text_len)
215 } else {
216 idx = dt_alloc(t, DT_ELEM)
217 let nd: *DomNode = dt_node(t, idx)
218 if s.tag != 0 { nd.tag = dt_dup(s.tag as *u8, 0, dt_clen(s.tag as *u8)) }
219 nd.voidf = s.voidf
220 var ai: i64 = 0
221 while ai < s.nattrs {
222 let ap: *DtAttr = dt_attr_at(s.attrs as *DtAttr, ai)
223 if (ap.name as i64) != 0 { dt_set_attr(t, idx, ap.name as *u8, ap.val as *u8) }
224 ai = ai + 1
225 }
226 var ch: i64 = s.first_child
227 var ccount: i64 = 0
228 while ch != (0 - 1) {
229 if ch < 0 { ch = 0 - 1 } else { if ch >= t.n { ch = 0 - 1 } else { if ccount > DT_MAGIC_65536 { ch = 0 - 1 } else {
230 let cc: i64 = dt_clone_subtree(t, ch)
231 dt_append_child(t, idx, cc)
232 ccount = ccount + 1
233 let cn: *DomNode = dt_node(t, ch)
234 ch = cn.next_sib
235 } } }
236 }
237 }
238 dt_clone_depth = dt_clone_depth - 1
239 return idx
240}
241
242// ---- attributes ----
243func dt_attr_at(base: *DtAttr, i: i64) -> *DtAttr {
244 return ((base as i64) + i * DT_ATTR_BYTES) as *DtAttr
245}
246// set/update attribute (name + value given as raw spans). name is lowercased + owned.
247func dt_set_attr_span(t: *DomTree, node: i64, nsrc: *u8, noff: i64, nlen: i64, vsrc: *u8, voff: i64, vlen: i64) -> i64 {
248 let nd: *DomNode = dt_node(t, node)
249 if nd.attrs == 0 {
250 nd.attrs = (sys_mmap(DT_ATTR_CAP * DT_ATTR_BYTES)) as i64
251 nd.nattrs = 0
252 }
253 let lname: i64 = dt_dup_lower(nsrc, noff, nlen)
254 let base: *DtAttr = nd.attrs as *DtAttr
255 var i: i64 = 0
256 while i < nd.nattrs {
257 let a: *DtAttr = dt_attr_at(base, i)
258 if dt_ceq(a.name as *u8, lname as *u8) == 1 {
259 a.val = dt_dup(vsrc, voff, vlen)
260 return 0
261 }
262 i = i + 1
263 }
264 if nd.nattrs >= DT_ATTR_CAP { return 0 - 1 }
265 let a2: *DtAttr = dt_attr_at(base, nd.nattrs)
266 a2.name = lname
267 a2.val = dt_dup(vsrc, voff, vlen)
268 nd.nattrs = nd.nattrs + 1
269 return 0
270}
271func dt_set_attr(t: *DomTree, node: i64, namec: *u8, valc: *u8) -> i64 {
272 return dt_set_attr_span(t, node, namec, 0, dt_clen(namec), valc, 0, dt_clen(valc))
273}
274// returns owned value cstr, or 0 if absent
275func dt_get_attr(t: *DomTree, node: i64, namec: *u8) -> *u8 {
276 let nd: *DomNode = dt_node(t, node)
277 if nd.attrs == 0 { return 0 as *u8 }
278 let lname: i64 = dt_dup_lower_c(namec)
279 let base: *DtAttr = nd.attrs as *DtAttr
280 var i: i64 = 0
281 while i < nd.nattrs {
282 let a: *DtAttr = dt_attr_at(base, i)
283 if dt_ceq(a.name as *u8, lname as *u8) == 1 { return a.val as *u8 }
284 i = i + 1
285 }
286 return 0 as *u8
287}
288
289// textContent = "..." : drop all children, install one text node
290func dt_set_text_content(t: *DomTree, node: i64, src: *u8, off: i64, len: i64) -> i64 {
291 let nd: *DomNode = dt_node(t, node)
292 nd.first_child = 0 - 1
293 nd.last_child = 0 - 1
294 // textContent="" removes all children and leaves NONE (real DOM). Appending an empty text node
295 // here left a stray #text -- jQuery's buildFragment does `fragment.textContent=""` to drop its temp
296 // <div>, so the stray node made parseHTML return an extra leading text node. Empty -> clear only.
297 if len <= 0 { return 0 }
298 let tx: i64 = dt_create_text(t, src, off, len)
299 dt_append_child(t, node, tx)
300 return 0
301}
302
303// ---- queries ----
304func dt_gebi_rec(t: *DomTree, idx: i64, idc: *u8) -> i64 {
305 if idx == (0 - 1) { return 0 - 1 }
306 let nd: *DomNode = dt_node(t, idx)
307 if nd.kind == DT_ELEM {
308 let v: *u8 = dt_get_attr(t, idx, "id\x00" as *u8)
309 if (v as i64) != 0 { if dt_ceq(v, idc) == 1 { return idx } }
310 }
311 var c: i64 = nd.first_child
312 while c != (0 - 1) {
313 let r: i64 = dt_gebi_rec(t, c, idc)
314 if r != (0 - 1) { return r }
315 let cn: *DomNode = dt_node(t, c)
316 c = cn.next_sib
317 }
318 return 0 - 1
319}
320func dt_get_element_by_id(t: *DomTree, idc: *u8) -> i64 { return dt_gebi_rec(t, t.root, idc) }
321
322func dt_fbt_rec(t: *DomTree, idx: i64, tagc: *u8) -> i64 {
323 if idx == (0 - 1) { return 0 - 1 }
324 let nd: *DomNode = dt_node(t, idx)
325 if nd.kind == DT_ELEM { if dt_ceq(nd.tag as *u8, tagc) == 1 { return idx } }
326 var c: i64 = nd.first_child
327 while c != (0 - 1) {
328 let r: i64 = dt_fbt_rec(t, c, tagc)
329 if r != (0 - 1) { return r }
330 let cn: *DomNode = dt_node(t, c)
331 c = cn.next_sib
332 }
333 return 0 - 1
334}
335func dt_first_by_tag(t: *DomTree, tagc: *u8) -> i64 { return dt_fbt_rec(t, t.root, tagc) }
336
337// does node's class attr contain `cls` as a whole space-separated token?
338func dt_has_class(t: *DomTree, node: i64, cls: *u8) -> i64 {
339 let v: *u8 = dt_get_attr(t, node, "class\x00" as *u8)
340 if (v as i64) == 0 { return 0 }
341 let cl: i64 = dt_clen(cls)
342 if cl == 0 { return 0 }
343 var i: i64 = 0
344 while v[i] != (0 as u8) {
345 while v[i] == (32 as u8) { i = i + 1 }
346 let s: i64 = i
347 while v[i] != (0 as u8) { if v[i] == (32 as u8) { break } i = i + 1 }
348 let tl: i64 = i - s
349 if tl == cl {
350 var j: i64 = 0
351 var ok: i64 = 1
352 while j < cl { if (v[s + j] & 0xff) != (cls[j] & 0xff) { ok = 0; j = cl } else { j = j + 1 } }
353 if ok == 1 { return 1 }
354 }
355 }
356 return 0
357}
358// selector match: mode 1='#id' (exact id), 2='.class' (token), 3='tag' (name pre-lowercased by caller).
359func dt_sel_match(t: *DomTree, node: i64, mode: i64, name: *u8) -> i64 {
360 let nd: *DomNode = dt_node(t, node)
361 if nd.kind != DT_ELEM { return 0 }
362 if mode == 1 { let v: *u8 = dt_get_attr(t, node, "id\x00" as *u8); if (v as i64) == 0 { return 0 } return dt_ceq(v, name) }
363 if mode == 2 { return dt_has_class(t, node, name) }
364 if (name[0] & 0xff) == 42 { return 1 } // '*' = ANY element (getElementsByTagName("*") / Sizzle universal)
365 return dt_ceq(nd.tag as *u8, name)
366}
367// ============ CSS COMPLEX-SELECTOR MATCHER (compound + attribute + descendant/child combinators) ============
368// The unified foundation for querySelectorAll(complexSel) + element.matches(sel): jQuery's Sizzle prefers
369// qsa when the whole selector is qsa-able, and uses element.matches for .is/.filter(sel)/.closest/.not.
370// String helpers on NUL-terminated cstrings (attribute-value operators):
371func dt_cstr_contains(hay: *u8, needle: *u8) -> i64 {
372 let hl: i64 = dt_clen(hay); let nl: i64 = dt_clen(needle)
373 if nl == 0 { return 1 }
374 var i: i64 = 0
375 while i + nl <= hl {
376 var j: i64 = 0; var ok: i64 = 1
377 while j < nl { if (hay[i + j] & 0xff) != (needle[j] & 0xff) { ok = 0; j = nl } else { j = j + 1 } }
378 if ok == 1 { return 1 }
379 i = i + 1
380 }
381 return 0
382}
383func dt_cstr_prefix(hay: *u8, needle: *u8) -> i64 {
384 let hl: i64 = dt_clen(hay); let nl: i64 = dt_clen(needle)
385 if nl > hl { return 0 }
386 var j: i64 = 0
387 while j < nl { if (hay[j] & 0xff) != (needle[j] & 0xff) { return 0 } j = j + 1 }
388 return 1
389}
390func dt_cstr_suffix(hay: *u8, needle: *u8) -> i64 {
391 let hl: i64 = dt_clen(hay); let nl: i64 = dt_clen(needle)
392 if nl > hl { return 0 }
393 var j: i64 = 0
394 while j < nl { if (hay[hl - nl + j] & 0xff) != (needle[j] & 0xff) { return 0 } j = j + 1 }
395 return 1
396}
397// attr op: 1='=' exact, 2='*=' substring, 3='^=' prefix, 4='$=' suffix, 5='~=' whitespace-token
398func dt_attr_op(val: *u8, op: i64, want: *u8) -> i64 {
399 if op == 1 { return dt_ceq(val, want) }
400 if op == 2 { return dt_cstr_contains(val, want) }
401 if op == 3 { return dt_cstr_prefix(val, want) }
402 if op == 4 { return dt_cstr_suffix(val, want) }
403 if op == 5 { // ~= : whitespace-separated token (reuse class-token logic shape)
404 let cl: i64 = dt_clen(want)
405 if cl == 0 { return 0 }
406 var i: i64 = 0
407 while val[i] != (0 as u8) {
408 while val[i] == (32 as u8) { i = i + 1 }
409 let s: i64 = i
410 while val[i] != (0 as u8) { if val[i] == (32 as u8) { break } i = i + 1 }
411 let tl: i64 = i - s
412 if tl == cl { var j: i64 = 0; var ok: i64 = 1; while j < cl { if (val[s + j] & 0xff) != (want[j] & 0xff) { ok = 0; j = cl } else { j = j + 1 } } if ok == 1 { return 1 } }
413 }
414 return 0
415 }
416 return 1
417}
418// match ONE compound (no combinators): tagOrStar? (#id | .class | [attr op val] | :scope)* against node.
419func dt_sel_compound(t: *DomTree, node: i64, sel: *u8, a: i64, b: i64, scope_root: i64) -> i64 {
420 let nd: *DomNode = dt_node(t, node)
421 if nd.kind != DT_ELEM { return 0 }
422 var i: i64 = a
423 while i < b {
424 let c: i64 = sel[i] & 0xff
425 if c == 58 {
426 // ':pseudo' -- supported: :scope (matches the querySelectorAll context element) and
427 // :not(sel) (negation; jQuery's .not()/.is(':not(..)') route through this). Any other pseudo
428 // makes this compound NON-matching (Sizzle handles the rest in its own matcher).
429 i = i + 1
430 let s0: i64 = i
431 while i < b { let cc: i64 = sel[i] & 0xff; if cc == 46 { break } if cc == 35 { break } if cc == 91 { break } if cc == 58 { break } if cc == 40 { break } if cc == 32 { break } i = i + 1 }
432 let pn: *u8 = (dt_dup(sel, s0, i - s0)) as *u8
433 // capture an optional (...) argument range [argS, argE)
434 var argS: i64 = 0 - 1
435 var argE: i64 = 0 - 1
436 if i < b { if (sel[i] & 0xff) == 40 { argS = i + 1; var d2: i64 = 1; i = i + 1; while i < b { let ac: i64 = sel[i] & 0xff; if ac == 40 { d2 = d2 + 1 } if ac == 41 { d2 = d2 - 1 } if d2 == 0 { argE = i; i = i + 1; break } i = i + 1 } } }
437 if dt_ceq(pn, "scope\x00" as *u8) == 1 { if node != scope_root { return 0 } }
438 else { if dt_ceq(pn, "not\x00" as *u8) == 1 {
439 if argS >= 0 { let inner: *u8 = (dt_dup(sel, argS, argE - argS)) as *u8; if dt_sel_matches(t, node, inner, argE - argS, scope_root) == 1 { return 0 } }
440 } else { return 0 } }
441 }
442 else { if c == 42 { i = i + 1 }
443 else { if c == 46 {
444 i = i + 1
445 let s0: i64 = i
446 while i < b { let cc: i64 = sel[i] & 0xff; if cc == 46 { break } if cc == 35 { break } if cc == 91 { break } i = i + 1 }
447 let cls: *u8 = (dt_dup(sel, s0, i - s0)) as *u8
448 if dt_has_class(t, node, cls) == 0 { return 0 }
449 }
450 else { if c == 35 {
451 i = i + 1
452 let s0: i64 = i
453 while i < b { let cc: i64 = sel[i] & 0xff; if cc == 46 { break } if cc == 35 { break } if cc == 91 { break } i = i + 1 }
454 let idv: *u8 = dt_get_attr(t, node, "id\x00" as *u8)
455 if (idv as i64) == 0 { return 0 }
456 let want: *u8 = (dt_dup(sel, s0, i - s0)) as *u8
457 if dt_ceq(idv, want) == 0 { return 0 }
458 }
459 else { if c == 91 {
460 i = i + 1
461 let n0: i64 = i
462 while i < b { let cc: i64 = sel[i] & 0xff; if cc == 93 { break } if cc == 61 { break } if cc == 42 { break } if cc == 94 { break } if cc == 36 { break } if cc == 126 { break } i = i + 1 }
463 let an: *u8 = (dt_dup(sel, n0, i - n0)) as *u8
464 let av: *u8 = dt_get_attr(t, node, an)
465 var op: i64 = 0
466 if i < b {
467 let oc: i64 = sel[i] & 0xff
468 if oc == 61 { op = 1; i = i + 1 }
469 else { if oc == 42 { op = 2; i = i + 1; if i < b { if (sel[i] & 0xff) == 61 { i = i + 1 } } }
470 else { if oc == 94 { op = 3; i = i + 1; if i < b { if (sel[i] & 0xff) == 61 { i = i + 1 } } }
471 else { if oc == 36 { op = 4; i = i + 1; if i < b { if (sel[i] & 0xff) == 61 { i = i + 1 } } }
472 else { if oc == 126 { op = 5; i = i + 1; if i < b { if (sel[i] & 0xff) == 61 { i = i + 1 } } } } } } }
473 }
474 var v0: i64 = i
475 while i < b { if (sel[i] & 0xff) == 93 { break } i = i + 1 }
476 var ve: i64 = i
477 if v0 < ve { let q: i64 = sel[v0] & 0xff; if q == 34 { v0 = v0 + 1; if ve > v0 { if (sel[ve - 1] & 0xff) == q { ve = ve - 1 } } } else { if q == 39 { v0 = v0 + 1; if ve > v0 { if (sel[ve - 1] & 0xff) == q { ve = ve - 1 } } } } }
478 if i < b { i = i + 1 }
479 if op == 0 { if (av as i64) == 0 { return 0 } }
480 else {
481 if (av as i64) == 0 { return 0 }
482 let want: *u8 = (dt_dup(sel, v0, ve - v0)) as *u8
483 if dt_attr_op(av, op, want) == 0 { return 0 }
484 }
485 }
486 else {
487 let s0: i64 = i
488 while i < b { let cc: i64 = sel[i] & 0xff; if cc == 46 { break } if cc == 35 { break } if cc == 91 { break } i = i + 1 }
489 let want: *u8 = (dt_dup_lower(sel, s0, i - s0)) as *u8
490 if dt_ceq(nd.tag as *u8, want) == 0 { return 0 }
491 } } } } }
492 }
493 return 1
494}
495// match a COMPLEX selector sel[a:b) (compounds joined by descendant-space or child-'>') against node,
496// RIGHT-TO-LEFT (the rightmost compound must match node; ancestors/parent must match the left part).
497func dt_sel_complex(t: *DomTree, node: i64, sel: *u8, a0: i64, b0: i64, scope_root: i64) -> i64 {
498 var a: i64 = a0
499 var b: i64 = b0
500 while a < b { if (sel[a] & 0xff) == 32 { a = a + 1 } else { break } }
501 while b > a { if (sel[b - 1] & 0xff) == 32 { b = b - 1 } else { break } }
502 // scan from right over the rightmost compound (stop at top-level space/'>' -- '[' depth aware)
503 var depth: i64 = 0
504 var i: i64 = b - 1
505 var split: i64 = a - 1
506 while i >= a {
507 let c: i64 = sel[i] & 0xff
508 if c == 93 { depth = depth + 1 }
509 else { if c == 91 { depth = depth - 1 }
510 else { if depth == 0 { if c == 32 { split = i; i = a - 1 } else { if c == 62 { split = i; i = a - 1 } } } } }
511 i = i - 1
512 }
513 if split < a { return dt_sel_compound(t, node, sel, a, b, scope_root) }
514 var rc: i64 = split + 1
515 while rc < b { if (sel[rc] & 0xff) == 32 { rc = rc + 1 } else { break } }
516 if dt_sel_compound(t, node, sel, rc, b, scope_root) == 0 { return 0 }
517 // combinator run = [le, split+1); child if it contains '>', else descendant
518 var le: i64 = split + 1
519 var hasChild: i64 = 0
520 while le > a { let c: i64 = sel[le - 1] & 0xff; if c == 32 { le = le - 1 } else { if c == 62 { hasChild = 1; le = le - 1 } else { break } } }
521 let nd: *DomNode = dt_node(t, node)
522 if hasChild == 1 {
523 if nd.parent == (0 - 1) { return 0 }
524 return dt_sel_complex(t, nd.parent, sel, a, le, scope_root)
525 }
526 var p: i64 = nd.parent
527 while p != (0 - 1) { if dt_sel_complex(t, p, sel, a, le, scope_root) == 1 { return 1 } let pn: *DomNode = dt_node(t, p); p = pn.parent }
528 return 0
529}
530// match a SELECTOR LIST (comma-separated) against node -- OR semantics. slen = strlen(sel). scope_root =
531// the element `:scope` refers to (the querySelectorAll context / -1 for none) so Sizzle's `:scope ul li` works.
532func dt_sel_matches(t: *DomTree, node: i64, sel: *u8, slen: i64, scope_root: i64) -> i64 {
533 var a: i64 = 0
534 var i: i64 = 0
535 var depth: i64 = 0
536 while i <= slen {
537 var atend: i64 = 0
538 if i == slen { atend = 1 }
539 var c: i64 = 0
540 if atend == 0 { c = sel[i] & 0xff }
541 if atend == 0 { if c == 91 { depth = depth + 1 } else { if c == 93 { depth = depth - 1 } } }
542 if atend == 1 { if dt_sel_complex(t, node, sel, a, i, scope_root) == 1 { return 1 } a = i + 1 }
543 else { if depth == 0 { if c == 44 { if dt_sel_complex(t, node, sel, a, i, scope_root) == 1 { return 1 } a = i + 1 } } }
544 i = i + 1
545 }
546 return 0
547}
548// whole-tree collect of nodes matching the selector list (querySelectorAll over complex selectors).
549func dt_sel_query_rec(t: *DomTree, idx: i64, sel: *u8, slen: i64, arr: *i64, cnt: *i64, cap: i64, scope_root: i64) -> i64 {
550 if idx == (0 - 1) { return 0 }
551 let nd: *DomNode = dt_node(t, idx)
552 if nd.kind == DT_ELEM { if dt_sel_matches(t, idx, sel, slen, scope_root) == 1 { if cnt[0] < cap { arr[cnt[0]] = idx; cnt[0] = cnt[0] + 1 } } }
553 var c: i64 = nd.first_child
554 while c != (0 - 1) { dt_sel_query_rec(t, c, sel, slen, arr, cnt, cap, scope_root); let cn: *DomNode = dt_node(t, c); c = cn.next_sib }
555 return 0
556}
557// collect matches into arr starting the walk at `root` subtree's CHILDREN (element-scoped) or whole tree.
558// `root` IS the `:scope` element (querySelectorAll context) -- Sizzle scopes descendant .find via `:scope ul li`.
559func dt_sel_query(t: *DomTree, root: i64, sel: *u8, slen: i64, arr: *i64, cap: i64) -> i64 {
560 let cnt: *i64 = sys_mmap(8) as *i64
561 cnt[0] = 0
562 let nd: *DomNode = dt_node(t, root)
563 var c: i64 = nd.first_child
564 while c != (0 - 1) { dt_sel_query_rec(t, c, sel, slen, arr, cnt, cap, root); let cn: *DomNode = dt_node(t, c); c = cn.next_sib }
565 return cnt[0]
566}
567func dt_query_one_rec(t: *DomTree, idx: i64, mode: i64, name: *u8) -> i64 {
568 if idx == (0 - 1) { return 0 - 1 }
569 if dt_sel_match(t, idx, mode, name) == 1 { return idx }
570 let nd: *DomNode = dt_node(t, idx)
571 var c: i64 = nd.first_child
572 while c != (0 - 1) {
573 let r: i64 = dt_query_one_rec(t, c, mode, name)
574 if r != (0 - 1) { return r }
575 let cn: *DomNode = dt_node(t, c)
576 c = cn.next_sib
577 }
578 return 0 - 1
579}
580func dt_query_one(t: *DomTree, mode: i64, name: *u8) -> i64 { return dt_query_one_rec(t, t.root, mode, name) }
581func dt_query_all_rec(t: *DomTree, idx: i64, mode: i64, name: *u8, arr: *i64, cnt: *i64, cap: i64) -> i64 {
582 if idx == (0 - 1) { return 0 }
583 if dt_sel_match(t, idx, mode, name) == 1 { if cnt[0] < cap { arr[cnt[0]] = idx; cnt[0] = cnt[0] + 1 } }
584 let nd: *DomNode = dt_node(t, idx)
585 var c: i64 = nd.first_child
586 while c != (0 - 1) {
587 dt_query_all_rec(t, c, mode, name, arr, cnt, cap)
588 let cn: *DomNode = dt_node(t, c)
589 c = cn.next_sib
590 }
591 return 0
592}
593func dt_query_all(t: *DomTree, mode: i64, name: *u8, arr: *i64, cap: i64) -> i64 {
594 let cnt: *i64 = sys_mmap(8) as *i64
595 cnt[0] = 0
596 dt_query_all_rec(t, t.root, mode, name, arr, cnt, cap)
597 return cnt[0]
598}
599
600// concat all descendant text into out (cap-guarded). returns bytes written.
601func dt_tc_rec(t: *DomTree, idx: i64, out: *u8, cap: i64, posp: *i64) -> i64 {
602 let nd: *DomNode = dt_node(t, idx)
603 if nd.kind == DT_TEXT {
604 let s: *u8 = nd.text as *u8
605 var k: i64 = 0
606 var p: i64 = posp[0]
607 while k < nd.text_len { if p < cap { out[p] = s[k]; p = p + 1 } k = k + 1 }
608 posp[0] = p
609 }
610 var c: i64 = nd.first_child
611 while c != (0 - 1) {
612 dt_tc_rec(t, c, out, cap, posp)
613 let cn: *DomNode = dt_node(t, c)
614 c = cn.next_sib
615 }
616 return 0
617}
618func dt_text_content(t: *DomTree, node: i64, out: *u8, cap: i64) -> i64 {
619 let posp: *i64 = sys_mmap(8) as *i64
620 posp[0] = 0
621 dt_tc_rec(t, node, out, cap, posp)
622 return posp[0]
623}
624
625// ---- parse attributes out of a start-tag source span ----
626func dt_is_name(b: i64) -> i64 {
627 if b >= 97 { if b <= 122 { return 1 } }
628 if b >= 65 { if b <= 90 { return 1 } }
629 if b >= 48 { if b <= 57 { return 1 } }
630 if b == 45 { return 1 }
631 if b == 95 { return 1 }
632 if b == 58 { return 1 }
633 return 0
634}
635func dt_is_ws(b: i64) -> i64 {
636 if b == 32 { return 1 }
637 if b == 9 { return 1 }
638 if b == 10 { return 1 }
639 if b == 13 { return 1 }
640 if b == 12 { return 1 }
641 return 0
642}
643func dt_parse_attrs(t: *DomTree, node: i64, src: *u8, soff: i64, slen: i64) -> i64 {
644 let end: i64 = soff + slen
645 var i: i64 = soff + 1 // past '<'
646 while i < end { if dt_is_name(src[i] & 0xff) == 1 { i = i + 1 } else { break } }
647 // (the loop above stops at first non-name char = end of tag name)
648 while i < end {
649 while i < end { if dt_is_ws(src[i] & 0xff) == 1 { i = i + 1 } else { break } }
650 if i >= end { break }
651 let ch: i64 = src[i] & 0xff
652 if ch == 62 { break } // '>'
653 if ch == 47 { i = i + 1; continue } // '/'
654 let nstart: i64 = i
655 while i < end { if dt_is_name(src[i] & 0xff) == 1 { i = i + 1 } else { break } }
656 let nlen: i64 = i - nstart
657 if nlen == 0 { i = i + 1; continue }
658 while i < end { if dt_is_ws(src[i] & 0xff) == 1 { i = i + 1 } else { break } }
659 if i < end { if (src[i] & 0xff) == 61 { // '='
660 i = i + 1
661 while i < end { if dt_is_ws(src[i] & 0xff) == 1 { i = i + 1 } else { break } }
662 if i < end {
663 let q: i64 = src[i] & 0xff
664 if q == 34 { i = i + 1; let vs: i64 = i; while i < end { if (src[i] & 0xff) == 34 { break } i = i + 1 } dt_set_attr_span(t, node, src, nstart, nlen, src, vs, i - vs); if i < end { i = i + 1 } }
665 else { if q == 39 { i = i + 1; let vs2: i64 = i; while i < end { if (src[i] & 0xff) == 39 { break } i = i + 1 } dt_set_attr_span(t, node, src, nstart, nlen, src, vs2, i - vs2); if i < end { i = i + 1 } }
666 else { let vs3: i64 = i; while i < end { let cc: i64 = src[i] & 0xff; if dt_is_ws(cc) == 1 { break } if cc == 62 { break } i = i + 1 } dt_set_attr_span(t, node, src, nstart, nlen, src, vs3, i - vs3) } }
667 }
668 } else {
669 dt_set_attr_span(t, node, src, nstart, nlen, "\x00" as *u8, 0, 0) // boolean attr
670 } }
671 }
672 return 0
673}
674
675// case-insensitive find of `needle` (nlen bytes) in buf[from..hlen), or -1.
676func dt_find_ci(buf: *u8, from: i64, hlen: i64, needle: *u8, nlen: i64) -> i64 {
677 if nlen <= 0 { return from }
678 var i: i64 = from
679 while (i + nlen) <= hlen {
680 var j: i64 = 0
681 var ok: i64 = 1
682 while j < nlen {
683 var a: i64 = buf[i + j] & 0xff
684 var b: i64 = needle[j] & 0xff
685 if a >= 65 { if a <= 90 { a = a + 32 } }
686 if b >= 65 { if b <= 90 { b = b + 32 } }
687 if a != b { ok = 0; j = nlen } else { j = j + 1 }
688 }
689 if ok == 1 { return i }
690 i = i + 1
691 }
692 return 0 - 1
693}
694// raw-text elements: their content is NOT html and must NOT be tokenized (RCDATA-safe round-trip).
695func dt_is_rawtext(tag: *u8) -> i64 {
696 if dt_ceq(tag, "script\x00" as *u8) == 1 { return 1 }
697 if dt_ceq(tag, "style\x00" as *u8) == 1 { return 1 }
698 return 0
699}
700func dt_rawclose(tag: *u8) -> *u8 {
701 if dt_ceq(tag, "script\x00" as *u8) == 1 { return "</script\x00" as *u8 }
702 return "</style\x00" as *u8
703}
704// ---- parse HTML -> tree (reuses nx_html_tokenizer) ----
705func dt_parse_into(t: *DomTree, start: i64, html: *u8, hlen: i64) -> i64 {
706 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
707 nx_html_cursor_init(c, html, hlen)
708 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
709 var cur: i64 = start
710 var go: i64 = 1
711 while go == 1 {
712 nx_html_next_token(c, tok)
713 let k: i64 = tok.kind
714 if k == NX_HTML_TOK_EOF { go = 0 }
715 else { if k == NX_HTML_TOK_START_TAG {
716 let e: i64 = dt_create_element_span(t, html, tok.name_off, tok.name_len)
717 dt_parse_attrs(t, e, html, tok.src_off, tok.src_len)
718 dt_append_child(t, cur, e)
719 let nd: *DomNode = dt_node(t, e)
720 if dt_is_rawtext(nd.tag as *u8) == 1 {
721 let close: *u8 = dt_rawclose(nd.tag as *u8)
722 let cl: i64 = dt_clen(close)
723 let ce: i64 = dt_find_ci(html, c.pos, hlen, close, cl)
724 if ce >= 0 {
725 if ce > c.pos { let rtx: i64 = dt_create_text(t, html, c.pos, ce - c.pos); dt_append_child(t, e, rtx) }
726 var gp: i64 = ce + cl
727 while gp < hlen { if (html[gp] & 0xff) == 62 { break } gp = gp + 1 }
728 if gp < hlen { gp = gp + 1 }
729 c.pos = gp
730 } else {
731 if c.pos < hlen { let rtx2: i64 = dt_create_text(t, html, c.pos, hlen - c.pos); dt_append_child(t, e, rtx2) }
732 c.pos = hlen
733 }
734 } else {
735 if nd.voidf == 0 { cur = e }
736 }
737 } else { if k == NX_HTML_TOK_SELF_CLOSING {
738 let e2: i64 = dt_create_element_span(t, html, tok.name_off, tok.name_len)
739 dt_parse_attrs(t, e2, html, tok.src_off, tok.src_len)
740 dt_append_child(t, cur, e2)
741 } else { if k == NX_HTML_TOK_END_TAG {
742 let tagc: *u8 = (dt_dup_lower(html, tok.name_off, tok.name_len)) as *u8
743 var anc: i64 = cur
744 var found: i64 = 0 - 1
745 while anc != (0 - 1) {
746 let an: *DomNode = dt_node(t, anc)
747 var matched: i64 = 0
748 if an.kind == DT_ELEM { if dt_ceq(an.tag as *u8, tagc) == 1 { matched = 1 } }
749 if matched == 1 { found = anc; anc = 0 - 1 }
750 else { anc = an.parent }
751 }
752 if found != (0 - 1) {
753 let fn: *DomNode = dt_node(t, found)
754 cur = fn.parent
755 if cur == (0 - 1) { cur = start }
756 }
757 } else { if k == NX_HTML_TOK_TEXT {
758 if tok.body_len > 0 {
759 let tx: i64 = dt_create_text(t, html, tok.body_off, tok.body_len)
760 dt_append_child(t, cur, tx)
761 }
762 } } } }
763 }
764 }
765 return 0
766}
767func dt_parse(html: *u8, hlen: i64) -> *DomTree {
768 let t: *DomTree = dt_new()
769 dt_parse_into(t, t.root, html, hlen)
770 return t
771}
772// element.innerHTML = html : drop children, parse the fragment as new children.
773func dt_set_inner_html(t: *DomTree, node: i64, html: *u8, hlen: i64) -> i64 {
774 let nd: *DomNode = dt_node(t, node)
775 nd.first_child = 0 - 1
776 nd.last_child = 0 - 1
777 dt_parse_into(t, node, html, hlen)
778 return 0
779}
780
781// ---- serialize tree -> HTML ----
782func dt_app(out: *u8, cap: i64, posp: *i64, s: *u8, n: i64) -> i64 {
783 var p: i64 = posp[0]
784 var i: i64 = 0
785 while i < n { if p < cap { out[p] = s[i]; p = p + 1 } i = i + 1 }
786 posp[0] = p
787 return 0
788}
789func dt_app_c(out: *u8, cap: i64, posp: *i64, c: *u8) -> i64 {
790 dt_app(out, cap, posp, c, dt_clen(c))
791 return 0
792}
793func dt_ser_rec(t: *DomTree, idx: i64, out: *u8, cap: i64, posp: *i64) -> i64 {
794 let nd: *DomNode = dt_node(t, idx)
795 if nd.kind == DT_TEXT { dt_app(out, cap, posp, nd.text as *u8, nd.text_len); return 0 }
796 if nd.kind == DT_ELEM {
797 dt_app_c(out, cap, posp, "<\x00" as *u8)
798 dt_app_c(out, cap, posp, nd.tag as *u8)
799 if nd.attrs != 0 {
800 let base: *DtAttr = nd.attrs as *DtAttr
801 var i: i64 = 0
802 while i < nd.nattrs {
803 let a: *DtAttr = dt_attr_at(base, i)
804 dt_app_c(out, cap, posp, " \x00" as *u8)
805 dt_app_c(out, cap, posp, a.name as *u8)
806 dt_app_c(out, cap, posp, "=\"\x00" as *u8)
807 dt_app_c(out, cap, posp, a.val as *u8)
808 dt_app_c(out, cap, posp, "\"\x00" as *u8)
809 i = i + 1
810 }
811 }
812 dt_app_c(out, cap, posp, ">\x00" as *u8)
813 if nd.voidf == 0 {
814 var c: i64 = nd.first_child
815 while c != (0 - 1) {
816 dt_ser_rec(t, c, out, cap, posp)
817 let cn: *DomNode = dt_node(t, c)
818 c = cn.next_sib
819 }
820 dt_app_c(out, cap, posp, "</\x00" as *u8)
821 dt_app_c(out, cap, posp, nd.tag as *u8)
822 dt_app_c(out, cap, posp, ">\x00" as *u8)
823 }
824 return 0
825 }
826 // DT_DOC: just children
827 var c2: i64 = nd.first_child
828 while c2 != (0 - 1) {
829 dt_ser_rec(t, c2, out, cap, posp)
830 let cn2: *DomNode = dt_node(t, c2)
831 c2 = cn2.next_sib
832 }
833 return 0
834}
835func dt_serialize(t: *DomTree, out: *u8, cap: i64) -> i64 {
836 let posp: *i64 = sys_mmap(8) as *i64
837 posp[0] = 0
838 dt_ser_rec(t, t.root, out, cap, posp)
839 return posp[0]
840}