code wiki / _hdl_build / nx_epub_book.nx
nx_epub_book.nx source
↩ module page · 869 lines · 42771 B
1// nx_epub_book.nx -- SOVEREIGN FULL-BOOK EPUB extraction (Reader-arc RUNG 2b, deliverable 1).
2//
3// Builds on nx_epub_read.nx (RUNG 2, GREEN over real Calibre epubs). Same sovereign parse path:
4// ZIP central-directory walker + nx_deflate inflate + container.xml -> OPF -> spine + nx_html_to_text.
5// NEW logic only (everything else reused verbatim):
6// (a) walk ALL spine items in reading order (not the first-real-chapter pick),
7// (b) decode_numeric_refs: a sovereign post-pass that rewrites &#DDDD; / &#xHHHH; numeric character
8// references to UTF-8 bytes (nx_html_to_text leaves these RAW -- the rung-2 output showed literal
9// ’ / “ / —; fixed here so curly quotes / em-dashes render natively),
10// (c) TOC/nav skip: flag the nav/TOC doc so the reader opens at real prose, but STILL include it in
11// chapters[] in reading order (operator requirement: skip-as-opening only, include all),
12// (d) emit knowledge/staging/media/reader/<slug>/book.json {title,author,format,nchapters,
13// chapters:[{idx,title,file,chars,is_nav}]} + one chap<K>.txt per chapter. ADDITIVE (rule 13).
14//
15// SOVEREIGN: pure Nishi organs + raw syscalls. NO python/unzip/3rd-party-xml/gcc/sh, NO calibre binaries.
16// NO-FAKE-GREEN: a REAL Calibre epub; title/author/spine/text MEASURED from bytes; after decode, NO chapter
17// may contain "&#" (has_raw_entities asserted 0). license_tier: ORIGINAL
18import "nx_syscalls.nx"
19import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
20import "nx_deflate.nx"
21import "nx_html_to_text.nx"
22import "nx_epub_nav.nx"
23import "nx_charset.nx" // MOJIBAKE FIX: nx_charset_repair_utf8 -- stray Windows-1252 bytes -> clean UTF-8
24const CHAP_MAGIC_1252: i64 = 1252
25const CHAP_MAGIC_1200: i64 = 1200
26const CHAP_MAGIC_1024: i64 = 1024
27const CHAP_MAGIC_262144: i64 = 262144
28const CHAP_MAGIC_2097152: i64 = 2097152
29const CHAP_MAGIC_8192: i64 = 8192
30const CHAP_MAGIC_65536: i64 = 65536
31const CHAP_MAGIC_16384: i64 = 16384
32const CHAP_MAGIC_2048: i64 = 2048
33const CHAP_MAGIC_32768: i64 = 32768
34const CHAP_MAGIC_5381: i64 = 5381
35const CHAP_MAGIC_2000: i64 = 2000
36
37// ---- io + string helpers (reused verbatim from nx_epub_read.nx) ----
38func er_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
39func er_w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, er_slen(s)); return 0 }
40// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
41// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
42// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
43// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
44func er_n(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
45func er_p(s: *u8) -> i64 { er_w(1, s); return 0 }
46func er_pn(v: i64) -> i64 { er_n(1, v); return 0 }
47
48func le16(b: *u8, o: i64) -> i64 { return (b[o] as i64) | ((b[o+1] as i64) << 8) }
49func le32(b: *u8, o: i64) -> i64 { return (b[o] as i64) | ((b[o+1] as i64) << 8) | ((b[o+2] as i64) << 16) | ((b[o+3] as i64) << 24) }
50
51func er_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[off+i]=s[i];i=i+1} return off+i }
52func er_catn(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var i: i64=0; while i<k{dst[off+i]=t[k-1-i];i=i+1} return off+k }
53
54// substring search; return index in hay[0..hl] or -1
55func find_sub(hay: *u8, hl: i64, needle: *u8) -> i64 {
56 let nl: i64 = er_slen(needle)
57 if nl == 0 { return 0-1 }
58 var i: i64 = 0
59 while i + nl <= hl {
60 var k: i64 = 0
61 var hit: i64 = 1
62 while k < nl { if hay[i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } }
63 if hit == 1 { return i }
64 i = i + 1
65 }
66 return 0-1
67}
68
69// count non-overlapping occurrences of needle in hay[0..hl]
70func count_sub(hay: *u8, hl: i64, needle: *u8) -> i64 {
71 let nl: i64 = er_slen(needle)
72 if nl == 0 { return 0 }
73 var c: i64 = 0
74 var i: i64 = 0
75 while i + nl <= hl {
76 var k: i64 = 0
77 var hit: i64 = 1
78 while k < nl { if hay[i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } }
79 if hit == 1 { c = c + 1; i = i + nl } else { i = i + 1 }
80 }
81 return c
82}
83
84// extract attribute value: find attr (e.g. `full-path="`) then copy until the next '"'
85func attr_val(hay: *u8, hl: i64, attr: *u8, out: *u8, outcap: i64) -> i64 {
86 let at: i64 = find_sub(hay, hl, attr)
87 if at < 0 { return 0-1 }
88 var s: i64 = at + er_slen(attr)
89 var o: i64 = 0
90 while s < hl { if hay[s] == (0x22 as u8) { break } if o < outcap-1 { out[o] = hay[s]; o = o + 1 } s = s + 1 }
91 out[o] = 0 as u8
92 return o
93}
94
95// extract element text: find opentag (e.g. `<dc:title`), skip to '>', copy until next '<'
96func elem_text(hay: *u8, hl: i64, opentag: *u8, out: *u8, outcap: i64) -> i64 {
97 let at: i64 = find_sub(hay, hl, opentag)
98 if at < 0 { return 0-1 }
99 var s: i64 = at
100 while s < hl { if hay[s] == (0x3e as u8) { s = s + 1; break } s = s + 1 }
101 var o: i64 = 0
102 while s < hl { if hay[s] == (0x3c as u8) { break } if o < outcap-1 { out[o] = hay[s]; o = o + 1 } s = s + 1 }
103 out[o] = 0 as u8
104 return o
105}
106
107// ---- ZIP central-directory walker (name-based extract) ----
108func find_eocd(z: *u8, zl: i64) -> i64 {
109 var i: i64 = zl - 22
110 if i < 0 { return 0-1 }
111 while i >= 0 {
112 if z[i] == (0x50 as u8) { if z[i+1] == (0x4b as u8) { if z[i+2] == (0x05 as u8) { if z[i+3] == (0x06 as u8) { return i } } } }
113 i = i - 1
114 }
115 return 0-1
116}
117
118func zip_find(z: *u8, zl: i64, target: *u8, ob: *i64) -> i64 {
119 let eocd: i64 = find_eocd(z, zl)
120 if eocd < 0 { return 0 }
121 let count: i64 = le16(z, eocd + 10)
122 var cd: i64 = le32(z, eocd + 16)
123 let tlen: i64 = er_slen(target)
124 var k: i64 = 0
125 while k < count {
126 if cd + 46 > zl { return 0 }
127 if z[cd] != (0x50 as u8) { return 0 }
128 let method: i64 = le16(z, cd + 10)
129 let comp: i64 = le32(z, cd + 20)
130 let uncomp: i64 = le32(z, cd + 24)
131 let fnlen: i64 = le16(z, cd + 28)
132 let extralen: i64 = le16(z, cd + 30)
133 let commentlen: i64 = le16(z, cd + 32)
134 let lho: i64 = le32(z, cd + 42)
135 var mt: i64 = 0
136 if fnlen == tlen {
137 mt = 1
138 var i: i64 = 0
139 while i < fnlen { if z[cd + 46 + i] != target[i] { mt = 0; i = fnlen } else { i = i + 1 } }
140 }
141 if mt == 1 {
142 let lfn: i64 = le16(z, lho + 26)
143 let lex: i64 = le16(z, lho + 28)
144 ob[0] = method; ob[1] = comp; ob[2] = uncomp; ob[3] = lho + 30 + lfn + lex
145 return 1
146 }
147 cd = cd + 46 + fnlen + extralen + commentlen
148 k = k + 1
149 }
150 return 0
151}
152
153func zip_read_named(z: *u8, zl: i64, target: *u8, out: *u8, outcap: i64) -> i64 {
154 let ob: *i64 = sys_mmap(64) as *i64
155 if zip_find(z, zl, target, ob) == 0 { return 0-1 }
156 let method: i64 = ob[0]
157 let comp: i64 = ob[1]
158 let dataoff: i64 = ob[3]
159 if method == 0 {
160 var i: i64 = 0
161 while i < comp { if i < outcap { out[i] = z[dataoff + i] } i = i + 1 }
162 return comp
163 }
164 if method == 8 {
165 let r: *NxDeflateResult = nx_deflate_inflate((z as i64 + dataoff) as *u8, comp, outcap)
166 if (r as i64) == 0 { return 0-1 }
167 let n: i64 = r.output_size
168 var i: i64 = 0
169 while i < n { if i < outcap { out[i] = r.output_data[i] } i = i + 1 }
170 return n
171 }
172 return 0-2
173}
174
175func href_for_id(opf: *u8, ol: i64, idref: *u8, out: *u8, outcap: i64) -> i64 {
176 let needle: *u8 = sys_mmap(512)
177 var w: i64 = er_cat(needle, 0, "id=\"" as *u8)
178 w = er_cat(needle, w, idref)
179 needle[w] = 0x22 as u8; w = w + 1
180 needle[w] = 0 as u8
181 let at: i64 = find_sub(opf, ol, needle)
182 if at < 0 { return 0-1 }
183 var ts: i64 = at
184 while ts > 0 { if opf[ts] == (0x3c as u8) { break } ts = ts - 1 }
185 var te: i64 = at
186 while te < ol { if opf[te] == (0x3e as u8) { break } te = te + 1 }
187 return attr_val((opf as i64 + ts) as *u8, te - ts, "href=\"" as *u8, out, outcap)
188}
189
190func join_path(opfpath: *u8, href: *u8, out: *u8) -> i64 {
191 let ol: i64 = er_slen(opfpath)
192 var lastslash: i64 = 0-1
193 var i: i64 = 0
194 while i < ol { if opfpath[i] == (0x2f as u8) { lastslash = i } i = i + 1 }
195 var o: i64 = 0
196 if lastslash >= 0 { var k: i64 = 0; while k <= lastslash { out[o] = opfpath[k]; o = o + 1; k = k + 1 } }
197 var j: i64 = 0
198 while href[j] != (0 as u8) { out[o] = href[j]; o = o + 1; j = j + 1 }
199 out[o] = 0 as u8
200 return o
201}
202
203// collect spine idref strings (in reading order) into arena (NUL-separated), offs[]=starts; ret count
204func get_spine_idrefs(opf: *u8, ol: i64, arena: *u8, offs: *i64, cap: i64) -> i64 {
205 let spos: i64 = find_sub(opf, ol, "<spine" as *u8)
206 if spos < 0 { return 0 }
207 var spe: i64 = find_sub((opf as i64 + spos) as *u8, ol - spos, "</spine" as *u8)
208 if spe < 0 { spe = ol - spos }
209 spe = spos + spe
210 var p: i64 = spos
211 var n: i64 = 0
212 var aoff: i64 = 0
213 while p < spe {
214 if n >= cap { break }
215 let rem: i64 = spe - p
216 let at: i64 = find_sub((opf as i64 + p) as *u8, rem, "idref=\"" as *u8)
217 if at < 0 { break }
218 var s: i64 = p + at + 7
219 offs[n] = aoff
220 while s < spe { if opf[s] == (0x22 as u8) { break } arena[aoff] = opf[s]; aoff = aoff + 1; s = s + 1 }
221 arena[aoff] = 0 as u8; aoff = aoff + 1
222 n = n + 1
223 p = s + 1
224 }
225 return n
226}
227
228// JSON string emit. MOJIBAKE FIX: every metadata string (title/author/toc-label/footnote/href) is first
229// repaired to clean UTF-8, so a stray Windows-1252 byte in a title renders correctly instead of U+FFFD.
230// Repair is idempotent on already-valid UTF-8, and emits only valid UTF-8 (never an unescaped " or control).
231func er_wjson(fd: i64, s: *u8) -> i64 {
232 var sl: i64 = 0; while s[sl] != (0 as u8) { sl = sl + 1 }
233 let rcap: i64 = sl * 3 + 16
234 let rep: *u8 = sys_mmap(rcap)
235 let rn: i64 = nx_charset_repair_utf8(s, sl, rep, rcap)
236 var i: i64 = 0
237 while i < rn {
238 let c: i64 = rep[i] as i64
239 if c == 0x22 { sys_write(fd, "\\\"" as *u8, 2) }
240 else { if c == 0x5c { sys_write(fd, "\\\\" as *u8, 2) }
241 else { if c < 0x20 { sys_write(fd, " " as *u8, 1) }
242 else { sys_write(fd, (rep as i64 + i) as *u8, 1) } } }
243 i = i + 1
244 }
245 sys_munmap(rep, rcap)
246 return 0
247}
248
249// ===== NEW (b): NUMERIC CHARACTER REFERENCE DECODE =================
250// UTF-8 encode codepoint cp into out[o..]; return new offset.
251func utf8_put(out: *u8, o: i64, cp: i64) -> i64 {
252 if cp < 0x80 {
253 out[o] = cp as u8; return o + 1
254 }
255 if cp < 0x800 {
256 out[o] = (0xC0 | (cp >> 6)) as u8
257 out[o+1] = (0x80 | (cp & 0x3F)) as u8
258 return o + 2
259 }
260 if cp < 0x10000 {
261 out[o] = (0xE0 | (cp >> 12)) as u8
262 out[o+1] = (0x80 | ((cp >> 6) & 0x3F)) as u8
263 out[o+2] = (0x80 | (cp & 0x3F)) as u8
264 return o + 3
265 }
266 out[o] = (0xF0 | (cp >> 18)) as u8
267 out[o+1] = (0x80 | ((cp >> 12) & 0x3F)) as u8
268 out[o+2] = (0x80 | ((cp >> 6) & 0x3F)) as u8
269 out[o+3] = (0x80 | (cp & 0x3F)) as u8
270 return o + 4
271}
272
273// Scan txt[0..n], rewrite &#DDDD; / &#xHHHH; -> UTF-8 into out (cap). Returns out length.
274// Defensive: a malformed ref ('&' not followed by a valid '#...;') is copied literally, advance 1.
275func decode_numeric_refs(txt: *u8, n: i64, out: *u8, outcap: i64) -> i64 {
276 var i: i64 = 0
277 var o: i64 = 0
278 while i < n {
279 let c: i64 = txt[i] as i64
280 var done: i64 = 0
281 if c == 0x26 { // '&'
282 if i + 1 < n {
283 if (txt[i+1] as i64) == 0x23 { // '#'
284 var j: i64 = i + 2
285 var hex: i64 = 0
286 if j < n {
287 let xc: i64 = txt[j] as i64
288 if xc == 0x78 { hex = 1; j = j + 1 } // 'x'
289 else { if xc == 0x58 { hex = 1; j = j + 1 } } // 'X'
290 }
291 var cp: i64 = 0
292 var ndig: i64 = 0
293 var scan: i64 = 1
294 while scan == 1 {
295 if j >= n { scan = 0 }
296 else {
297 let dc: i64 = txt[j] as i64
298 var dv: i64 = 0-1
299 if hex == 1 {
300 if dc >= 48 { if dc <= 57 { dv = dc - 48 } }
301 if dc >= 97 { if dc <= 102 { dv = dc - 87 } }
302 if dc >= 65 { if dc <= 70 { dv = dc - 55 } }
303 } else {
304 if dc >= 48 { if dc <= 57 { dv = dc - 48 } }
305 }
306 if dv < 0 { scan = 0 }
307 else {
308 if hex == 1 { cp = cp * 16 + dv } else { cp = cp * 10 + dv }
309 ndig = ndig + 1
310 j = j + 1
311 }
312 }
313 }
314 // valid ref iff digit(s) present AND terminated by ';'
315 if ndig > 0 { if j < n { if (txt[j] as i64) == 0x3b {
316 if cp > 0 { if cp <= 0x10FFFF {
317 if o + 4 < outcap { o = utf8_put(out, o, cp) }
318 i = j + 1
319 done = 1
320 } }
321 } } }
322 }
323 }
324 }
325 if done == 0 {
326 if o < outcap - 1 { out[o] = txt[i]; o = o + 1 }
327 i = i + 1
328 }
329 }
330 out[o] = 0 as u8
331 return o
332}
333
334// ===== NEW (c): nav/TOC detection heuristic ========================
335// is_nav(raw_xhtml, rawn, text, textn): 1 if this spine item is the navigation/TOC doc.
336// Signals: an epub3 nav declares epub:type="toc" OR <nav ; OR it's short prose dominated by links.
337func is_nav_doc(raw: *u8, rawn: i64, text: *u8, textn: i64) -> i64 {
338 if find_sub(raw, rawn, "epub:type=\"toc\"" as *u8) >= 0 { return 1 }
339 if find_sub(raw, rawn, "epub:type='toc'" as *u8) >= 0 { return 1 }
340 if find_sub(raw, rawn, "<nav " as *u8) >= 0 { return 1 }
341 if find_sub(raw, rawn, "properties=\"nav\"" as *u8) >= 0 { return 1 }
342 if find_sub(raw, rawn, "type=\"toc\"" as *u8) >= 0 { return 1 }
343 // content backstop: link-dominated short page = a hand-rolled TOC.
344 let anchors: i64 = count_sub(raw, rawn, "<a " as *u8)
345 if anchors >= 8 { if textn < CHAP_MAGIC_1200 { return 1 } }
346 return 0
347}
348
349// first non-empty line of decoded text -> out (trimmed, cap chars). ret length or -1.
350func first_line(txt: *u8, n: i64, out: *u8, outcap: i64) -> i64 {
351 var s: i64 = 0
352 // skip leading blank lines / whitespace
353 while s < n { if txt[s] == (10 as u8) { s = s + 1 } else { if txt[s] == (32 as u8) { s = s + 1 } else { if txt[s] == (9 as u8) { s = s + 1 } else { break } } } }
354 if s >= n { return 0-1 }
355 var o: i64 = 0
356 while s < n { if txt[s] == (10 as u8) { break } if o < outcap-1 { out[o] = txt[s]; o = o + 1 } s = s + 1 }
357 // trim trailing ws
358 while o > 0 { if out[o-1] == (32 as u8) { o = o - 1 } else { if out[o-1] == (9 as u8) { o = o - 1 } else { break } } }
359 out[o] = 0 as u8
360 if o <= 0 { return 0-1 }
361 return o
362}
363
364// TITLE FIX: inner text of the first <open>..</close>, with nested inline tags (<span>, <b>, <a>...) STRIPPED.
365// elem_text stopped at the first nested '<', so a real-world heading like <h1><span>Chapter One</span></h1>
366// extracted to "" -> the chapter degraded to a bare "Chapter K". This keeps the heading text. ret len (0 if none).
367func eb_heading_inner(raw: *u8, rawn: i64, open: *u8, close: *u8, out: *u8, cap: i64) -> i64 {
368 out[0] = 0 as u8
369 let h: i64 = find_sub(raw, rawn, open)
370 if h < 0 { return 0 }
371 var gt: i64 = 0-1; var p: i64 = h
372 while p < rawn { if raw[p] == (0x3e as u8) { gt = p; p = rawn } else { p = p + 1 } }
373 if gt < 0 { return 0 }
374 let cs0: i64 = gt + 1
375 let he: i64 = find_sub((raw as i64 + cs0) as *u8, rawn - cs0, close)
376 var ce: i64 = rawn
377 if he >= 0 { ce = cs0 + he }
378 var o: i64 = 0; var i: i64 = cs0; var intag: i64 = 0
379 while i < ce {
380 let c: i64 = raw[i] as i64
381 if c == 0x3c { intag = 1 } else { if c == 0x3e { intag = 0 } else { if intag == 0 { if o < cap-1 { out[o]=raw[i] as u8; o=o+1 } } } }
382 i = i + 1
383 }
384 out[o] = 0 as u8
385 return o
386}
387
388// derive a chapter title: <h1>/<h2>/<h3> heading (inner tags stripped), else first decoded text line, else "Chapter K".
389func chap_title(raw: *u8, rawn: i64, dtxt: *u8, dn: i64, k: i64, out: *u8, outcap: i64) -> i64 {
390 var t: i64 = eb_heading_inner(raw, rawn, "<h1" as *u8, "</h1" as *u8, out, outcap)
391 if t <= 0 { t = eb_heading_inner(raw, rawn, "<h2" as *u8, "</h2" as *u8, out, outcap) }
392 if t <= 0 { t = eb_heading_inner(raw, rawn, "<h3" as *u8, "</h3" as *u8, out, outcap) }
393 if t <= 0 { t = first_line(dtxt, dn, out, outcap) }
394 if t <= 0 {
395 var o: i64 = er_cat(out, 0, "Chapter " as *u8)
396 o = er_catn(out, o, k)
397 out[o] = 0 as u8
398 return o
399 }
400 // trim leading + trailing whitespace the heading/source formatting leaves
401 var s: i64 = 0
402 while s < t { let c: i64 = out[s] as i64; if c==32 { s=s+1 } else { if c==10 { s=s+1 } else { if c==9 { s=s+1 } else { if c==13 { s=s+1 } else { break } } } } }
403 if s > 0 { var w: i64 = 0; while s + w < t { out[w] = out[s+w]; w = w + 1 } out[w] = 0 as u8; t = w }
404 while t > 0 { let c2: i64 = out[t-1] as i64; if c2==32 { t=t-1 } else { if c2==10 { t=t-1 } else { if c2==9 { t=t-1 } else { if c2==13 { t=t-1 } else { break } } } } }
405 out[t] = 0 as u8
406 return t
407}
408
409const CHAP_CAP: i64 = 4194304
410
411// mkdirat AT_FDCWD path 0755 (idempotent; -EEXIST harmless). x86_64 mkdirat=258.
412func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 }
413
414// ===== R-TOC helpers: locate the nav document + resolve nav hrefs to emitted chapter indices =====
415// find the manifest element OWNING a marker (e.g. properties="nav" or the dtbncx media-type) -> its href.
416func elem_owner_href(opf: *u8, ol: i64, marker: *u8, out: *u8, outcap: i64) -> i64 {
417 let at: i64 = find_sub(opf, ol, marker)
418 if at < 0 { return 0-1 }
419 var ts: i64 = at
420 while ts > 0 { if opf[ts] == (0x3c as u8) { break } ts = ts - 1 }
421 var te: i64 = at
422 while te < ol { if opf[te] == (0x3e as u8) { break } te = te + 1 }
423 return attr_val((opf as i64 + ts) as *u8, te - ts, "href=\"" as *u8, out, outcap)
424}
425// copy basename (after last '/') of src into out; ret length.
426func base_after_slash(src: *u8, out: *u8, outcap: i64) -> i64 {
427 var n: i64 = 0; while src[n] != (0 as u8) { n = n + 1 }
428 var ls: i64 = 0-1; var i: i64 = 0
429 while i < n { if src[i] == (0x2f as u8) { ls = i } i = i + 1 }
430 var o: i64 = 0; var s: i64 = ls + 1
431 while s < n { if o < outcap-1 { out[o] = src[s]; o = o + 1 } s = s + 1 }
432 out[o] = 0 as u8
433 return o
434}
435// truncate a NUL-term string at the first '#'; ret new length.
436func strip_frag(s: *u8) -> i64 {
437 var i: i64 = 0
438 while s[i] != (0 as u8) { if s[i] == (0x23 as u8) { s[i] = 0 as u8; return i } i = i + 1 }
439 return i
440}
441func str_eq0(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8){ if a[i]!=b[i]{return 0} i=i+1 } if b[i]!=(0 as u8){return 0} return 1 }
442
443// R-ART-2: copy raw XHTML -> out, replacing each <img ... src="X" ...> with a text marker
444// "\n[[nximg:<basename(X)>]]\n" so the (image-stripping) nx_html_to_text PRESERVES figure POSITIONS as inline
445// text the reader renders as <img>. Localized to nx_epub_book (NOT shared nx_html_to_text -> no leak to wiki/
446// research consumers). Other markup copied verbatim. Returns out length.
447func img_markerize(raw: *u8, n: i64, out: *u8, cap: i64) -> i64 {
448 var i: i64 = 0
449 var o: i64 = 0
450 while i < n {
451 var isimg: i64 = 0
452 if i + 4 < n { if raw[i]==(0x3c as u8) { if raw[i+1]==(0x69 as u8) { if raw[i+2]==(0x6d as u8) { if raw[i+3]==(0x67 as u8) {
453 let c: i64 = raw[i+4] as i64
454 if c==0x20 { isimg=1 } else { if c==0x09 { isimg=1 } else { if c==0x0a { isimg=1 } else { if c==0x0d { isimg=1 } else { if c==0x2f { isimg=1 } else { if c==0x3e { isimg=1 } } } } } }
455 } } } } }
456 if isimg == 1 {
457 var te: i64 = i
458 while te < n { if raw[te]==(0x3e as u8) { break } te = te + 1 }
459 let src: *u8 = sys_mmap(CHAP_MAGIC_1024)
460 let got: i64 = attr_val((raw as i64 + i) as *u8, te - i, "src=\"" as *u8, src, CHAP_MAGIC_1024)
461 if got > 0 {
462 let base: *u8 = sys_mmap(512)
463 base_after_slash(src, base, 512)
464 strip_frag(base)
465 if o < cap-1 { out[o]=0x0a as u8; o=o+1 }
466 let pfx: *u8 = "[[nximg:" as *u8
467 var mk: i64 = 0
468 while pfx[mk]!=(0 as u8){ if o<cap-1 { out[o]=pfx[mk]; o=o+1 } mk=mk+1 }
469 var bj: i64 = 0
470 while base[bj]!=(0 as u8){ if o<cap-1 { out[o]=base[bj]; o=o+1 } bj=bj+1 }
471 if o < cap-2 { out[o]=0x5d as u8; o=o+1; out[o]=0x5d as u8; o=o+1 }
472 if o < cap-1 { out[o]=0x0a as u8; o=o+1 }
473 }
474 if te < n { i = te + 1 } else { i = n }
475 } else {
476 if o < cap-1 { out[o] = raw[i]; o = o + 1 }
477 i = i + 1
478 }
479 }
480 out[o] = 0 as u8
481 return o
482}
483
484// does raw[i..] start with "<TAG" delimited by a following space/tab/nl/>// ? (so "<a" != "<aside")
485func tag_is(raw: *u8, n: i64, i: i64, tag: *u8) -> i64 {
486 if raw[i] != (0x3c as u8) { return 0 }
487 var k: i64 = 0
488 while tag[k] != (0 as u8) { if i+1+k >= n { return 0 } if raw[i+1+k] != tag[k] { return 0 } k = k + 1 }
489 if i+1+k >= n { return 0 }
490 let nc: i64 = raw[i+1+k] as i64
491 if nc==0x20 { return 1 }
492 if nc==0x09 { return 1 }
493 if nc==0x0a { return 1 }
494 if nc==0x0d { return 1 }
495 if nc==0x3e { return 1 }
496 if nc==0x2f { return 1 }
497 return 0
498}
499
500// FOOTNOTES (EPUB epub:type=noteref / epub:type=footnote), mirroring img_markerize:
501// - a noteref <a epub:type="noteref" href="#X" ...>D</a> -> inline text marker [[fnref:X:D]]
502// - a footnote <aside epub:type="footnote" id="X"> INNER </aside> -> EXTRACTED (id X + raw INNER recorded in
503// fnctx) and REMOVED from the prose flow (emits nothing). The reader links each ref to its extracted note.
504// fnctx[0]=nfn, [1]=id_arena(*u8), [2]=id_off(*i64), [3]=inner_arena(*u8), [4]=inner_off(*i64). Caller resets
505// fnctx[0]=0 per chapter (arenas reused). Returns out length. Localized to nx_epub_book (no nx_html_to_text leak).
506func fn_markerize(raw: *u8, n: i64, out: *u8, cap: i64, fnctx: *i64) -> i64 {
507 let id_ar: *u8 = fnctx[1] as *u8
508 let id_of: *i64 = fnctx[2] as *i64
509 let in_ar: *u8 = fnctx[3] as *u8
510 let in_of: *i64 = fnctx[4] as *i64
511 var nfn: i64 = 0
512 var ido: i64 = 0
513 var ino: i64 = 0
514 var i: i64 = 0
515 var o: i64 = 0
516 while i < n {
517 var handled: i64 = 0
518 if tag_is(raw, n, i, "aside" as *u8) == 1 {
519 var aend: i64 = i; while aend < n { if raw[aend]==(0x3e as u8) { break } aend=aend+1 }
520 if find_sub((raw as i64+i) as *u8, aend-i, "footnote" as *u8) >= 0 {
521 let idv: *u8 = sys_mmap(256); idv[0]=0 as u8
522 attr_val((raw as i64+i) as *u8, aend-i, "id=\"" as *u8, idv, 256)
523 let cf: i64 = find_sub((raw as i64+aend+1) as *u8, n-aend-1, "</aside" as *u8)
524 var cstart: i64 = n
525 if cf >= 0 { cstart = aend+1+cf }
526 if nfn < 64 {
527 id_of[nfn]=ido; var a:i64=0; while idv[a]!=(0 as u8){ id_ar[ido]=idv[a]; ido=ido+1; a=a+1 } id_ar[ido]=0 as u8; ido=ido+1
528 in_of[nfn]=ino; var b:i64=aend+1; while b<cstart { in_ar[ino]=raw[b]; ino=ino+1; b=b+1 } in_ar[ino]=0 as u8; ino=ino+1
529 nfn=nfn+1
530 }
531 var askip: i64 = cstart; while askip<n { if raw[askip]==(0x3e as u8){break} askip=askip+1 }
532 if askip<n { i=askip+1 } else { i=n }
533 handled=1
534 }
535 }
536 if handled == 0 { if tag_is(raw, n, i, "a" as *u8) == 1 {
537 var rend: i64 = i; while rend < n { if raw[rend]==(0x3e as u8) { break } rend=rend+1 }
538 if find_sub((raw as i64+i) as *u8, rend-i, "noteref" as *u8) >= 0 {
539 let hrefv: *u8 = sys_mmap(256); hrefv[0]=0 as u8
540 attr_val((raw as i64+i) as *u8, rend-i, "href=\"" as *u8, hrefv, 256)
541 var hs:i64=0; if hrefv[0]==(0x23 as u8){ hs=1 }
542 var dend:i64=rend+1; while dend<n { if raw[dend]==(0x3c as u8){break} dend=dend+1 }
543 let pfx:*u8="[[fnref:" as *u8; var m:i64=0; while pfx[m]!=(0 as u8){ if o<cap-1{out[o]=pfx[m];o=o+1} m=m+1 }
544 var hi:i64=hs; while hrefv[hi]!=(0 as u8){ if o<cap-1{out[o]=hrefv[hi];o=o+1} hi=hi+1 }
545 if o<cap-1{out[o]=0x3a as u8;o=o+1}
546 var di:i64=rend+1; while di<dend{ if o<cap-1{out[o]=raw[di];o=o+1} di=di+1 }
547 if o<cap-2{out[o]=0x5d as u8;o=o+1;out[o]=0x5d as u8;o=o+1}
548 var rskip:i64=dend; while rskip<n { if raw[rskip]==(0x3e as u8){break} rskip=rskip+1 }
549 if rskip<n { i=rskip+1 } else { i=n }
550 handled=1
551 }
552 } }
553 if handled == 0 { if o<cap-1 { out[o]=raw[i]; o=o+1 } i=i+1 }
554 }
555 out[o]=0 as u8
556 fnctx[0]=nfn
557 return o
558}
559
560func do_full_book(epath: *u8, slug: *u8) -> i64 {
561 let lb: *i64 = sys_mmap(16) as *i64
562 let zip: *u8 = sys_read_file(epath, lb)
563 if (zip as i64) == 0 { er_p("FULLBOOK-FAIL read\n" as *u8); return 0-1 }
564 let zl: i64 = lb[0]
565
566 // container.xml -> OPF path
567 let cbuf: *u8 = sys_mmap(CHAP_MAGIC_262144)
568 let cn: i64 = zip_read_named(zip, zl, "META-INF/container.xml" as *u8, cbuf, CHAP_MAGIC_262144)
569 if cn < 0 { er_p("FULLBOOK-FAIL container\n" as *u8); return 0-1 }
570 let opfpath: *u8 = sys_mmap(CHAP_MAGIC_1024)
571 let opn: i64 = attr_val(cbuf, cn, "full-path=\"" as *u8, opfpath, CHAP_MAGIC_1024)
572 if opn < 0 { er_p("FULLBOOK-FAIL opfpath\n" as *u8); return 0-1 }
573
574 // OPF -> metadata + spine
575 let obuf: *u8 = sys_mmap(CHAP_MAGIC_2097152)
576 let on: i64 = zip_read_named(zip, zl, opfpath, obuf, CHAP_MAGIC_2097152)
577 if on < 0 { er_p("FULLBOOK-FAIL opf\n" as *u8); return 0-1 }
578 let title: *u8 = sys_mmap(CHAP_MAGIC_1024)
579 var tn: i64 = elem_text(obuf, on, "<dc:title" as *u8, title, CHAP_MAGIC_1024)
580 if tn < 0 { tn = elem_text(obuf, on, "<title" as *u8, title, CHAP_MAGIC_1024) }
581 if tn < 0 { title[0] = 0 as u8 }
582 let author: *u8 = sys_mmap(CHAP_MAGIC_1024)
583 var an: i64 = elem_text(obuf, on, "<dc:creator" as *u8, author, CHAP_MAGIC_1024)
584 if an < 0 { author[0] = 0 as u8 }
585 // ISSUE-3 metadata: capture dc:publisher / dc:date / dc:description so the reader's author page can show
586 // "all the book info we have local" (parity with the MOBI/KF8 EXTH path).
587 let publisher: *u8 = sys_mmap(CHAP_MAGIC_1024)
588 if elem_text(obuf, on, "<dc:publisher" as *u8, publisher, CHAP_MAGIC_1024) < 0 { publisher[0] = 0 as u8 }
589 let pubdate: *u8 = sys_mmap(256)
590 if elem_text(obuf, on, "<dc:date" as *u8, pubdate, 256) < 0 { pubdate[0] = 0 as u8 }
591 let descr: *u8 = sys_mmap(CHAP_MAGIC_8192)
592 if elem_text(obuf, on, "<dc:description" as *u8, descr, CHAP_MAGIC_8192) < 0 { descr[0] = 0 as u8 }
593
594 let arena: *u8 = sys_mmap(CHAP_MAGIC_65536)
595 let offs: *i64 = sys_mmap(8 * 512) as *i64
596 let nspine: i64 = get_spine_idrefs(obuf, on, arena, offs, 512)
597 if nspine <= 0 { er_p("FULLBOOK-FAIL no-spine\n" as *u8); return 0-1 }
598
599 // per-slug output dir (additive; idempotent)
600 let dir: *u8 = sys_mmap(512)
601 var dlen: i64 = er_cat(dir, 0, "knowledge/staging/media/reader/" as *u8)
602 dlen = er_cat(dir, dlen, slug); dir[dlen] = 0 as u8
603 ensure_dir(dir)
604
605 // book.json path
606 let jp: *u8 = sys_mmap(512)
607 var jo: i64 = er_cat(jp, 0, dir as *u8); jo = er_cat(jp, jo, "/book.json" as *u8); jp[jo] = 0 as u8
608 let jfd: i64 = sys_openat_wr(jp, 0x1a4)
609 if jfd < 0 { er_p("FULLBOOK-FAIL open-json\n" as *u8); return 0-1 }
610 er_w(jfd, "{\"title\":\"" as *u8); er_wjson(jfd, title)
611 er_w(jfd, "\",\"author\":\"" as *u8); er_wjson(jfd, author)
612 er_w(jfd, "\",\"publisher\":\"" as *u8); er_wjson(jfd, publisher)
613 er_w(jfd, "\",\"pubdate\":\"" as *u8); er_wjson(jfd, pubdate)
614 er_w(jfd, "\",\"isbn\":\"\",\"description\":\"" as *u8); er_wjson(jfd, descr)
615 er_w(jfd, "\",\"format\":\"epub\",\"rfix\":1,\"slug\":\"" as *u8); er_wjson(jfd, slug) // rfix=reader-fix schema -> server re-extracts older caches
616 er_w(jfd, "\",\"chapters\":[" as *u8)
617
618 let xbuf: *u8 = sys_mmap(CHAP_CAP)
619 let tbuf: *u8 = sys_mmap(CHAP_CAP)
620 let dbuf: *u8 = sys_mmap(CHAP_CAP)
621 let rbuf: *u8 = sys_mmap(CHAP_CAP) // MOJIBAKE FIX: repaired (clean-UTF-8) copy of dbuf, written to chap<i>.txt
622 let mbuf: *u8 = sys_mmap(CHAP_CAP) // R-ART-2: <img>-markerized copy fed to html_to_text
623 let fbuf: *u8 = sys_mmap(CHAP_CAP) // FOOTNOTES: fn-markerized copy (refs->markers, asides removed)
624 let ftbuf: *u8 = sys_mmap(CHAP_CAP) // footnote inner raw -> html_to_text
625 let fdbuf: *u8 = sys_mmap(CHAP_CAP) // footnote text -> decode_numeric_refs
626 let fnctx: *i64 = sys_mmap(8*8) as *i64
627 fnctx[0] = 0
628 fnctx[1] = sys_mmap(CHAP_MAGIC_16384) as i64 // id arena
629 fnctx[2] = sys_mmap(8*64) as i64 // id offsets
630 fnctx[3] = sys_mmap(CHAP_MAGIC_262144) as i64 // inner-html arena
631 fnctx[4] = sys_mmap(8*64) as i64 // inner offsets
632 let href: *u8 = sys_mmap(CHAP_MAGIC_1024)
633 let chpath: *u8 = sys_mmap(CHAP_MAGIC_2048)
634 let ctitle: *u8 = sys_mmap(CHAP_MAGIC_1024)
635 let chfile: *u8 = sys_mmap(512)
636 // R-TOC: record each emitted chapter's content basename so nav hrefs resolve to chapter indices.
637 let cbar: *u8 = sys_mmap(CHAP_MAGIC_32768)
638 let cboff: *i64 = sys_mmap(8 * 512) as *i64
639 var cbcur: i64 = 0
640
641 var total_chars: i64 = 0
642 var has_raw: i64 = 0
643 var nonnav: i64 = 0
644 var emitted: i64 = 0
645 var si: i64 = 0
646 while si < nspine {
647 let idref: *u8 = (arena as i64 + offs[si]) as *u8
648 let hn: i64 = href_for_id(obuf, on, idref, href, CHAP_MAGIC_1024)
649 if hn > 0 {
650 join_path(opfpath, href, chpath)
651 let xn: i64 = zip_read_named(zip, zl, chpath, xbuf, CHAP_CAP)
652 if xn > 0 {
653 cboff[emitted] = cbcur // R-TOC: basename of this chapter's content path
654 cbcur = cbcur + base_after_slash(chpath, (cbar as i64 + cbcur) as *u8, 256) + 1
655 fnctx[0] = 0
656 let fxn: i64 = fn_markerize(xbuf, xn, fbuf, CHAP_CAP, fnctx) // FOOTNOTES: extract asides + mark refs
657 let mxn: i64 = img_markerize(fbuf, fxn, mbuf, CHAP_CAP) // R-ART-2: preserve <img> positions inline
658 // ISSUE-3 hyperlinks: emit_links=1 -> <a href> become inline STX/SOH/ETX markers the reader renders
659 // as clickable <a> (external->new tab, internal->jump to chapter). Footnote refs were already pulled
660 // out by fn_markerize above, so these are real cross-references, not notes.
661 let tn2: i64 = nx_html_to_text_x(mbuf, mxn, tbuf, CHAP_CAP, 1)
662 let dn0: i64 = decode_numeric_refs(tbuf, tn2, dbuf, CHAP_CAP)
663 // MOJIBAKE FIX: repair stray Windows-1252 bytes (curly apostrophe/quotes/em-dash) -> clean UTF-8
664 // so the reader stops showing U+FFFD. Idempotent on already-valid UTF-8. rbuf/dn replace dbuf/dn0.
665 let dn: i64 = nx_charset_repair_utf8(dbuf, dn0, rbuf, CHAP_CAP)
666 // no-fake-green assertion: no numeric refs may survive the decode pass
667 if find_sub(rbuf, dn, "&#" as *u8) >= 0 { has_raw = 1 }
668 let nav: i64 = is_nav_doc(xbuf, xn, rbuf, dn)
669 if nav == 0 { nonnav = nonnav + 1 }
670 // write chap<emitted>.txt
671 var co: i64 = er_cat(chfile, 0, "chap" as *u8)
672 co = er_catn(chfile, co, emitted)
673 co = er_cat(chfile, co, ".txt" as *u8); chfile[co] = 0 as u8
674 var fpo: i64 = er_cat(chpath, 0, dir as *u8) // reuse chpath buf for full path
675 fpo = er_cat(chpath, fpo, "/" as *u8)
676 fpo = er_cat(chpath, fpo, chfile as *u8); chpath[fpo] = 0 as u8
677 let cfd: i64 = sys_openat_wr(chpath, 0x1a4)
678 if cfd >= 0 { sys_write(cfd, rbuf, dn); sys_close(cfd) }
679 // title
680 chap_title(xbuf, xn, rbuf, dn, emitted, ctitle, CHAP_MAGIC_1024)
681 // json chapters[] entry
682 if emitted > 0 { er_w(jfd, "," as *u8) }
683 er_w(jfd, "{\"idx\":" as *u8); er_n(jfd, emitted)
684 er_w(jfd, ",\"title\":\"" as *u8); er_wjson(jfd, ctitle)
685 er_w(jfd, "\",\"file\":\"" as *u8); er_wjson(jfd, chfile)
686 // ISSUE-3: source basename (e.g. "ch3.xhtml") so the reader maps an internal <a href> -> this chapter idx.
687 er_w(jfd, "\",\"src\":\"" as *u8); er_wjson(jfd, (cbar as i64 + cboff[emitted]) as *u8)
688 er_w(jfd, "\",\"chars\":" as *u8); er_n(jfd, dn)
689 er_w(jfd, ",\"is_nav\":" as *u8); er_n(jfd, nav)
690 er_w(jfd, ",\"fn\":[" as *u8)
691 let fnid_ar: *u8 = fnctx[1] as *u8
692 let fnid_of: *i64 = fnctx[2] as *i64
693 let fnin_ar: *u8 = fnctx[3] as *u8
694 let fnin_of: *i64 = fnctx[4] as *i64
695 var fi: i64 = 0
696 while fi < fnctx[0] {
697 let fid: *u8 = (fnid_ar as i64 + fnid_of[fi]) as *u8
698 let fin: *u8 = (fnin_ar as i64 + fnin_of[fi]) as *u8
699 let ftn: i64 = nx_html_to_text(fin, er_slen(fin), ftbuf, CHAP_CAP)
700 decode_numeric_refs(ftbuf, ftn, fdbuf, CHAP_CAP)
701 if fi > 0 { er_w(jfd, "," as *u8) }
702 er_w(jfd, "{\"id\":\"" as *u8); er_wjson(jfd, fid)
703 er_w(jfd, "\",\"text\":\"" as *u8); er_wjson(jfd, fdbuf)
704 er_w(jfd, "\"}" as *u8)
705 fi = fi + 1
706 }
707 er_w(jfd, "]" as *u8)
708 er_w(jfd, "}" as *u8)
709 total_chars = total_chars + dn
710 emitted = emitted + 1
711 }
712 }
713 si = si + 1
714 }
715
716 er_w(jfd, "],\"nchapters\":" as *u8); er_n(jfd, emitted)
717
718 // ===== R-TOC: emit the hierarchical TOC from the navigation document (EPUB3 nav, else EPUB2 NCX) =====
719 let navhref: *u8 = sys_mmap(CHAP_MAGIC_1024)
720 let navbuf: *u8 = sys_mmap(CHAP_CAP)
721 var navn: i64 = 0
722 var is_ncx: i64 = 0
723 var found_nav: i64 = 0
724 if elem_owner_href(obuf, on, "properties=\"nav\"" as *u8, navhref, CHAP_MAGIC_1024) > 0 {
725 join_path(opfpath, navhref, chpath)
726 navn = zip_read_named(zip, zl, chpath, navbuf, CHAP_CAP)
727 if navn > 0 { found_nav = 1 }
728 }
729 if found_nav == 0 {
730 if elem_owner_href(obuf, on, "application/x-dtbncx+xml" as *u8, navhref, CHAP_MAGIC_1024) > 0 {
731 join_path(opfpath, navhref, chpath)
732 navn = zip_read_named(zip, zl, chpath, navbuf, CHAP_CAP)
733 if navn > 0 { found_nav = 1; is_ncx = 1 }
734 }
735 }
736 er_w(jfd, ",\"toc\":[" as *u8)
737 var ntoc: i64 = 0
738 if found_nav == 1 {
739 let tdep: *i64 = sys_mmap(8 * 512) as *i64
740 let tloff: *i64 = sys_mmap(8 * 512) as *i64
741 let thoff: *i64 = sys_mmap(8 * 512) as *i64
742 let tlar: *u8 = sys_mmap(CHAP_MAGIC_65536)
743 let thar: *u8 = sys_mmap(CHAP_MAGIC_65536)
744 var tc: i64 = 0
745 if is_ncx == 1 { tc = ncx_parse(navbuf, navn, tdep, tlar, tloff, thar, thoff, 512) }
746 else { tc = nav_parse_xhtml(navbuf, navn, tdep, tlar, tloff, thar, thoff, 512) }
747 let keybuf: *u8 = sys_mmap(CHAP_MAGIC_1024)
748 var ti: i64 = 0
749 while ti < tc {
750 let lab: *u8 = (tlar as i64 + tloff[ti]) as *u8
751 let hrf: *u8 = (thar as i64 + thoff[ti]) as *u8
752 base_after_slash(hrf, keybuf, CHAP_MAGIC_1024)
753 strip_frag(keybuf)
754 var idx: i64 = 0 - 1
755 var ci: i64 = 0
756 while ci < emitted {
757 let cb: *u8 = (cbar as i64 + cboff[ci]) as *u8
758 if str_eq0(cb, keybuf) == 1 { idx = ci; ci = emitted } else { ci = ci + 1 }
759 }
760 if ntoc > 0 { er_w(jfd, "," as *u8) }
761 er_w(jfd, "{\"depth\":" as *u8); er_n(jfd, tdep[ti])
762 er_w(jfd, ",\"title\":\"" as *u8); er_wjson(jfd, lab)
763 er_w(jfd, "\",\"idx\":" as *u8); er_n(jfd, idx)
764 er_w(jfd, ",\"href\":\"" as *u8); er_wjson(jfd, hrf)
765 er_w(jfd, "\"}" as *u8)
766 ntoc = ntoc + 1
767 ti = ti + 1
768 }
769 }
770 er_w(jfd, "],\"ntoc\":" as *u8); er_n(jfd, ntoc)
771 er_w(jfd, ",\"toc_source\":\"" as *u8)
772 if found_nav == 0 { er_w(jfd, "spine-fallback" as *u8) } else { if is_ncx == 1 { er_w(jfd, "ncx" as *u8) } else { er_w(jfd, "nav-xhtml" as *u8) } }
773 er_w(jfd, "\"" as *u8) // close toc_source string (object stays open for assets)
774
775 // ===== R-ART: extract cover + embedded image assets to the slug dir; reference them in book.json =====
776 // cover href: EPUB3 properties="cover-image" preferred, else EPUB2 <meta name="cover" content="ID"> -> item id
777 let coverhref: *u8 = sys_mmap(CHAP_MAGIC_1024)
778 var has_cover: i64 = 0
779 if elem_owner_href(obuf, on, "properties=\"cover-image\"" as *u8, coverhref, CHAP_MAGIC_1024) > 0 { has_cover = 1 }
780 if has_cover == 0 {
781 let cmeta: *u8 = sys_mmap(256)
782 let mp: i64 = find_sub(obuf, on, "name=\"cover\"" as *u8)
783 if mp >= 0 {
784 var me: i64 = mp
785 while me < on { if obuf[me] == (0x3e as u8) { break } me = me + 1 }
786 if attr_val((obuf as i64 + mp) as *u8, me - mp, "content=\"" as *u8, cmeta, 256) > 0 {
787 if href_for_id(obuf, on, cmeta, coverhref, CHAP_MAGIC_1024) > 0 { has_cover = 1 }
788 }
789 }
790 }
791 let coverbase: *u8 = sys_mmap(512); coverbase[0] = 0 as u8
792 if has_cover == 1 { base_after_slash(coverhref, coverbase, 512) }
793
794 er_w(jfd, ",\"assets\":[" as *u8)
795 let imgbuf: *u8 = sys_mmap(CHAP_CAP)
796 let ahref: *u8 = sys_mmap(CHAP_MAGIC_1024)
797 let abase: *u8 = sys_mmap(512)
798 let apath: *u8 = sys_mmap(CHAP_MAGIC_2048)
799 var nass: i64 = 0
800 var scan: i64 = find_sub(obuf, on, "media-type=\"image/" as *u8)
801 while scan >= 0 {
802 var ts: i64 = scan; while ts > 0 { if obuf[ts] == (0x3c as u8) { break } ts = ts - 1 }
803 var te: i64 = scan; while te < on { if obuf[te] == (0x3e as u8) { break } te = te + 1 }
804 let hh: i64 = attr_val((obuf as i64 + ts) as *u8, te - ts, "href=\"" as *u8, ahref, CHAP_MAGIC_1024)
805 if hh > 0 {
806 join_path(opfpath, ahref, apath) // content path inside the zip
807 let ib: i64 = zip_read_named(zip, zl, apath, imgbuf, CHAP_CAP)
808 if ib > 0 {
809 base_after_slash(ahref, abase, 512)
810 var ao: i64 = er_cat(apath, 0, dir as *u8) // reuse apath for the output path
811 ao = er_cat(apath, ao, "/" as *u8)
812 ao = er_cat(apath, ao, abase as *u8); apath[ao] = 0 as u8
813 let afd: i64 = sys_openat_wr(apath, 0x1a4)
814 if afd >= 0 { sys_write(afd, imgbuf, ib); sys_close(afd) }
815 // content fingerprint (djb2) -- a stable content id; canonical CID + library foundation = R-ART-2
816 var hsh: i64 = CHAP_MAGIC_5381
817 var bi: i64 = 0
818 while bi < ib { hsh = ((hsh << 5) + hsh) + (imgbuf[bi] as i64); bi = bi + 1 }
819 var is_cov: i64 = 0
820 if has_cover == 1 { if str_eq0(abase, coverbase) == 1 { is_cov = 1 } }
821 if nass > 0 { er_w(jfd, "," as *u8) }
822 er_w(jfd, "{\"file\":\"" as *u8); er_wjson(jfd, abase)
823 er_w(jfd, "\",\"bytes\":" as *u8); er_n(jfd, ib)
824 er_w(jfd, ",\"fp\":" as *u8); er_n(jfd, hsh)
825 er_w(jfd, ",\"is_cover\":" as *u8); er_n(jfd, is_cov)
826 er_w(jfd, "}" as *u8)
827 nass = nass + 1
828 }
829 }
830 let nxt: i64 = find_sub((obuf as i64 + scan + 1) as *u8, on - scan - 1, "media-type=\"image/" as *u8)
831 if nxt < 0 { scan = 0 - 1 } else { scan = scan + 1 + nxt }
832 }
833 er_w(jfd, "],\"nassets\":" as *u8); er_n(jfd, nass)
834 er_w(jfd, ",\"cover\":\"" as *u8); if has_cover == 1 { er_wjson(jfd, coverbase) }
835 er_w(jfd, "\"}" as *u8)
836 sys_close(jfd)
837
838 er_p("BOOK title=\"" as *u8); er_w(1, title)
839 er_p("\" author=\"" as *u8); er_w(1, author)
840 er_p("\" nchapters=" as *u8); er_pn(emitted)
841 er_p(" nonnav=" as *u8); er_pn(nonnav)
842 er_p(" total_chars=" as *u8); er_pn(total_chars)
843 er_p(" has_raw_entities=" as *u8); er_pn(has_raw)
844 er_p(" ntoc=" as *u8); er_pn(ntoc)
845 er_p(" toc_source=" as *u8)
846 if found_nav == 0 { er_p("spine-fallback" as *u8) } else { if is_ncx == 1 { er_p("ncx" as *u8) } else { er_p("nav-xhtml" as *u8) } }
847 er_p(" nassets=" as *u8); er_pn(nass)
848 er_p(" cover=" as *u8); er_w(1, coverbase)
849 er_p("\n" as *u8)
850
851 if emitted <= 0 { return 0-1 }
852 if total_chars < CHAP_MAGIC_2000 { return 0-1 }
853 if has_raw != 0 { return 0-1 }
854 if nonnav <= 0 { return 0-1 }
855 return 0
856}
857
858// argv-driven (additive): `nx_epub_book <epub-path> <slug>` extracts ANY book on demand; with no args it
859// keeps the original hardcoded default so existing (ark-loop) usage is unchanged.
860func main(argc: i64, argv: *i64) -> i64 {
861 er_p("FULLBOOK: start (sovereign full-spine + numeric-entity decode + nav-skip)\n" as *u8)
862 ensure_dir("knowledge/staging/media/reader" as *u8)
863 var epath: *u8 = "/mnt/nas_homes/elderwesto/books/Richard Lee Byers/Dissolution_ War of the Spider Queen - Book I (7926)/Dissolution_ War of the Spider Queen - Boo - Richard Lee Byers.epub" as *u8
864 var slug: *u8 = "dissolution_wotsq_b1" as *u8
865 if argc >= 3 { epath = argv[1] as *u8; slug = argv[2] as *u8 }
866 let r: i64 = do_full_book(epath, slug)
867 if r == 0 { er_p("FULLBOOK-OK\n" as *u8); sys_exit(0); return 0 }
868 er_p("FULLBOOK-FAIL\n" as *u8); sys_exit(1); return 1
869}