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}