code wiki / _hdl_build / nx_render_html.nx
nx_render_html.nx source
↩ module page · 730 lines · 38214 B
1// nx_render_html.nx -- REUSABLE: render an HTML byte buffer to a styled 24-bit BMP. The full
2// styled pipeline (extracted from nx_render_wiki_styled so the file-driver AND nx_browse compose it,
3// not duplicate it): combined [HTML][UA-CSS] buffer -> nx_layout_from_dom -> box-indexed CssElement[]
4// -> nx_css_apply -> font-size/spacing-aware layout -> HTML-entity decode -> inherited-color +
5// font-size-scaled paint -> 24-bit BMP. UA sheet = heading bars + box-model spacing + blue links +
6// font-size hierarchy. 100% sovereign (no gcc/V8/Chrome). This organ has NO main (it's a library).
7import "nx_syscalls.nx"
8import "nx_css_vars.nx" // native var(--token) resolution (fleet --nx-color-* design tokens) -- 3rd-party-free
9import "nx_css_tokenize.nx"
10import "nx_css_parse.nx"
11import "nx_css_apply.nx"
12import "nx_css_color_decode.nx"
13import "nx_css_selector_match.nx"
14import "nx_html_tokenizer.nx"
15import "nx_html_entities.nx"
16import "nx_dom_query.nx"
17import "nx_layout_box.nx"
18import "nx_layout_default_display.nx"
19import "nx_layout_from_dom.nx"
20import "nx_layout_block.nx"
21import "nx_paint_solid_rect.nx"
22import "nx_paint_text.nx"
23import "nx_font_bitmap_5x7.nx"
24import "nx_paint_walk_layout.nx"
25import "nx_png_write.nx"
26const K_MAGIC_4096: i64 = 4096
27const K_MAGIC_1000000: i64 = 1000000
28const K_MAGIC_262144: i64 = 262144
29const K_MAGIC_16384: i64 = 16384
30const K_MAGIC_8192: i64 = 8192
31const K_MAGIC_32768: i64 = 32768
32const K_MAGIC_5000: i64 = 5000
33
34func rh_slen(s: *u8) -> i64 { var k: i64=0; while s[k]!=(0 as u8){k=k+1} return k }
35func rh_u32(fd: i64, v: i64) -> i64 { let b: *u8=sys_mmap(4); b[0]=(v&255) as u8; b[1]=((v>>8)&255) as u8; b[2]=((v>>16)&255) as u8; b[3]=((v>>24)&255) as u8; sys_write(fd,b,4); return 0 }
36func rh_u16(fd: i64, v: i64) -> i64 { let b: *u8=sys_mmap(2); b[0]=(v&255) as u8; b[1]=((v>>8)&255) as u8; sys_write(fd,b,2); return 0 }
37func rh_save(path: *u8, fb: *Framebuffer, w: i64, y0: i64, y1: i64) -> i64 {
38 let h: i64 = y1 - y0
39 let row3: i64=w*3; let pad: i64=(4-(row3%4))%4; let rowstride: i64=row3+pad; let datasize: i64=rowstride*h
40 let fd: i64=sys_openat_wr(path,420)
41 if fd<0 { return 0-1 }
42 sys_write(fd,"BM\x00" as *u8,2); rh_u32(fd,54+datasize); rh_u32(fd,0); rh_u32(fd,54)
43 rh_u32(fd,40); rh_u32(fd,w); rh_u32(fd,h); rh_u16(fd,1); rh_u16(fd,24); rh_u32(fd,0); rh_u32(fd,datasize); rh_u32(fd,0); rh_u32(fd,0); rh_u32(fd,0); rh_u32(fd,0)
44 let rowbuf: *u8=sys_mmap(rowstride+4); let px: *CssColor=(sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
45 var y: i64=y1-1
46 while y>=y0 {
47 var x: i64=0
48 while x<w {
49 nx_framebuffer_get_pixel(fb,x,y,px)
50 if px.a==0 { rowbuf[x*3]=255 as u8; rowbuf[x*3+1]=255 as u8; rowbuf[x*3+2]=255 as u8 }
51 else { rowbuf[x*3]=px.b as u8; rowbuf[x*3+1]=px.g as u8; rowbuf[x*3+2]=px.r as u8 }
52 x=x+1
53 }
54 var p: i64=0
55 while p<pad { rowbuf[row3+p]=0 as u8; p=p+1 }
56 sys_write(fd,rowbuf,rowstride); y=y-1
57 }
58 sys_close(fd); return 0
59}
60func rh_extract(html: *u8, hlen: i64, elements: *CssElement, maxn: i64) -> i64 {
61 let c: *HtmlCursor=sys_mmap(24) as *HtmlCursor; nx_html_cursor_init(c,html,hlen)
62 let tok: *HtmlToken=sys_mmap(56) as *HtmlToken
63 let ao: *i64=sys_mmap(8) as *i64; let al: *i64=sys_mmap(8) as *i64; let bo: *i64=sys_mmap(8) as *i64; let bl: *i64=sys_mmap(8) as *i64
64 var n: i64=0
65 var cskip: i64=0
66 while 1==1 {
67 nx_html_next_token(c,tok)
68 if tok.kind==NX_HTML_TOK_EOF { return n }
69 if n>=maxn { return n }
70 var ie: i64=0
71 // mirror nx_layout_from_dom's DEPTH-counted <nav>/<aside>/role=navigation chrome-skip EXACTLY
72 // so element order == box order (else the box-indexed cascade + <main> detection misalign).
73 if cskip > 0 {
74 if tok.kind==NX_HTML_TOK_START_TAG { cskip = cskip + 1 }
75 if tok.kind==NX_HTML_TOK_END_TAG { cskip = cskip - 1 }
76 } else {
77 if tok.kind==NX_HTML_TOK_START_TAG {
78 if _lfd_is_chrome_start(html, tok.name_off, tok.name_len, tok.src_off, tok.src_len) == 1 { cskip = 1 }
79 else { ie = 1 }
80 }
81 if tok.kind==NX_HTML_TOK_SELF_CLOSING { ie = 1 }
82 }
83 if ie==1 {
84 let e: *CssElement=((elements as i64)+n*NX_CSS_ELEMENT_BYTES) as *CssElement
85 e.src=html; e.tag_off=tok.name_off; e.tag_len=tok.name_len
86 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"id\x00" as *u8,ao,al)==1 { e.id_off=ao[0]; e.id_len=al[0] } else { e.id_off=0; e.id_len=0 }
87 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"class\x00" as *u8,bo,bl)==1 { e.class_off=bo[0]; e.class_len=bl[0] } else { e.class_off=0; e.class_len=0 }
88 n=n+1
89 }
90 }
91 return n
92}
93func rh_is_elem(k: i64) -> i64 { if k==NX_LAYOUT_BOX_BLOCK {return 1} if k==NX_LAYOUT_BOX_INLINE {return 1} if k==NX_LAYOUT_BOX_INLINE_BLOCK {return 1} return 0 }
94func rh_digit(c: i64) -> i64 { if c >= 48 { if c <= 57 { return 1 } } return 0 }
95// find NUL-terminated `pat` in src[off..end); offset or -1.
96func rh_find(src: *u8, off: i64, end: i64, pat: *u8) -> i64 {
97 var pl: i64 = 0
98 while pat[pl] != (0 as u8) { pl = pl + 1 }
99 if pl == 0 { return off }
100 var i: i64 = off
101 while i + pl <= end {
102 var j: i64 = 0
103 var ok: i64 = 1
104 while j < pl { if (src[i+j]&0xff) != (pat[j]&0xff) { ok = 0; j = pl } else { j = j + 1 } }
105 if ok == 1 { return i }
106 i = i + 1
107 }
108 return 0 - 1
109}
110// integer value of the first digit run at/after p (within end); -1 if none.
111func rh_num_after(src: *u8, p: i64, end: i64) -> i64 {
112 var i: i64 = p
113 var st: i64 = 0
114 while st == 0 { if i >= end { st = 2 } else { if rh_digit(src[i]&0xff)==1 { st = 1 } else { i = i + 1 } } }
115 if st == 2 { return 0 - 1 }
116 var v: i64 = 0
117 while i < end { if rh_digit(src[i]&0xff)==1 { v = v*10 + ((src[i]&0xff)-48); i = i + 1 } else { i = end } }
118 return v
119}
120// Does the @media condition src[off..end) match viewport width vw? Handles max-width/min-width
121// (the responsive breakpoints); unknown features default to MATCH (apply the rules).
122func rh_media_matches(src: *u8, off: i64, end: i64, vw: i64) -> i64 {
123 var result: i64 = 1
124 let mx: i64 = rh_find(src, off, end, "max-width\x00" as *u8)
125 if mx >= 0 { let n: i64 = rh_num_after(src, mx+9, end); if n >= 0 { if vw > n { result = 0 } } }
126 let mn: i64 = rh_find(src, off, end, "min-width\x00" as *u8)
127 if mn >= 0 { let n: i64 = rh_num_after(src, mn+9, end); if n >= 0 { if vw < n { result = 0 } } }
128 return result
129}
130// Filter CSS by viewport: keep `@media (cond){...}` blocks whose cond matches vw (flattened, wrapper
131// removed), drop non-matching ones; pass everything else through. So the parser only sees rules that
132// actually apply at our width -- the gate to applying a page's responsive CSS without conflicts.
133// @media-flatten to our viewport, THEN resolve var(--token) custom properties into `out` (so the native
134// engine renders the fleet's --nx-color-* design tokens; a var-less page expands byte-identical -> no gate
135// regression). Filtering lands in an internal scratch; cx_expand copies+substitutes into `out`.
136func rh_filter_media(src: *u8, off: i64, len: i64, out: *u8, out_cap: i64, vw: i64) -> i64 {
137 let scratch: *u8 = sys_mmap(out_cap + 16)
138 let end: i64 = off + len
139 var i: i64 = off
140 var o: i64 = 0
141 while i < end {
142 var is_media: i64 = 0
143 if (src[i]&0xff) == 64 { if i + 6 <= end { if rh_find(src, i, i+6, "@media\x00" as *u8) == i { is_media = 1 } } }
144 if is_media == 1 {
145 var brace: i64 = 0 - 1
146 var j: i64 = i + 6
147 while j < end { if (src[j]&0xff)==123 { brace = j; j = end } else { j = j + 1 } }
148 if brace < 0 { i = end }
149 else {
150 let matches: i64 = rh_media_matches(src, i+6, brace, vw)
151 var depth: i64 = 1
152 var k: i64 = brace + 1
153 var closeb: i64 = 0 - 1
154 while k < end { if closeb >= 0 { k = end } else {
155 let ck: i64 = src[k]&0xff
156 if ck == 123 { depth = depth + 1 }
157 if ck == 125 { depth = depth - 1; if depth == 0 { closeb = k } }
158 k = k + 1
159 } }
160 if closeb < 0 { i = end }
161 else {
162 if matches == 1 {
163 var m: i64 = brace + 1
164 while m < closeb { if o < out_cap - 1 { scratch[o] = src[m]; o = o + 1 } m = m + 1 }
165 }
166 i = closeb + 1
167 }
168 }
169 } else {
170 if o < out_cap - 1 { scratch[o] = src[i]; o = o + 1 }
171 i = i + 1
172 }
173 }
174 // resolve var(--token) then clamp() (fluid type) at THIS viewport -> concrete values the parser understands.
175 // Both are byte-identical no-ops when absent, so var/clamp-less pages (all existing gates) are unchanged.
176 let scratch2: *u8 = sys_mmap(out_cap + 16)
177 let ex: i64 = cx_expand(scratch, o, scratch2, out_cap)
178 return clamp_resolve(scratch2, ex, out, out_cap, vw)
179}
180// Extract the concatenated text of all inline <style> blocks (the page's own CSS) into `out`. So we
181// can parse + cascade the page's REAL styles (incl. display:none) alongside our UA reader stylesheet.
182// Copy CSS bytes html[off..off+len) into out[*op..], STRIPPING `<![CDATA[` / `]]>` markers. XHTML
183// stylesheets wrap their CSS in a CDATA section; those markers are not CSS and (left in) corrupt the first
184// rule's selector. Only the literal 9-byte open / 3-byte close sequences are removed (single ']' or '>'
185// from child/attr selectors are untouched).
186func _rh_copy_css(html: *u8, off: i64, len: i64, out: *u8, op: *i64, cap: i64) -> i64 {
187 var i: i64 = 0
188 while i < len {
189 var skip: i64 = 0
190 if (html[off+i]&0xff) == 60 { if i + 9 <= len {
191 if (html[off+i+1]&0xff)==33 { if (html[off+i+2]&0xff)==91 { if (html[off+i+3]&0xff)==67 { if (html[off+i+4]&0xff)==68 { if (html[off+i+5]&0xff)==65 { if (html[off+i+6]&0xff)==84 { if (html[off+i+7]&0xff)==65 { if (html[off+i+8]&0xff)==91 {
192 i = i + 9; skip = 1
193 } } } } } } } }
194 } }
195 if skip == 0 { if (html[off+i]&0xff) == 93 { if i + 3 <= len {
196 if (html[off+i+1]&0xff)==93 { if (html[off+i+2]&0xff)==62 { i = i + 3; skip = 1 } }
197 } } }
198 if skip == 0 {
199 let oo: i64 = op[0]
200 if oo < cap - 2 { out[oo] = html[off+i]; op[0] = oo + 1 }
201 i = i + 1
202 }
203 }
204 return 0
205}
206
207func rh_extract_styles(html: *u8, hlen: i64, out: *u8, out_cap: i64) -> i64 {
208 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
209 nx_html_cursor_init(c, html, hlen)
210 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
211 let raw: *HtmlToken = sys_mmap(56) as *HtmlToken
212 let op: *i64 = sys_mmap(8) as *i64
213 op[0] = 0
214 var keep: i64 = 1
215 while keep == 1 {
216 nx_html_next_token(c, tok)
217 if tok.kind == NX_HTML_TOK_EOF { keep = 0 }
218 else {
219 if tok.kind == NX_HTML_TOK_START_TAG {
220 // Raw-text tags (style/script/textarea/title) need raw-body consumption so a '<' inside the
221 // body (notably `<![CDATA[`) isn't mis-tokenized as markup. We only KEEP <style> bodies.
222 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 {
223 nx_html_consume_raw_text(c, (html as i64 + tok.name_off) as *u8, tok.name_len, raw)
224 if _lfd_name_eq(html, tok.name_off, tok.name_len, "style\x00" as *u8) == 1 {
225 _rh_copy_css(html, raw.body_off, raw.body_len, out, op, out_cap)
226 if op[0] < out_cap - 2 { out[op[0]] = 10 as u8; op[0] = op[0] + 1 }
227 }
228 }
229 }
230 }
231 }
232 return op[0]
233}
234
235// ---- complex CSS selector matching (compound / descendant / child / comma-list) ----
236func _rh_namechar(c: i64) -> i64 {
237 if c >= 48 { if c <= 57 { return 1 } }
238 if c >= 65 { if c <= 90 { return 1 } }
239 if c >= 97 { if c <= 122 { return 1 } }
240 if c == 45 { return 1 }
241 if c == 95 { return 1 }
242 return 0
243}
244// End of the namechar run starting at s (the first non-namechar position, or end).
245func _rh_name_end(buf: *u8, s: i64, end: i64) -> i64 {
246 var e: i64 = s
247 var d: i64 = 0
248 while d == 0 { if e >= end { d = 1 } else { if _rh_namechar(buf[e]&0xff)==1 { e=e+1 } else { d=1 } } }
249 return e
250}
251// Match a COMPOUND selector buf[off..off+len) (adjacent simple selectors, no combinator) against el.
252// Handles tag / .class / #id / * ; SKIPS :pseudo and [attr] (we can't evaluate those -> ignored).
253func _rh_match_compound(buf: *u8, off: i64, len: i64, el: *CssElement) -> i64 {
254 let end: i64 = off + len
255 var i: i64 = off
256 var any: i64 = 0
257 while i < end {
258 let c: i64 = buf[i] & 0xff
259 if c == 46 {
260 let s: i64 = i + 1
261 let e: i64 = _rh_name_end(buf, s, end)
262 if _css_class_list_contains(buf, el.class_off, el.class_len, s, e - s) != 1 { return 0 }
263 any = 1; i = e
264 } else { if c == 35 {
265 let s: i64 = i + 1
266 let e: i64 = _rh_name_end(buf, s, end)
267 if _css_bytes_equal(buf, el.id_off, el.id_len, s, e - s) != 1 { return 0 }
268 any = 1; i = e
269 } else { if c == 42 {
270 any = 1; i = i + 1
271 } else { if c == 58 {
272 i = _rh_name_end(buf, i + 1, end)
273 } else { if c == 91 {
274 var jj: i64 = i + 1
275 var dn: i64 = 0
276 while dn == 0 { if jj >= end { dn = 1 } else { if (buf[jj]&0xff)==93 { jj=jj+1; dn=1 } else { jj=jj+1 } } }
277 i = jj
278 } else { if _rh_namechar(c) == 1 {
279 let s: i64 = i
280 let e: i64 = _rh_name_end(buf, s, end)
281 if _css_bytes_equal(buf, el.tag_off, el.tag_len, s, e - s) != 1 { return 0 }
282 any = 1; i = e
283 } else { i = i + 1 } } } } } }
284 }
285 return any
286}
287// Match a single (comma-free) selector against box_idx, walking ancestors for descendant/child combs.
288func _rh_match_one(buf: *u8, off: i64, len: i64, box_idx: i64, tree: *LayoutTree, boxel: *CssElement, coff: *i64, clen: *i64, ccmb: *i64) -> i64 {
289 // coff/clen/ccmb (>=32 i64 each) are caller-provided scratch -- HOISTED out of this fn: the cascade
290 // calls it O(n_boxes x n_rules) times and a per-call sys_mmap (no free) OOMs on real pages (a 422KB
291 // Wikipedia page x 8192 rules = ~134M calls x 768B). ccmb = combinator BEFORE this compound (1=desc 2=child).
292 var nc: i64 = 0
293 let end: i64 = off + len
294 var i: i64 = off
295 var pend: i64 = 0 // pending combinator for the next compound
296 while i < end {
297 let c: i64 = buf[i] & 0xff
298 if c == 32 { if pend == 0 { pend = 1 } i = i + 1 }
299 else { if c == 9 { if pend == 0 { pend = 1 } i = i + 1 }
300 else { if c == 62 { pend = 2; i = i + 1 }
301 else {
302 // start of a compound: read until ws / '>' / end
303 let s: i64 = i
304 var te: i64 = s
305 var done: i64 = 0
306 while done == 0 {
307 if te >= end { done = 1 }
308 else { let cc: i64 = buf[te]&0xff; if cc==32 { done=1 } else { if cc==9 { done=1 } else { if cc==62 { done=1 } else { te=te+1 } } } }
309 }
310 if nc < 32 { coff[nc]=s; clen[nc]=te - s; ccmb[nc]=pend; nc=nc+1 }
311 pend = 0
312 i = te
313 } } }
314 }
315 if nc == 0 { return 0 }
316 // match rightmost compound against the box
317 if _rh_match_compound(buf, coff[nc-1], clen[nc-1], ((boxel as i64)+box_idx*NX_CSS_ELEMENT_BYTES) as *CssElement) != 1 { return 0 }
318 var cur: i64 = box_idx
319 var ci: i64 = nc - 2
320 while ci >= 0 {
321 let comb: i64 = ccmb[ci+1]
322 if comb == 2 {
323 let cb: *LayoutBox = ((tree.boxes as i64)+cur*NX_LAYOUT_BOX_BYTES) as *LayoutBox
324 cur = cb.parent_idx
325 if cur < 0 { return 0 }
326 if _rh_match_compound(buf, coff[ci], clen[ci], ((boxel as i64)+cur*NX_CSS_ELEMENT_BYTES) as *CssElement) != 1 { return 0 }
327 } else {
328 var found: i64 = 0
329 let cb2: *LayoutBox = ((tree.boxes as i64)+cur*NX_LAYOUT_BOX_BYTES) as *LayoutBox
330 cur = cb2.parent_idx
331 var ag: i64 = 0
332 while found == 0 {
333 if cur < 0 { found = 2 }
334 else { if ag >= K_MAGIC_4096 { found = 2 }
335 else {
336 if _rh_match_compound(buf, coff[ci], clen[ci], ((boxel as i64)+cur*NX_CSS_ELEMENT_BYTES) as *CssElement) == 1 { found = 1 }
337 else { let cb3: *LayoutBox = ((tree.boxes as i64)+cur*NX_LAYOUT_BOX_BYTES) as *LayoutBox; cur = cb3.parent_idx; ag = ag + 1 }
338 }
339 }
340 }
341 if found != 1 { return 0 }
342 }
343 ci = ci - 1
344 }
345 return 1
346}
347// Match a full selector (possibly comma-separated) buf[off..off+len) against box_idx. 1 if ANY matches.
348func rh_match_complex(buf: *u8, off: i64, len: i64, box_idx: i64, tree: *LayoutTree, boxel: *CssElement, coff: *i64, clen: *i64, ccmb: *i64) -> i64 {
349 let end: i64 = off + len
350 var seg: i64 = off
351 var i: i64 = off
352 var matched: i64 = 0
353 var keep: i64 = 1
354 while keep == 1 {
355 var atend: i64 = 0
356 if i >= end { atend = 1 }
357 var iscomma: i64 = 0
358 if atend == 0 { if (buf[i]&0xff) == 44 { iscomma = 1 } }
359 if atend == 1 { if _rh_match_one(buf, seg, i - seg, box_idx, tree, boxel, coff, clen, ccmb) == 1 { matched = 1 } keep = 0 }
360 else { if iscomma == 1 { if _rh_match_one(buf, seg, i - seg, box_idx, tree, boxel, coff, clen, ccmb) == 1 { matched = 1 } seg = i + 1; i = i + 1 }
361 else { i = i + 1 } }
362 }
363 return matched
364}
365
366func rh_namechar2(c: i64) -> i64 {
367 if c>=48 { if c<=57 { return 1 } }
368 if c>=65 { if c<=90 { return 1 } }
369 if c>=97 { if c<=122 { return 1 } }
370 if c==45 { return 1 } // -
371 if c==95 { return 1 } // _
372 return 0
373}
374// CSS SPECIFICITY of selector buf[off..off+len) as a single comparable int: id*1e6 + (class+attr+pseudo)
375// *1e3 + type. (W3C cascade ยง6.4.3 a-b-c packed.) Approximate but correct for the common forms (tag,
376// .class, #id, p.class, .a .b, div>p). '*' = 0. Comma-lists use the whole-selector count (good enough;
377// per-matching-compound specificity is a later refinement).
378func rh_specificity(buf: *u8, off: i64, len: i64) -> i64 {
379 let end: i64 = off + len
380 var nid: i64 = 0
381 var ncls: i64 = 0
382 var ntype: i64 = 0
383 var i: i64 = off
384 while i < end {
385 let c: i64 = buf[i] & 0xff
386 if c == 35 { // '#' id
387 nid = nid + 1; i = i + 1
388 var s: i64 = 1
389 while s == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s=0 } } else { s=0 } }
390 } else { if c == 46 { // '.' class
391 ncls = ncls + 1; i = i + 1
392 var s2: i64 = 1
393 while s2 == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s2=0 } } else { s2=0 } }
394 } else { if c == 91 { // '[' attr
395 ncls = ncls + 1
396 var s3: i64 = 1
397 while s3 == 1 { if i < end { if (buf[i]&0xff)==93 { i=i+1; s3=0 } else { i=i+1 } } else { s3=0 } }
398 } else { if c == 58 { // ':' pseudo
399 ncls = ncls + 1; i = i + 1
400 if i < end { if (buf[i]&0xff)==58 { i=i+1 } }
401 var s4: i64 = 1
402 while s4 == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s4=0 } } else { s4=0 } }
403 } else { if c == 42 { // '*' universal -> 0
404 i = i + 1
405 } else { if rh_namechar2(c) == 1 { // bare type selector
406 ntype = ntype + 1
407 var s5: i64 = 1
408 while s5 == 1 { if i < end { if rh_namechar2(buf[i]&0xff)==1 { i=i+1 } else { s5=0 } } else { s5=0 } }
409 } else { // whitespace / > / + / ~ / , -> skip
410 i = i + 1
411 } } } } } }
412 }
413 return nid*K_MAGIC_1000000 + ncls*1000 + ntype
414}
415
416// Promote INLINE boxes that carry an explicit block-level `display` (flex/grid/block/table) to BLOCK kind, so
417// width/height apply (nx_layout_block block path) AND background/border paint (both gated on kind==BLOCK).
418// Box kind is otherwise assigned from the TAG name only (nx_layout_from_dom), ignoring CSS `display` -- so
419// `<span>` with `display:flex` (e.g. the office .fico kind-badges) never got a painted, sized box. SAFE BY
420// CONSTRUCTION: only promotes INLINE *up*, never demotes a BLOCK box -> pinned block-level layouts are
421// byte-identical; a page with NO display-overridden inline element is unchanged. Runs after cascade, before
422// layout. (inline-block/inline-flex intentionally NOT promoted here -- INLINE_BLOCK bg paint is a separate rung.)
423func rh_promote_display_kinds(tree: *LayoutTree, computed: *CssComputedDecl, ncomp: i64, src: *u8) -> i64 {
424 let dprop: *u8 = "display\x00" as *u8
425 var bi: i64 = 0
426 while bi < tree.count {
427 let b: *LayoutBox = ((tree.boxes as i64) + bi * NX_LAYOUT_BOX_BYTES) as *LayoutBox
428 if b.kind == NX_LAYOUT_BOX_INLINE {
429 var promote: i64 = 0
430 var j: i64 = ncomp - 1
431 while j >= 0 {
432 let cd: *CssComputedDecl = ((computed as i64) + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
433 if cd.element_idx == bi { if cd.prop_len == 7 {
434 var sp: i64 = 1
435 var k: i64 = 0
436 while k < 7 { if (src[cd.prop_off+k]&0xff) != (dprop[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
437 if sp == 1 {
438 let vl: i64 = cd.val_len
439 let v0: i64 = src[cd.val_off]&0xff
440 // Block-level & "blockified" display values -> promote INLINE box to BLOCK so it gets a
441 // sized box AND paints. last-wins: first match stops the reverse scan.
442 // vl==4: flex(f)/grid(g) -- NOT none(n)
443 // vl==5: block(b)/table(t)
444 // vl==11: inline-flex / inline-grid (buttons/pills: <a class=btn display:inline-flex>)
445 // vl==12: inline-block / inline-table (badges/tags/chips: <a class=chip display:inline-block>)
446 // inline-* are spec-inline-level -> INLINE_BLOCK (promote=2): they now flow ATOMICALLY on
447 // the line (padding/border/width honored + bg painted) instead of each taking a full-width
448 // block row. Fixes chip/tag/nav-pill lists (the atlas Family-Tree page = the driver).
449 if vl == 4 { if v0 == 102 { promote = 1 } if v0 == 103 { promote = 1 } }
450 if vl == 5 { if v0 == 98 { promote = 1 } if v0 == 116 { promote = 1 } }
451 if vl == 11 { promote = 2 }
452 if vl == 12 { promote = 2 }
453 j = 0
454 }
455 } }
456 j = j - 1
457 }
458 if promote == 1 { b.kind = NX_LAYOUT_BOX_BLOCK }
459 if promote == 2 { b.kind = NX_LAYOUT_BOX_INLINE_BLOCK }
460 }
461 bi = bi + 1
462 }
463 return 0
464}
465
466// Cascade using the COMPLEX matcher (compound/descendant/child) + the box tree (for ancestor walks).
467// Output = CssComputedDecl[] consumed by last-match-wins readers. Per box, matched rules are emitted in
468// ASCENDING-SPECIFICITY order (stable: ties keep source order), so the existing reverse-scan-last-wins
469// naturally yields SPECIFICITY-then-source-order -- correct CSS cascade -- with NO reader change.
470func rh_cascade(buf: *u8, rules: *CssRule, n_rules: i64, decls: *CssDeclaration, n_decls: i64,
471 boxel: *CssElement, n_boxes: i64, tree: *LayoutTree,
472 computed: *CssComputedDecl, max_computed: i64, out_count: *i64) -> i64 {
473 // selector-matcher scratch allocated ONCE (not per match call) -- the per-call mmap was the cascade's
474 // OOM on real pages. Reused across every box x rule. >=32 i64 each (the compound-selector cap).
475 let mc_coff: *i64 = sys_mmap(8*32) as *i64
476 let mc_clen: *i64 = sys_mmap(8*32) as *i64
477 let mc_ccmb: *i64 = sys_mmap(8*32) as *i64
478 // precompute specificity per rule ONCE (not per box)
479 let spec: *i64 = sys_mmap(8 * (n_rules + 1)) as *i64
480 var rr: i64 = 0
481 while rr < n_rules {
482 let rl: *CssRule = (rules as *u8 + (rr as nx_size) * (NX_CSS_RULE_BYTES as nx_size)) as *CssRule
483 if rl.sel_full_len > 0 { spec[rr] = rh_specificity(buf, rl.sel_full_off, rl.sel_full_len) } else { spec[rr] = 0 }
484 rr = rr + 1
485 }
486 let matched: *i64 = sys_mmap(8 * (n_rules + 1)) as *i64
487 var written: i64 = 0
488 var e: i64 = 0
489 while e < n_boxes {
490 // gather matched rule indices for this box, in source order
491 var nm: i64 = 0
492 var r: i64 = 0
493 while r < n_rules {
494 let rule: *CssRule = (rules as *u8 + (r as nx_size) * (NX_CSS_RULE_BYTES as nx_size)) as *CssRule
495 if rule.sel_full_len > 0 {
496 if rh_match_complex(buf, rule.sel_full_off, rule.sel_full_len, e, tree, boxel, mc_coff, mc_clen, mc_ccmb) == 1 { matched[nm] = r; nm = nm + 1 }
497 }
498 r = r + 1
499 }
500 // STABLE insertion sort matched[0..nm) by specificity ASCENDING (equal spec keeps source order):
501 // shift only strictly-greater, so the existing reverse-scan-last-wins yields specificity-then-source.
502 var a: i64 = 1
503 while a < nm {
504 let key: i64 = matched[a]
505 let ks: i64 = spec[key]
506 var b: i64 = a - 1
507 var done: i64 = 0
508 while done == 0 {
509 if b < 0 { done = 1 }
510 else { if spec[matched[b]] > ks { matched[b+1] = matched[b]; b = b - 1 } else { done = 1 } }
511 }
512 matched[b+1] = key
513 a = a + 1
514 }
515 // emit declarations in ascending-specificity order
516 var m: i64 = 0
517 while m < nm {
518 let rule: *CssRule = (rules as *u8 + (matched[m] as nx_size) * (NX_CSS_RULE_BYTES as nx_size)) as *CssRule
519 var d: i64 = 0
520 while d < rule.decl_count {
521 let sdi: i64 = rule.decl_first + d
522 if sdi >= n_decls { d = rule.decl_count }
523 else {
524 if written >= max_computed { out_count[0] = written; return 0 }
525 let sd: *CssDeclaration = (decls as *u8 + (sdi as nx_size) * (NX_CSS_DECLARATION_BYTES as nx_size)) as *CssDeclaration
526 let cd: *CssComputedDecl = (computed as *u8 + (written as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
527 cd.element_idx = e
528 cd.prop_off = sd.prop_off; cd.prop_len = sd.prop_len
529 cd.val_kind = sd.val_kind; cd.val_off = sd.val_off; cd.val_len = sd.val_len
530 cd.unit_off = sd.unit_off; cd.unit_len = sd.unit_len
531 written = written + 1; d = d + 1
532 }
533 }
534 m = m + 1
535 }
536 e = e + 1
537 }
538 out_count[0] = written
539 return 1
540}
541
542// Render `raw` (hlen bytes of HTML) into a freshly-allocated framebuffer. out3 receives [VW, VH,
543// box_count]; returns the framebuffer (0 if no content). The render core; the to-bmp/to-png writers
544// below share it (one render path, two output formats).
545func _rh_to_fb(raw: *u8, hlen: i64, out3: *i64) -> *Framebuffer {
546 out3[0] = 0; out3[1] = 0; out3[2] = 0
547 if hlen <= 0 { return 0 as *Framebuffer }
548 // UA stylesheet: heading bars + box-model SPACING + BLUE LINKS + FONT-SIZE hierarchy. nx_layout_block
549 // honors margin/padding/font-size and nx_paint_walk_layout honors color/font-size from the cascade.
550 let css: *u8 = "h1{background-color:#a8c8ff;margin-top:10px;margin-bottom:4px;font-size:40px}h2{background-color:#d0d0d0;margin-top:10px;margin-bottom:3px;font-size:28px}h3{background-color:#e4e4e4;margin-top:6px;margin-bottom:2px;font-size:24px}body{background-color:#ffffff;padding-left:8px;padding-top:6px}p{margin-top:6px;margin-bottom:5px}ul{padding-left:16px;margin-top:3px;margin-bottom:3px}ol{padding-left:24px;margin-top:3px;margin-bottom:3px}li{margin-bottom:2px}tr{display:flex}th{font-weight:bold;padding-left:4px;padding-right:4px}td{padding-left:4px;padding-right:4px}a{color:#3366cc}b{font-weight:bold}strong{font-weight:bold}i{font-style:italic}em{font-style:italic}.vector-menu-content-list{display:none}.vector-dropdown{display:none}.mw-interlanguage-selector{display:none}.wbc-editpage{display:none}.infobox{float:right;width:300px;margin-left:8px;background-color:#f8f8f8}\x00"
551 let clen: i64 = rh_slen(css)
552 let STYCAP: i64 = K_MAGIC_262144
553 let buf: *u8 = sys_mmap(hlen + STYCAP + clen + 16)
554 var i: i64 = 0
555 while i < hlen { buf[i] = raw[i]; i = i + 1 }
556 // [HTML][page's own inline <style> CSS, @media-FILTERED to our viewport][UA reader stylesheet].
557 // Now that flex (R1) + @media filtering (R2) exist, apply the page's inline CSS but ONLY the rules
558 // matching our 800px width (rh_filter_media) -- so conflicting narrow/wide @media rules no longer
559 // all fire at once. (Linked load.php stylesheets, which hold the vector-menu chrome rules, are a
560 // later rung: fetch them too.)
561 // [HTML][page's @media-filtered inline CSS][UA reader stylesheet LAST]. UA goes LAST so our reader
562 // rules WIN the cascade (last-in-source-order) for the elements we target (chrome display:none, heading
563 // bars, blue links, tables, bold/italic, .infobox). Now SAFE: declaration-level parser recovery means
564 // a malformed page declaration no longer aborts the sheet, so UA-last is reached. (The first attempt at
565 // this broke because the page CSS aborted at a multi-value decl before UA parsed -- recovery fixed it.)
566 let rawsty: *u8 = sys_mmap(STYCAP)
567 let rawstylen: i64 = rh_extract_styles(raw, hlen, rawsty, STYCAP)
568 let stylen: i64 = rh_filter_media(rawsty, 0, rawstylen, ((buf as i64)+hlen) as *u8, STYCAP, 800)
569 i = 0
570 while i < clen { buf[hlen + stylen + i] = css[i]; i = i + 1 }
571 let total: i64 = hlen + stylen + clen
572
573 let VW: i64 = 800
574 let boxes: *LayoutBox = (sys_mmap(NX_LAYOUT_BOX_BYTES*K_MAGIC_16384+16)) as *LayoutBox
575 let tree: *LayoutTree = (sys_mmap(64)) as *LayoutTree
576 nx_layout_tree_init(tree, boxes, K_MAGIC_16384)
577 let stk: *LayoutFromDomStack = (sys_mmap(NX_LAYOUT_FROM_DOM_STACK_BYTES+8)) as *LayoutFromDomStack
578 let sidx: *i64 = (sys_mmap(8*128)) as *i64
579 nx_layout_from_dom_stack_init(stk, sidx, 128)
580 nx_layout_from_dom(buf, hlen, tree, stk)
581
582 let ext: *CssElement = (sys_mmap(NX_CSS_ELEMENT_BYTES*K_MAGIC_16384 as nx_size)) as *CssElement
583 let n_ext: i64 = rh_extract(buf, hlen, ext, K_MAGIC_16384)
584 let boxel: *CssElement = (sys_mmap((NX_CSS_ELEMENT_BYTES*(tree.count+1)) as nx_size)) as *CssElement
585 var k: i64 = 0
586 i = 0
587 while i < tree.count {
588 let b: *LayoutBox = ((tree.boxes as i64)+i*NX_LAYOUT_BOX_BYTES) as *LayoutBox
589 let e: *CssElement = ((boxel as i64)+i*NX_CSS_ELEMENT_BYTES) as *CssElement
590 e.src = buf
591 var mapped: i64 = 0
592 if rh_is_elem(b.kind)==1 { if b.parent_idx != (0-1) { if k < n_ext {
593 let s: *CssElement = ((ext as i64)+k*NX_CSS_ELEMENT_BYTES) as *CssElement
594 e.tag_off=s.tag_off; e.tag_len=s.tag_len; e.id_off=s.id_off; e.id_len=s.id_len; e.class_off=s.class_off; e.class_len=s.class_len
595 k=k+1; mapped=1
596 } } }
597 if mapped==0 { e.tag_off=0; e.tag_len=0; e.id_off=0; e.id_len=0; e.class_off=0; e.class_len=0 }
598 i = i + 1
599 }
600
601 let cur: *CssCursor = (sys_mmap(NX_CSS_CURSOR_BYTES) as nx_size) as *CssCursor
602 nx_css_cursor_init(cur, buf, total); cur.pos = hlen
603 let tok: *CssToken = (sys_mmap(NX_CSS_TOKEN_BYTES) as nx_size) as *CssToken
604 let rules: *CssRule = (sys_mmap(NX_CSS_RULE_BYTES*K_MAGIC_8192 as nx_size)) as *CssRule
605 let decls: *CssDeclaration = (sys_mmap(NX_CSS_DECLARATION_BYTES*K_MAGIC_32768 as nx_size)) as *CssDeclaration
606 let st: *CssParseState = (sys_mmap(128) as nx_size) as *CssParseState
607 nx_css_parse_state_init(st, cur, tok, rules, K_MAGIC_8192, decls, K_MAGIC_32768)
608 nx_css_parse(st)
609 let computed: *CssComputedDecl = (sys_mmap(NX_CSS_COMPUTED_DECL_BYTES*K_MAGIC_262144 as nx_size)) as *CssComputedDecl
610 let cb: *i64 = (sys_mmap(8)) as *i64
611 rh_cascade(buf, rules, st.rule_count, decls, st.decl_count, boxel, tree.count, tree, computed, K_MAGIC_262144, cb)
612 let ncomp: i64 = cb[0]
613 rh_promote_display_kinds(tree, computed, ncomp, buf) // inline + display:flex/grid/block -> BLOCK (sized + painted)
614
615 let pbuf: *u8 = sys_mmap(256)
616 let ltable: *LayoutPropTable = (sys_mmap(NX_LAYOUT_PROP_TABLE_BYTES)) as *LayoutPropTable
617 nx_layout_prop_table_init(ltable, pbuf)
618 let resolve: *CssResolveCtx = (sys_mmap(NX_CSS_RESOLVE_CTX_BYTES)) as *CssResolveCtx
619 resolve.root_font_size_px = 16; resolve.parent_font_size_px = 16; resolve.parent_dimension_px = VW
620 let lctx: *LayoutCtx = (sys_mmap(NX_LAYOUT_CTX_BYTES)) as *LayoutCtx
621 nx_layout_ctx_init(lctx, tree, computed, ncomp, buf, VW, resolve)
622 let page_h: i64 = nx_layout_block_layout(lctx, ltable, 0)
623 var VH: i64 = page_h
624 if VH < 64 { VH = 64 }
625 if VH > K_MAGIC_5000 { VH = K_MAGIC_5000 }
626
627 let fbpx: *u8 = (sys_mmap((VW*VH*4) as nx_size)) as *u8
628 let fb: *Framebuffer = (sys_mmap(NX_FRAMEBUFFER_BYTES as nx_size)) as *Framebuffer
629 nx_framebuffer_init(fb, fbpx, VW, VH); nx_framebuffer_clear(fb)
630 let ptbuf: *u8 = (sys_mmap(64)) as *u8
631 let ptable: *PaintPropTable = (sys_mmap(NX_PAINT_PROP_TABLE_BYTES as nx_size)) as *PaintPropTable
632 nx_paint_prop_table_init(ptable, ptbuf)
633 let pctx: *PaintCtx = (sys_mmap(NX_PAINT_CTX_BYTES as nx_size)) as *PaintCtx
634 nx_paint_ctx_init(pctx, fb, tree, computed, ncomp, buf, ptable)
635 let ink: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
636 ink.r=0; ink.g=0; ink.b=0; ink.a=255
637 // Text prep per TEXT box: HTML-entity decode -> UTF-8-decode + ASCII-fold (accented Latin ->
638 // base letter so language names/diacritics are READABLE in the ASCII 5x7 font, non-Latin skipped),
639 // accumulated into one scratch buffer the paint reads from.
640 let dtxt: *u8 = sys_mmap(hlen + 16)
641 let dtmp: *u8 = sys_mmap(hlen + 16)
642 var dpos: i64 = 0
643 var di: i64 = 0
644 while di < tree.count {
645 let dbx: *LayoutBox = ((tree.boxes as i64) + di*NX_LAYOUT_BOX_BYTES) as *LayoutBox
646 if dbx.kind == NX_LAYOUT_BOX_TEXT {
647 let denc: i64 = nx_html_decode_entities(buf, dbx.text_off, dbx.text_len, dtmp, hlen + 16)
648 let dnl: i64 = nx_fold_ascii(dtmp, 0, denc, ((dtxt as i64)+dpos) as *u8, (hlen+16)-dpos)
649 dbx.text_off = dpos
650 dbx.text_len = dnl
651 dpos = dpos + dnl
652 }
653 di = di + 1
654 }
655 nx_paint_ctx_set_text(pctx, dtxt, ink)
656 nx_paint_ctx_set_boxel(pctx, boxel) // enable <ul><li> disc bullets
657 nx_paint_walk_layout(pctx, 0)
658
659 // CONTENT FOCUS: if the page has an HTML5 <main> (the article container), crop the output to it so
660 // the header/sidebar nav + footer chrome don't bury the article. General (HTML5 semantic element).
661 var cy0: i64 = 0
662 var cy1: i64 = VH
663 var mi: i64 = 0
664 var fnd: i64 = 0
665 while mi < tree.count {
666 if fnd == 0 {
667 let mel: *CssElement = ((boxel as i64)+mi*NX_CSS_ELEMENT_BYTES) as *CssElement
668 if mel.tag_len == 4 {
669 if (buf[mel.tag_off]&0xff)==0x6D { if (buf[mel.tag_off+1]&0xff)==0x61 { if (buf[mel.tag_off+2]&0xff)==0x69 { if (buf[mel.tag_off+3]&0xff)==0x6E {
670 let mbx: *LayoutBox = ((tree.boxes as i64)+mi*NX_LAYOUT_BOX_BYTES) as *LayoutBox
671 cy0 = mbx.y
672 cy1 = mbx.y + mbx.h
673 fnd = 1
674 } } } }
675 }
676 }
677 mi = mi + 1
678 }
679 if cy1 > VH { cy1 = VH }
680 if cy0 < 0 { cy0 = 0 }
681 if cy0 >= cy1 { cy0 = 0; cy1 = VH }
682
683 out3[0] = VW
684 out3[1] = VH
685 out3[2] = tree.count
686 out3[3] = cy0
687 out3[4] = cy1
688 return fb
689}
690
691// Convert the rendered framebuffer (top-down RGBA, transparent->white) to a PNG via the sovereign
692// nx_png_write_rgb -- so the browse output is DIRECTLY viewable (Read/Photos show PNG, not BMP).
693func rh_save_png(path: *u8, fb: *Framebuffer, w: i64, y0: i64, y1: i64) -> i64 {
694 let h: i64 = y1 - y0
695 let rgb: *u8 = sys_mmap(w * h * 3 + 16)
696 let px: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
697 var yy: i64 = 0
698 while yy < h {
699 let fy: i64 = y0 + yy
700 var x: i64 = 0
701 while x < w {
702 nx_framebuffer_get_pixel(fb, x, fy, px)
703 let d: i64 = (yy * w + x) * 3
704 if px.a == 0 { rgb[d]=255 as u8; rgb[d+1]=255 as u8; rgb[d+2]=255 as u8 }
705 else { rgb[d]=px.r as u8; rgb[d+1]=px.g as u8; rgb[d+2]=px.b as u8 }
706 x = x + 1
707 }
708 yy = yy + 1
709 }
710 nx_png_write_rgb(path, rgb, w, h)
711 return 0
712}
713
714// Render `raw` (hlen bytes of HTML) to a styled 24-bit BMP at `bmp_path`. Returns the box count.
715func nx_render_html_to_bmp(raw: *u8, hlen: i64, bmp_path: *u8) -> i64 {
716 let o3: *i64 = sys_mmap(64) as *i64
717 let fb: *Framebuffer = _rh_to_fb(raw, hlen, o3)
718 if o3[2] <= 0 { return 0 }
719 rh_save(bmp_path, fb, o3[0], o3[3], o3[4])
720 return o3[2]
721}
722
723// Render `raw` to a styled PNG at `png_path` (directly viewable, no BMP->PNG hop). Returns box count.
724func nx_render_html_to_png(raw: *u8, hlen: i64, png_path: *u8) -> i64 {
725 let o3: *i64 = sys_mmap(64) as *i64
726 let fb: *Framebuffer = _rh_to_fb(raw, hlen, o3)
727 if o3[2] <= 0 { return 0 }
728 rh_save_png(png_path, fb, o3[0], o3[3], o3[4])
729 return o3[2]
730}