code wiki / _hdl_build / nx_reader_render.nx

nx_reader_render.nx source

↩ module page · 456 lines · 30265 B

1// nx_reader_render.nx -- SOVEREIGN, ZERO-JAVASCRIPT reader. Operator law: "we shouldn't be using js -- we want 2// the nishi ecosystem everywhere." So ALL reader logic lives in this Nishi organ (parse book.json + chapters + 3// toc + assets, lay out the page); the browser runs NO code at all. Interactions are pure HTML+CSS: 4// - theme + font-size switching via hidden <input type=radio> + the `#id:checked ~ .app{--vars}` technique 5// - the Contents drawer via native <details>/<summary> 6// - navigation + TOC jumps via plain <a href="#cN"> anchors 7// Output: knowledge/staging/media/reader_static_<slug>.html -- a self-contained page, 0 <script>, 0 third-party. 8// Reads OUR own book.json (emitted by nx_epub_book) + chap<N>.txt + extracted images. expect_exit: 0 ORIGINAL 9import "nx_syscalls.nx" 10const K_MAGIC_1024: i64 = 1024 11const K_MAGIC_8192: i64 = 8192 12const K_MAGIC_1099: i64 = 1099 13const K_MAGIC_1100: i64 = 1100 14 15func slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 16func w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, slen(s)); return 0 } 17func wn(fd: i64, v0: i64) -> i64 { var v: i64=v0; if v<0 { sys_write(fd,"-" as *u8,1); v=0-v } let b: *u8=sys_mmap(24); var k: i64=0; if v==0 {b[0]=48 as u8;k=1} while v>0 {b[k]=(48+(v%10)) as u8; v=v/10; k=k+1} let o: *u8=sys_mmap(24); var j: i64=0; while j<k {o[j]=b[k-1-j];j=j+1} sys_write(fd,o,k); return 0 } 18func p(s: *u8) -> i64 { w(1, s); return 0 } 19 20// write bytes [0,n) of s to fd, HTML-escaping & < > (safe for text content) 21func wesc_n(fd: i64, s: *u8, n: i64) -> i64 { 22 var i: i64 = 0 23 while i < n { 24 let c: i64 = s[i] as i64 25 if c == 0x26 { w(fd, "&amp;" as *u8) } 26 else { if c == 0x3c { w(fd, "&lt;" as *u8) } 27 else { if c == 0x3e { w(fd, "&gt;" as *u8) } 28 else { sys_write(fd, (s as i64 + i) as *u8, 1) } } } 29 i = i + 1 30 } 31 return 0 32} 33func wesc_z(fd: i64, s: *u8) -> i64 { wesc_n(fd, s, slen(s)); return 0 } 34 35func jfrom(hay: *u8, hl: i64, start: i64, needle: *u8) -> i64 { 36 let nl: i64 = slen(needle); if nl == 0 { return 0-1 } 37 var i: i64 = start; if i < 0 { i = 0 } 38 while i + nl <= hl { var k: i64=0; var hit: i64=1; while k<nl { if hay[i+k]!=needle[k]{hit=0;k=nl}else{k=k+1} } if hit==1 {return i} i=i+1 } 39 return 0-1 40} 41// parse a (possibly negative) int starting at/after pos (skips one leading non-digit run up to a digit/minus). 42func jint(hay: *u8, hl: i64, pos: i64) -> i64 { 43 var i: i64 = pos 44 while i < hl { let c: i64 = hay[i] as i64; if c==0x2d { break } if c>=48 { if c<=57 { break } } i=i+1 } 45 var neg: i64 = 0 46 if i < hl { if hay[i]==(0x2d as u8) { neg=1; i=i+1 } } 47 var v: i64 = 0 48 while i < hl { let c: i64 = hay[i] as i64; if c<48 { break } if c>57 { break } v = v*10 + (c-48); i=i+1 } 49 if neg==1 { return 0-v } 50 return v 51} 52// copy a JSON string value into out: pos = index of the OPENING quote; unescape \" and \\ ; stop at closing ". 53func jstr(hay: *u8, hl: i64, pos: i64, out: *u8, cap: i64) -> i64 { 54 var i: i64 = pos 55 if i < hl { if hay[i]==(0x22 as u8) { i=i+1 } } // skip opening quote 56 var o: i64 = 0 57 while i < hl { 58 let c: i64 = hay[i] as i64 59 if c == 0x22 { i = hl } // closing quote -> stop 60 else { if c == 0x5c { if i+1 < hl { if o<cap-1 { out[o]=hay[i+1]; o=o+1 } i=i+2 } else { i=i+1 } } 61 else { if o<cap-1 { out[o]=hay[i] as u8; o=o+1 } i=i+1 } } 62 } 63 out[o] = 0 as u8 64 return o 65} 66// find `"key":"` then jstr the value. returns len or -1. 67func jval_str(hay: *u8, hl: i64, key: *u8, out: *u8, cap: i64) -> i64 { 68 let at: i64 = jfrom(hay, hl, 0, key) 69 if at < 0 { return 0-1 } 70 return jstr(hay, hl, at + slen(key) - 1, out, cap) // key ends with the opening quote 71} 72// parse the int at/after posp[0] (skipping non-digit/minus), advancing posp[0] past it -- for the flat by:[c,n,...] array 73func jnext(hay: *u8, hl: i64, posp: *i64) -> i64 { 74 var i: i64 = posp[0] 75 while i < hl { let c: i64 = hay[i] as i64; if c==0x2d { break } if c>=48 { if c<=57 { break } } i=i+1 } 76 var neg: i64 = 0 77 if i < hl { if hay[i]==(0x2d as u8) { neg=1; i=i+1 } } 78 var v: i64 = 0 79 while i < hl { let c: i64 = hay[i] as i64; if c<48 { break } if c>57 { break } v=v*10+(c-48); i=i+1 } 80 posp[0] = i 81 if neg==1 { return 0-v } 82 return v 83} 84 85const CAP: i64 = 4194304 86 87// 1 if txt[s..e) equals the NUL-terminated title exactly. 88func line_eq(txt: *u8, s: i64, e: i64, title: *u8) -> i64 { 89 var tl: i64 = 0; while title[tl] != (0 as u8) { tl = tl + 1 } 90 if (e - s) != tl { return 0 } 91 var k: i64 = 0 92 while k < tl { if txt[s+k] != title[k] { return 0 } k = k + 1 } 93 return 1 94} 95// render one chapter's decoded text: blank-split into <p>; a [[nximg:FILE]] line -> <img src="reader/<slug>/FILE">. 96// Skips leading lines that DUPLICATE the chapter title (nx_html_to_text renders both <title> and <h1> -> the 97// chapter heading would otherwise repeat 2-3x before the prose). 98// write paragraph bytes [0,n), HTML-escaping & < >, but expanding [[fnref:X:D]] markers (emitted by nx_epub_book 99// fn_markerize) into a zero-JS superscript footnote ref: <sup class="fnref"><a id="ref-X" href="#X">D</a></sup>. 100func w_para_fn(fd: i64, s: *u8, n: i64) -> i64 { 101 var i: i64 = 0 102 while i < n { 103 var isfn: i64 = 0 104 if i + 8 <= n { if s[i]==(0x5b as u8) { if s[i+1]==(0x5b as u8) { if s[i+2]==(0x66 as u8) { if s[i+3]==(0x6e as u8) { if s[i+4]==(0x72 as u8) { if s[i+5]==(0x65 as u8) { if s[i+6]==(0x66 as u8) { if s[i+7]==(0x3a as u8) { isfn=1 } } } } } } } } } 105 if isfn == 1 { 106 var j: i64 = i + 8 107 let xb: *u8 = sys_mmap(128); var xo: i64 = 0 108 while j < n { if s[j]==(0x3a as u8) { break } if xo<127 { xb[xo]=s[j]; xo=xo+1 } j=j+1 } 109 xb[xo]=0 as u8 110 if j < n { j=j+1 } 111 let db: *u8 = sys_mmap(64); var dd: i64 = 0 112 while j < n { var stop: i64=0; if s[j]==(0x5d as u8) { if j+1<n { if s[j+1]==(0x5d as u8) { stop=1 } } } if stop==1 { break } if dd<63 { db[dd]=s[j]; dd=dd+1 } j=j+1 } 113 db[dd]=0 as u8 114 if j+1 < n { j=j+2 } else { j=n } 115 w(fd, "<sup class=\"fnref\"><a id=\"ref-" as *u8); wesc_z(fd, xb); w(fd, "\" href=\"#" as *u8); wesc_z(fd, xb); w(fd, "\">" as *u8); wesc_z(fd, db); w(fd, "</a></sup>" as *u8) 116 i = j 117 } else { 118 let c: i64 = s[i] as i64 119 if c == 0x26 { w(fd, "&amp;" as *u8) } 120 else { if c == 0x3c { w(fd, "&lt;" as *u8) } 121 else { if c == 0x3e { w(fd, "&gt;" as *u8) } 122 else { sys_write(fd, (s as i64 + i) as *u8, 1) } } } 123 i = i + 1 124 } 125 } 126 return 0 127} 128func render_text(fd: i64, txt: *u8, n: i64, slug: *u8, title: *u8) -> i64 { 129 var i: i64 = 0 130 var sk: i64 = 1 131 while sk == 1 { 132 var j: i64 = i 133 var wj: i64 = 1 134 while wj == 1 { if j >= n { wj = 0 } else { let c: i64 = txt[j] as i64; if c==10 { j=j+1 } else { if c==32 { j=j+1 } else { if c==13 { j=j+1 } else { if c==9 { j=j+1 } else { wj=0 } } } } } } 135 if j >= n { i = j; sk = 0 } else { 136 var le: i64 = j; while le < n { if txt[le]==(10 as u8) { break } le=le+1 } 137 var te: i64 = le; while te > j { let c2: i64 = txt[te-1] as i64; if c2==32 { te=te-1 } else { if c2==13 { te=te-1 } else { if c2==9 { te=te-1 } else { break } } } } 138 if line_eq(txt, j, te, title) == 1 { i = le + 1 } else { i = j; sk = 0 } 139 } 140 } 141 while i < n { 142 var e: i64 = i 143 while e < n { if txt[e]==(10 as u8) { break } e=e+1 } // line [i,e) 144 // trim trailing CR/space 145 var le: i64 = e 146 while le > i { let c: i64 = txt[le-1] as i64; if c==13 { le=le-1 } else { if c==32 { le=le-1 } else { break } } } 147 // non-empty? 148 var has: i64 = 0 149 var k: i64 = i 150 while k < le { let c: i64 = txt[k] as i64; if c!=32 { if c!=9 { has=1; k=le } else {k=k+1} } else {k=k+1} } 151 if has == 1 { 152 // [[nximg:FILE]] ? 153 var isimg: i64 = 0 154 if le - i >= 10 { if jfrom(txt, le, i, "[[nximg:" as *u8) == i { if txt[le-1]==(0x5d as u8) { if txt[le-2]==(0x5d as u8) { isimg=1 } } } } 155 if isimg == 1 { 156 let fs: i64 = i + 8 157 let fe: i64 = le - 2 158 w(fd, "<img src=\"reader/" as *u8); w(fd, slug); w(fd, "/" as *u8) 159 sys_write(fd, (txt as i64 + fs) as *u8, fe - fs) 160 w(fd, "\" alt=\"\">\n" as *u8) 161 } else { 162 w(fd, "<p>" as *u8); w_para_fn(fd, (txt as i64 + i) as *u8, le - i); w(fd, "</p>\n" as *u8) 163 } 164 } 165 i = e + 1 166 } 167 return 0 168} 169 170func main(argc: i64, argv: *i64) -> i64 { 171 var slug: *u8 = "nishi_fixture" as *u8 172 if argc >= 2 { slug = argv[1] as *u8 } 173 174 // paths 175 let dir: *u8 = sys_mmap(K_MAGIC_1024) 176 var dl: i64 = 0 177 let pfx: *u8 = "knowledge/staging/media/reader/" as *u8 178 var z: i64 = 0; while pfx[z]!=(0 as u8){ dir[dl]=pfx[z]; dl=dl+1; z=z+1 } 179 z = 0; while slug[z]!=(0 as u8){ dir[dl]=slug[z]; dl=dl+1; z=z+1 } 180 dir[dl] = 0 as u8 181 let bjp: *u8 = sys_mmap(K_MAGIC_1024) 182 var bl: i64 = 0; while dir[bl]!=(0 as u8){ bjp[bl]=dir[bl]; bl=bl+1 } 183 let sj: *u8 = "/book.json" as *u8; z=0; while sj[z]!=(0 as u8){ bjp[bl]=sj[z]; bl=bl+1; z=z+1 } bjp[bl]=0 as u8 184 185 let lp: *i64 = sys_mmap(8) as *i64; lp[0]=0 186 let bj: *u8 = sys_read_file(bjp, lp) 187 if (bj as i64)==0 { p("READER-RENDER FAIL read book.json: " as *u8); p(bjp); p("\n" as *u8); sys_exit(1); return 1 } 188 let bn: i64 = lp[0] 189 190 // X-Ray index (book-intrinsic notable terms from nx_reader_xray). ABSENT-safe: if not generated, no panel. 191 let xrp: *u8 = sys_mmap(K_MAGIC_1024) 192 var xpl: i64 = 0; while dir[xpl]!=(0 as u8){ xrp[xpl]=dir[xpl]; xpl=xpl+1 } 193 let xsf: *u8 = "/xray.json" as *u8; z=0; while xsf[z]!=(0 as u8){ xrp[xpl]=xsf[z]; xpl=xpl+1; z=z+1 } xrp[xpl]=0 as u8 194 let xlp: *i64 = sys_mmap(8) as *i64; xlp[0]=0 195 let xj: *u8 = sys_read_file(xrp, xlp) 196 let xn: i64 = xlp[0] 197 198 let title: *u8 = sys_mmap(K_MAGIC_1024); title[0]=0 as u8; jval_str(bj, bn, "\"title\":\"" as *u8, title, K_MAGIC_1024) 199 let author: *u8 = sys_mmap(K_MAGIC_1024); author[0]=0 as u8; jval_str(bj, bn, "\"author\":\"" as *u8, author, K_MAGIC_1024) 200 let cover: *u8 = sys_mmap(512); cover[0]=0 as u8; jval_str(bj, bn, "\"cover\":\"" as *u8, cover, 512) 201 let publisher: *u8 = sys_mmap(K_MAGIC_1024); publisher[0]=0 as u8; jval_str(bj, bn, "\"publisher\":\"" as *u8, publisher, K_MAGIC_1024) 202 let pubdate: *u8 = sys_mmap(256); pubdate[0]=0 as u8; jval_str(bj, bn, "\"pubdate\":\"" as *u8, pubdate, 256) 203 let descr: *u8 = sys_mmap(K_MAGIC_8192); descr[0]=0 as u8; jval_str(bj, bn, "\"description\":\"" as *u8, descr, K_MAGIC_8192) 204 // reading-time pre-pass: sum chapters[].chars -> estimated minutes (~1100 chars/min) + chapter count 205 var total_chars: i64 = 0 206 var nchap: i64 = 0 207 let pcs: i64 = jfrom(bj, bn, 0, "\"chapters\":[" as *u8) 208 if pcs >= 0 { 209 var pce: i64 = jfrom(bj, bn, pcs, "],\"nchapters\":" as *u8) // end of chapters[] (NOT first "]", which now matches a nested fn:[]) 210 if pce < 0 { pce = bn } 211 var pcur: i64 = pcs 212 var pgo: i64 = 1 213 while pgo == 1 { 214 let cat: i64 = jfrom(bj, bn, pcur, "\"chars\":" as *u8) 215 if cat < 0 { pgo = 0 } else { if cat > pce { pgo = 0 } else { 216 total_chars = total_chars + jint(bj, bn, cat + 8) 217 nchap = nchap + 1 218 pcur = cat + 8 219 } } 220 } 221 } 222 var total_min: i64 = (total_chars + K_MAGIC_1099) / K_MAGIC_1100 223 if total_min < 1 { total_min = 1 } 224 225 // output 226 let outp: *u8 = sys_mmap(K_MAGIC_1024) 227 var ol: i64 = 0 228 let op: *u8 = "knowledge/staging/media/reader_static_" as *u8; z=0; while op[z]!=(0 as u8){ outp[ol]=op[z]; ol=ol+1; z=z+1 } 229 z=0; while slug[z]!=(0 as u8){ outp[ol]=slug[z]; ol=ol+1; z=z+1 } 230 let oe: *u8 = ".html" as *u8; z=0; while oe[z]!=(0 as u8){ outp[ol]=oe[z]; ol=ol+1; z=z+1 } outp[ol]=0 as u8 231 let fd: i64 = sys_openat_wr(outp, 0x1a4) 232 if fd < 0 { p("READER-RENDER FAIL open out\n" as *u8); sys_exit(1); return 1 } 233 234 // ===== HEAD + CSS (themes/font via radio :checked, NO JS) ===== 235 w(fd, "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\n" as *u8) 236 w(fd, "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n" as *u8) 237 w(fd, "<title>" as *u8); wesc_z(fd, title); w(fd, "</title>\n<style>\n" as *u8) 238 w(fd, "*{box-sizing:border-box} html,body{margin:0}\n" as *u8) 239 w(fd, ".thr{position:absolute;width:0;height:0;opacity:0;pointer-events:none}\n" as *u8) 240 w(fd, ".app{--bg:#fbfbf9;--fg:#1d1d1b;--muted:#6b6b66;--rule:#e3e3dd;--sheet:#fff;--accent:#7a5cff;--fs:19px;--ff:Georgia,'Iowan Old Style','Times New Roman',serif;--ls:normal;--ws:normal;--lh:1.62;background:var(--bg);color:var(--fg);min-height:100vh;font-family:var(--ff);font-size:var(--fs);line-height:var(--lh);transition:background .2s,color .2s}\n" as *u8) 241 w(fd, "#th-sepia:checked~.app{--bg:#f4ecd8;--fg:#43391f;--muted:#8a7a55;--rule:#e0d4b6;--sheet:#fbf4e3}\n" as *u8) 242 w(fd, "#th-dark:checked~.app{--bg:#15161a;--fg:#d8d8d2;--muted:#8d8d88;--rule:#2a2c33;--sheet:#1e2027;--accent:#9b86ff}\n" as *u8) 243 w(fd, "#th-night:checked~.app{--bg:#1a1206;--fg:#e8c890;--muted:#9a7c50;--rule:#3a2c14;--sheet:#211608;--accent:#ffb24d}\n" as *u8) 244 w(fd, "#th-black:checked~.app{--bg:#000;--fg:#c8b48c;--muted:#7a6f55;--rule:#171712;--sheet:#0a0a07;--accent:#e0a050}\n" as *u8) 245 w(fd, "#fs-s:checked~.app{--fs:16px} #fs-l:checked~.app{--fs:22px}\n" as *u8) 246 w(fd, "#fn-dys:checked~.app{--ff:'Atkinson Hyperlegible',Verdana,Tahoma,'Trebuchet MS','Comic Sans MS',sans-serif;--ls:.045em;--ws:.16em;--lh:1.9}\n" as *u8) // SC39 a11y: dyslexia-friendly reading mode (BDA style guide: sans-serif + generous letter/word spacing + line-height + no-justify/hyphen), zero-JS via radio:checked 247 w(fd, ".bar{position:sticky;top:0;z-index:5;display:flex;gap:.5rem;align-items:center;padding:.5rem .8rem;background:var(--sheet);border-bottom:1px solid var(--rule);font-family:system-ui,-apple-system,sans-serif}\n" as *u8) 248 w(fd, ".bar .ttl{font-weight:600;font-size:14px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n" as *u8) 249 w(fd, ".tbar label{cursor:pointer;border:1px solid var(--rule);border-radius:8px;padding:.25rem .5rem;font-size:13px;color:var(--fg)}\n" as *u8) 250 w(fd, ".toc>summary{cursor:pointer;list-style:none;border:1px solid var(--rule);border-radius:8px;padding:.3rem .6rem;font-size:14px}\n" as *u8) 251 w(fd, ".toc[open]>nav{position:absolute;margin-top:.4rem;background:var(--sheet);border:1px solid var(--rule);border-radius:10px;padding:.3rem;max-height:72vh;overflow:auto;max-width:82vw;box-shadow:0 10px 30px rgba(0,0,0,.25);z-index:9}\n" as *u8) 252 w(fd, ".toc nav a{display:block;color:var(--fg);text-decoration:none;padding:.45rem .6rem;border-bottom:1px solid var(--rule);font-size:.95rem;white-space:nowrap}\n" as *u8) 253 w(fd, ".toc nav a.d1{padding-left:1.8rem} .toc nav a.d2{padding-left:3rem} .toc nav a.d3{padding-left:4.2rem}\n" as *u8) 254 w(fd, ".xray>summary{cursor:pointer;list-style:none;border:1px solid var(--rule);border-radius:8px;padding:.3rem .6rem;font-size:14px}\n" as *u8) 255 w(fd, ".xray[open]>nav{position:absolute;margin-top:.4rem;background:var(--sheet);border:1px solid var(--rule);border-radius:10px;padding:.5rem;max-height:72vh;overflow:auto;max-width:82vw;box-shadow:0 10px 30px rgba(0,0,0,.25);z-index:9}\n" as *u8) 256 w(fd, ".xray nav a{display:flex;justify-content:space-between;gap:1rem;color:var(--fg);text-decoration:none;padding:.45rem .6rem;border-bottom:1px solid var(--rule);font-size:.95rem;white-space:nowrap}\n" as *u8) 257 w(fd, ".xray nav a span{color:var(--muted);font-variant-numeric:tabular-nums}\n" as *u8) 258 w(fd, ".xr-cap{color:var(--muted);font-size:.8rem;margin:.1rem .3rem .5rem;font-family:system-ui,sans-serif;white-space:normal}\n" as *u8) 259 w(fd, ".xray .xr-dist{display:flex;flex-wrap:wrap;gap:.3rem;margin:.1rem 0 .55rem;padding:0 .2rem}\n" as *u8) 260 w(fd, ".xray .xr-dist a{display:inline-block;color:var(--muted);text-decoration:none;font-size:.78rem;border:1px solid var(--rule);border-radius:6px;padding:.05rem .4rem;font-variant-numeric:tabular-nums;white-space:nowrap}\n" as *u8) 261 w(fd, ".fnref{font-size:.7em;line-height:0;vertical-align:super} .fnref a{color:var(--accent);text-decoration:none;padding:0 .12em}\n" as *u8) 262 w(fd, ".footnotes{margin:2.4rem 0 0;padding:1rem 0 0;border-top:1px solid var(--rule);font-size:.86rem;color:var(--muted);padding-left:1.6rem}\n" as *u8) 263 w(fd, ".footnotes li{margin:0 0 .5rem} .footnotes li:target{background:rgba(122,92,255,.14);border-radius:5px;padding:.2rem .4rem;margin-left:-.4rem}\n" as *u8) // zero-JS: clicking a ref highlights its note via :target 264 w(fd, ".fnback{color:var(--accent);text-decoration:none;margin-left:.35rem}\n" as *u8) 265 w(fd, "@media(pointer:coarse){.xray>summary,.xray nav a{min-height:44px;align-items:center}}\n" as *u8) 266 w(fd, "main{max-width:34rem;margin:0 auto;padding:1.6rem 1.25rem 5rem}\n" as *u8) 267 w(fd, "main h1{font-size:1.5rem;margin:.2rem 0} .author{color:var(--muted);font-style:italic;margin:0 0 .2rem}\n" as *u8) 268 w(fd, ".meta{color:var(--muted);font-size:.85rem;margin:0 0 2rem;font-family:system-ui,sans-serif}\n" as *u8) 269 w(fd, "main h2{font-size:1.05rem;color:var(--muted);font-family:system-ui,sans-serif;margin:2.6rem 0 1rem;border-top:1px solid var(--rule);padding-top:1rem}\n" as *u8) 270 w(fd, "main p{margin:0 0 1.05em;hyphens:auto;-webkit-hyphens:auto;overflow-wrap:break-word;letter-spacing:var(--ls);word-spacing:var(--ws)}\n" as *u8) 271 w(fd, "#fn-dys:checked~.app main p{hyphens:none;-webkit-hyphens:none;text-align:left}\n" as *u8) // dyslexia: no hyphenation/justification (BDA) 272 w(fd, "main img{max-width:100%;height:auto;display:block;margin:1.2em auto;border-radius:4px}\n" as *u8) 273 w(fd, ".cover{max-width:62%;margin:1rem auto 2.2rem;box-shadow:0 10px 30px rgba(0,0,0,.3)}\n" as *u8) 274 w(fd, "main section>p:first-of-type::first-letter{float:left;font-size:3.1rem;line-height:.78;padding:.06rem .55rem 0 0;font-weight:700;color:var(--accent)}\n" as *u8) 275 w(fd, "@media print{.bar,.skip,.thr{display:none!important}.app{background:#fff;color:#000}main{max-width:none;padding:0}main section{break-before:page}.cover{max-width:48%}main a{color:#000;text-decoration:none}}\n" as *u8) 276 w(fd, "@media(min-width:900px){main{max-width:42rem}}\n" as *u8) 277 w(fd, ".skip{position:absolute;left:-999px;top:0;background:#7a5cff;color:#fff;padding:.5rem .8rem;border-radius:0 0 8px 0;z-index:99} .skip:focus{left:0}\n" as *u8) // a11y: skip-to-content 278 w(fd, "@media(pointer:coarse){.tbar label,.toc>summary,.toc nav a{min-height:44px;display:flex;align-items:center}}\n" as *u8) // S-class a11y: >=44px touch targets on touch devices (Apple HIG / Material) 279 w(fd, ":focus-visible{outline:3px solid #7a5cff;outline-offset:2px;border-radius:3px}\n" as *u8) // WCAG 2.4.7: visible keyboard focus on every control 280 w(fd, "@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;scroll-behavior:auto!important}}\n" as *u8) // WCAG 2.3.3: respect the OS reduced-motion preference 281 w(fd, "</style></head>\n<body>\n" as *u8) 282 w(fd, "<a class=\"skip\" href=\"#main\">Skip to content</a>\n" as *u8) 283 284 // hidden radios (theme + font), BEFORE .app so `~ .app` applies 285 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-light\" checked>\n" as *u8) 286 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-sepia\">\n" as *u8) 287 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-dark\">\n" as *u8) 288 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-night\">\n" as *u8) 289 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-black\">\n" as *u8) 290 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxfs\" id=\"fs-s\">\n" as *u8) 291 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxfs\" id=\"fs-m\" checked>\n" as *u8) 292 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxfs\" id=\"fs-l\">\n" as *u8) 293 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxfont\" id=\"fn-normal\" checked>\n" as *u8) 294 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxfont\" id=\"fn-dys\">\n" as *u8) 295 296 w(fd, "<div class=\"app\">\n<header class=\"bar\">\n" as *u8) 297 w(fd, "<details class=\"toc\"><summary>\xe2\x98\xb0 Contents</summary><nav aria-label=\"Table of contents\">\n" as *u8) 298 // ===== TOC (parse toc[] -> anchors) ===== 299 var tstart: i64 = jfrom(bj, bn, 0, "\"toc\":[" as *u8) 300 var ntoc: i64 = 0 301 if tstart >= 0 { 302 var tend: i64 = jfrom(bj, bn, tstart, "]" as *u8) 303 if tend < 0 { tend = bn } 304 let lab: *u8 = sys_mmap(K_MAGIC_1024) 305 var cur: i64 = tstart 306 var go: i64 = 1 307 while go == 1 { 308 let d_at: i64 = jfrom(bj, bn, cur, "\"depth\":" as *u8) 309 if d_at < 0 { go = 0 } else { if d_at > tend { go = 0 } else { 310 let depth: i64 = jint(bj, bn, d_at + 8) 311 let t_at: i64 = jfrom(bj, bn, d_at, "\"title\":\"" as *u8) 312 jstr(bj, bn, t_at + 8, lab, K_MAGIC_1024) 313 let i_at: i64 = jfrom(bj, bn, t_at, "\"idx\":" as *u8) 314 let idx: i64 = jint(bj, bn, i_at + 6) 315 w(fd, "<a class=\"d" as *u8); wn(fd, depth); w(fd, "\" href=\"#c" as *u8); wn(fd, idx); w(fd, "\">" as *u8) 316 wesc_z(fd, lab); w(fd, "</a>\n" as *u8) 317 ntoc = ntoc + 1 318 cur = i_at + 6 319 } } 320 } 321 } 322 w(fd, "</nav></details>\n" as *u8) 323 // ===== X-Ray panel (zero-JS; rendered only when nx_reader_xray produced an index) ===== 324 if (xj as i64) != 0 { if xn > 0 { 325 let xc_at: i64 = jfrom(xj, xn, 0, "\"nterms\":" as *u8) 326 var xcount: i64 = 0 327 if xc_at >= 0 { xcount = jint(xj, xn, xc_at + 9) } 328 let xterms: i64 = jfrom(xj, xn, 0, "\"terms\":[" as *u8) 329 if xcount > 0 { if xterms >= 0 { 330 w(fd, "<details class=\"xray\"><summary>\xe2\x9c\xa6 X-Ray</summary><nav aria-label=\"X-Ray: notable people and terms\">\n" as *u8) 331 w(fd, "<p class=\"xr-cap\">Auto-derived from this book \xc2\xb7 " as *u8); wn(fd, xcount); w(fd, " notable terms</p>\n" as *u8) 332 let xnm: *u8 = sys_mmap(256) 333 var xcur: i64 = xterms 334 var xgo: i64 = 1 335 while xgo == 1 { 336 let n_at: i64 = jfrom(xj, xn, xcur, "\"name\":\"" as *u8) 337 if n_at < 0 { xgo = 0 } else { 338 jstr(xj, xn, n_at + 7, xnm, 256) 339 let c_at: i64 = jfrom(xj, xn, n_at, "\"count\":" as *u8) 340 let cval: i64 = jint(xj, xn, c_at + 8) 341 let f_at: i64 = jfrom(xj, xn, n_at, "\"first\":" as *u8) 342 let fval: i64 = jint(xj, xn, f_at + 8) 343 w(fd, "<a href=\"#c" as *u8); wn(fd, fval); w(fd, "\"><b>" as *u8); wesc_z(fd, xnm); w(fd, "</b> <span>" as *u8); wn(fd, cval); w(fd, "\xc3\x97</span></a>\n" as *u8) 344 // per-chapter concordance chips (jump to each chapter the term appears in): "chap#\xc2\xb7count" 345 let by_at: i64 = jfrom(xj, xn, n_at, "\"by\":[" as *u8) 346 if by_at >= 0 { 347 var by_e: i64 = jfrom(xj, xn, by_at + 6, "]" as *u8) 348 if by_e < 0 { by_e = xn } 349 w(fd, "<p class=\"xr-dist\">" as *u8) 350 let pp: *i64 = sys_mmap(8) as *i64; pp[0] = by_at + 6 351 var dgo: i64 = 1 352 while dgo == 1 { 353 if pp[0] >= by_e { dgo = 0 } else { 354 let dc: i64 = jnext(xj, xn, pp) 355 if pp[0] > by_e { dgo = 0 } else { 356 let dn: i64 = jnext(xj, xn, pp) 357 w(fd, "<a href=\"#c" as *u8); wn(fd, dc); w(fd, "\">" as *u8); wn(fd, dc+1); w(fd, "\xc2\xb7" as *u8); wn(fd, dn); w(fd, "</a>" as *u8) 358 } 359 } 360 } 361 w(fd, "</p>\n" as *u8) 362 } 363 xcur = f_at + 8 364 } 365 } 366 w(fd, "</nav></details>\n" as *u8) 367 } } 368 } } 369 w(fd, "<span class=\"ttl\">" as *u8); wesc_z(fd, title); w(fd, "</span>\n" as *u8) 370 w(fd, "<span class=\"tbar\" role=\"group\" aria-label=\"Display settings\"><label for=\"th-light\">Light</label><label for=\"th-sepia\">Sepia</label><label for=\"th-dark\">Dark</label><label for=\"th-night\">Night</label><label for=\"th-black\">Black</label> <label for=\"fs-s\">A-</label><label for=\"fs-l\">A+</label> <label for=\"fn-normal\">Serif</label><label for=\"fn-dys\">Dyslexia</label></span>\n" as *u8) 371 w(fd, "</header>\n<main id=\"main\">\n" as *u8) 372 373 if cover[0] != (0 as u8) { 374 w(fd, "<img class=\"cover\" src=\"reader/" as *u8); w(fd, slug); w(fd, "/" as *u8); w(fd, cover); w(fd, "\" alt=\"Book cover\">\n" as *u8) 375 } 376 w(fd, "<h1>" as *u8); wesc_z(fd, title); w(fd, "</h1>\n" as *u8) 377 if author[0] != (0 as u8) { w(fd, "<p class=\"author\">" as *u8); wesc_z(fd, author); w(fd, "</p>\n" as *u8) } 378 // publisher / date (from EXTH 101/106 -- "all the book info", extracted offline from the file) 379 if publisher[0] != (0 as u8) { 380 w(fd, "<p class=\"meta\">" as *u8); wesc_z(fd, publisher) 381 if pubdate[0] != (0 as u8) { w(fd, " \xc2\xb7 " as *u8); wesc_z(fd, pubdate) } 382 w(fd, "</p>\n" as *u8) 383 } else { if pubdate[0] != (0 as u8) { w(fd, "<p class=\"meta\">" as *u8); wesc_z(fd, pubdate); w(fd, "</p>\n" as *u8) } } 384 w(fd, "<p class=\"meta\">~" as *u8); wn(fd, total_min); w(fd, " min read \xc2\xb7 " as *u8); wn(fd, nchap); w(fd, " chapters</p>\n" as *u8) 385 // "About this book" from the publisher's description (EXTH 103) -- zero-JS native <details> 386 if descr[0] != (0 as u8) { 387 w(fd, "<details class=\"about\"><summary>About this book</summary><p>" as *u8); wesc_z(fd, descr); w(fd, "</p></details>\n" as *u8) 388 } 389 390 // ===== chapters[] -> sections (read each chap file, render) ===== 391 let cbuf: *u8 = sys_mmap(CAP) 392 let cpath: *u8 = sys_mmap(K_MAGIC_1024) 393 let lab2: *u8 = sys_mmap(K_MAGIC_1024) 394 let file: *u8 = sys_mmap(256) 395 var cstart: i64 = jfrom(bj, bn, 0, "\"chapters\":[" as *u8) 396 var nsec: i64 = 0 397 if cstart >= 0 { 398 var cend: i64 = jfrom(bj, bn, cstart, "],\"nchapters\":" as *u8) // end of chapters[] (NOT first "]", which now matches a nested fn:[]) 399 if cend < 0 { cend = bn } 400 var cur: i64 = cstart + 11 401 var go: i64 = 1 402 while go == 1 { 403 let i_at: i64 = jfrom(bj, bn, cur, "\"idx\":" as *u8) 404 if i_at < 0 { go = 0 } else { if i_at > cend { go = 0 } else { 405 let idx: i64 = jint(bj, bn, i_at + 6) 406 let t_at: i64 = jfrom(bj, bn, i_at, "\"title\":\"" as *u8) 407 jstr(bj, bn, t_at + 8, lab2, K_MAGIC_1024) 408 let f_at: i64 = jfrom(bj, bn, t_at, "\"file\":\"" as *u8) 409 jstr(bj, bn, f_at + 7, file, 256) 410 // read chap file 411 var pl: i64 = 0; while dir[pl]!=(0 as u8){ cpath[pl]=dir[pl]; pl=pl+1 } cpath[pl]=0x2f as u8; pl=pl+1 412 var fz: i64 = 0; while file[fz]!=(0 as u8){ cpath[pl]=file[fz]; pl=pl+1; fz=fz+1 } cpath[pl]=0 as u8 413 let clp: *i64 = sys_mmap(8) as *i64; clp[0]=0 414 let ct: *u8 = sys_read_file(cpath, clp) 415 w(fd, "<section id=\"c" as *u8); wn(fd, idx); w(fd, "\">\n<h2>" as *u8); wesc_z(fd, lab2); w(fd, "</h2>\n" as *u8) 416 if (ct as i64) != 0 { render_text(fd, ct, clp[0], slug, lab2) } 417 // ===== footnotes for this chapter (zero-JS): chapters[idx].fn[] -> <ol> with :target highlight + back-links ===== 418 let fn_at: i64 = jfrom(bj, bn, i_at, "\"fn\":[" as *u8) 419 var fn_end: i64 = jfrom(bj, bn, i_at + 6, "\"idx\":" as *u8) 420 if fn_end < 0 { fn_end = bn } 421 if fn_at >= 0 { if fn_at < fn_end { 422 let fidb: *u8 = sys_mmap(256) 423 let ftxb: *u8 = sys_mmap(K_MAGIC_8192) 424 var fcur: i64 = fn_at + 6 425 var anyfn: i64 = 0 426 var fgo: i64 = 1 427 while fgo == 1 { 428 let idat: i64 = jfrom(bj, bn, fcur, "\"id\":\"" as *u8) 429 if idat < 0 { fgo = 0 } else { if idat >= fn_end { fgo = 0 } else { 430 jstr(bj, bn, idat + 5, fidb, 256) 431 let txat: i64 = jfrom(bj, bn, idat, "\"text\":\"" as *u8) 432 jstr(bj, bn, txat + 7, ftxb, K_MAGIC_8192) 433 if anyfn == 0 { w(fd, "<ol class=\"footnotes\" aria-label=\"Footnotes\">\n" as *u8); anyfn = 1 } 434 w(fd, "<li id=\"" as *u8); wesc_z(fd, fidb); w(fd, "\">" as *u8); wesc_z(fd, ftxb) 435 w(fd, " <a class=\"fnback\" href=\"#ref-" as *u8); wesc_z(fd, fidb); w(fd, "\" aria-label=\"Back to text\">\xe2\x86\xa9</a></li>\n" as *u8) 436 fcur = txat + 7 437 } } 438 } 439 if anyfn == 1 { w(fd, "</ol>\n" as *u8) } 440 } } 441 w(fd, "</section>\n" as *u8) 442 nsec = nsec + 1 443 cur = f_at + 7 444 } } 445 } 446 } 447 448 w(fd, "</main>\n</div>\n</body></html>\n" as *u8) 449 sys_close(fd) 450 451 p("READER-RENDER ok slug=" as *u8); p(slug); p(" sections=" as *u8); wn(1, nsec); p(" toc=" as *u8); wn(1, ntoc) 452 p(" cover=" as *u8); if cover[0]!=(0 as u8) { p(cover) } else { p("(none)" as *u8) } 453 p(" -> " as *u8); p(outp); p(" [ZERO JS]\n" as *u8) 454 if nsec <= 0 { sys_exit(1); return 1 } 455 sys_exit(0); return 0 456}