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, "&" as *u8) }
26 else { if c == 0x3c { w(fd, "<" as *u8) }
27 else { if c == 0x3e { w(fd, ">" 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, "&" as *u8) }
120 else { if c == 0x3c { w(fd, "<" as *u8) }
121 else { if c == 0x3e { w(fd, ">" 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}