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// &#x2019; / &#x201c; / &#x2014;; 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}