code wiki / _hdl_build / nx_fb2_book.nx

nx_fb2_book.nx source

↩ module page · 194 lines · 11225 B

1// nx_fb2_book.nx -- SOVEREIGN FB2 (FictionBook 2.0) -> Nishi format. The Nishi equivalent of Calibre's FB2 input 2// plugin (OSS = benchmark, NOT a dependency -- per the sovereignty law). FB2 is a SINGLE XML file: 3// <FictionBook><description><title-info><book-title>T</book-title> 4// <author><first-name>F</first-name><last-name>L</last-name></author></title-info></description> 5// <body> <section><title>..</title><p>..</p>..</section> .. </body></FictionBook> 6// Extract title/author from <description>, run nx_html_to_text over the <body> only (FB2's <p>/<emphasis> are 7// HTML-ish; unknown tags strip cleanly leaving the prose), emit the SAME book.json the zero-JS reader renders. 8// Rung 1: whole <body> -> one chapter (readable); <section> chapter-splitting + <binary> images = next rungs. 9// Usage: nx_fb2_book <fb2-path> <slug>. expect_exit: 0 license_tier: ORIGINAL 10import "nx_syscalls.nx" 11import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 12import "nx_html_to_text.nx" 13import "nx_chapter_split.nx" 14const K_MAGIC_1024: i64 = 1024 15 16func fb_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 17func fb_w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, fb_slen(s)); return 0 } 18// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 19// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 20// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 21// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 22func fb_n(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 } 23func fb_p(s: *u8) -> i64 { fb_w(1, s); return 0 } 24func fb_pn(v: i64) -> i64 { fb_n(1, v); return 0 } 25func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 } 26func 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 } 27 28func find_sub(hay: *u8, hl: i64, needle: *u8) -> i64 { 29 let nl: i64 = fb_slen(needle); if nl == 0 { return 0-1 } 30 var i: i64 = 0 31 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 } 32 return 0-1 33} 34// element text: find opentag (e.g. "<book-title"), skip to '>', copy until next '<'. ret len or -1. 35func elem_text(hay: *u8, hl: i64, opentag: *u8, out: *u8, outcap: i64) -> i64 { 36 let at: i64 = find_sub(hay, hl, opentag) 37 if at < 0 { out[0]=0 as u8; return 0-1 } 38 var s: i64 = at 39 while s < hl { if hay[s] == (0x3e as u8) { s = s + 1; break } s = s + 1 } 40 var o: i64 = 0 41 while s < hl { if hay[s] == (0x3c as u8) { break } if o < outcap-1 { out[o] = hay[s]; o = o + 1 } s = s + 1 } 42 out[o] = 0 as u8 43 return o 44} 45func er_wjson(fd: i64, s: *u8) -> i64 { 46 var i: i64 = 0 47 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c == 0x22 { sys_write(fd, "\\\"" as *u8, 2) } else { if c == 0x5c { sys_write(fd, "\\\\" as *u8, 2) } else { if c < 0x20 { sys_write(fd, " " as *u8, 1) } else { sys_write(fd, (s as i64 + i) as *u8, 1) } } } i = i + 1 } 48 return 0 49} 50func utf8_put(out: *u8, o: i64, cp: i64) -> i64 { 51 if cp < 0x80 { out[o] = cp as u8; return o + 1 } 52 if cp < 0x800 { out[o] = (0xC0 | (cp >> 6)) as u8; out[o+1] = (0x80 | (cp & 0x3F)) as u8; return o + 2 } 53 if cp < 0x10000 { out[o] = (0xE0 | (cp >> 12)) as u8; out[o+1] = (0x80 | ((cp >> 6) & 0x3F)) as u8; out[o+2] = (0x80 | (cp & 0x3F)) as u8; return o + 3 } 54 out[o] = (0xF0 | (cp >> 18)) as u8; out[o+1] = (0x80 | ((cp >> 12) & 0x3F)) as u8; out[o+2] = (0x80 | ((cp >> 6) & 0x3F)) as u8; out[o+3] = (0x80 | (cp & 0x3F)) as u8; return o + 4 55} 56func decode_numeric_refs(txt: *u8, n: i64, out: *u8, outcap: i64) -> i64 { 57 var i: i64 = 0 58 var o: i64 = 0 59 while i < n { 60 let c: i64 = txt[i] as i64 61 var done: i64 = 0 62 if c == 0x26 { if i + 1 < n { if (txt[i+1] as i64) == 0x23 { 63 var j: i64 = i + 2 64 var hex: i64 = 0 65 if j < n { let xc: i64 = txt[j] as i64; if xc == 0x78 { hex = 1; j = j + 1 } else { if xc == 0x58 { hex = 1; j = j + 1 } } } 66 var cp: i64 = 0 67 var ndig: i64 = 0 68 var scan: i64 = 1 69 while scan == 1 { 70 if j >= n { scan = 0 } else { 71 let dc: i64 = txt[j] as i64 72 var dv: i64 = 0-1 73 if hex == 1 { if dc >= 48 { if dc <= 57 { dv = dc - 48 } } if dc >= 97 { if dc <= 102 { dv = dc - 87 } } if dc >= 65 { if dc <= 70 { dv = dc - 55 } } } 74 else { if dc >= 48 { if dc <= 57 { dv = dc - 48 } } } 75 if dv < 0 { scan = 0 } else { if hex == 1 { cp = cp * 16 + dv } else { cp = cp * 10 + dv } ndig = ndig + 1; j = j + 1 } 76 } 77 } 78 if ndig > 0 { if j < n { if (txt[j] as i64) == 0x3b { if cp > 0 { if cp <= 0x10FFFF { if o + 4 < outcap { o = utf8_put(out, o, cp) } i = j + 1; done = 1 } } } } } 79 } } } 80 if done == 0 { if o < outcap - 1 { out[o] = txt[i]; o = o + 1 } i = i + 1 } 81 } 82 out[o] = 0 as u8 83 return o 84} 85 86const FCAP: i64 = 8388608 87 88func do_fb2(path: *u8, slug: *u8) -> i64 { 89 let lp: *i64 = sys_mmap(16) as *i64 90 let z: *u8 = sys_read_file(path, lp) 91 if (z as i64) == 0 { fb_p("FB2-FAIL read " as *u8); fb_p(path); fb_p("\n" as *u8); return 0-1 } 92 let zl: i64 = lp[0] 93 if find_sub(z, zl, "<FictionBook" as *u8) < 0 { fb_p("FB2-FAIL not-fictionbook\n" as *u8); return 0-1 } 94 95 let title: *u8 = sys_mmap(K_MAGIC_1024); title[0]=0 as u8; elem_text(z, zl, "<book-title" as *u8, title, K_MAGIC_1024) 96 let fn: *u8 = sys_mmap(512); fn[0]=0 as u8; elem_text(z, zl, "<first-name" as *u8, fn, 512) 97 let ln: *u8 = sys_mmap(512); ln[0]=0 as u8; elem_text(z, zl, "<last-name" as *u8, ln, 512) 98 let author: *u8 = sys_mmap(K_MAGIC_1024) 99 var ao: i64 = 0; var fi: i64=0; while fn[fi]!=(0 as u8){ author[ao]=fn[fi]; ao=ao+1; fi=fi+1 } 100 if ao > 0 { author[ao]=0x20 as u8; ao=ao+1 } 101 var li: i64=0; while ln[li]!=(0 as u8){ author[ao]=ln[li]; ao=ao+1; li=li+1 } author[ao]=0 as u8 102 103 // <body> ... </body> only (skip <description> so title/author don't end up in the prose) 104 let bs: i64 = find_sub(z, zl, "<body" as *u8) 105 if bs < 0 { fb_p("FB2-FAIL no-body\n" as *u8); return 0-1 } 106 var bo: i64 = bs; while bo < zl { if z[bo]==(0x3e as u8) { bo=bo+1; break } bo=bo+1 } 107 var be: i64 = find_sub((z as i64 + bo) as *u8, zl - bo, "</body>" as *u8) 108 if be < 0 { be = zl } else { be = bo + be } 109 110 // ---- split the <body> into chapters on <section> (FB2 chapters); none -> 1 chapter (whole body, back-compat) ---- 111 let bptr: *u8 = (z as i64 + bo) as *u8 112 let blen: i64 = be - bo 113 let MAXCH: i64 = 512 114 let seg_s: *i64 = sys_mmap(MAXCH*8) as *i64 115 let seg_e: *i64 = sys_mmap(MAXCH*8) as *i64 116 let nseg: i64 = cs_split_on(bptr, blen, "<section" as *u8, seg_s, seg_e, MAXCH) 117 var start: i64 = 0 118 if nseg > 1 { start = 1 } // skip the pre-section front-matter (FB2 body title/epigraph) -> not a chapter 119 120 ensure_dir("knowledge/staging/media/reader" as *u8) 121 let dir: *u8 = sys_mmap(512) 122 var dl: i64 = er_cat(dir, 0, "knowledge/staging/media/reader/" as *u8); dl = er_cat(dir, dl, slug); dir[dl]=0 as u8 123 ensure_dir(dir) 124 125 let segtxt: *u8 = sys_mmap(FCAP); let segdec: *u8 = sys_mmap(FCAP) 126 let titles: *u8 = sys_mmap(MAXCH * 128) 127 let chchars: *i64 = sys_mmap(MAXCH*8) as *i64 128 var total: i64 = 0; var oc: i64 = 0 129 var ci2: i64 = start 130 while ci2 < nseg { 131 let s0: i64 = seg_s[ci2]; let s1: i64 = seg_e[ci2] 132 let stn: i64 = nx_html_to_text((bptr as i64 + s0) as *u8, s1 - s0, segtxt, FCAP) 133 let sdn: i64 = decode_numeric_refs(segtxt, stn, segdec, FCAP) 134 if sdn > 0 { 135 chchars[oc] = sdn; total = total + sdn 136 let cfp: *u8 = sys_mmap(K_MAGIC_1024); var cl2: i64 = er_cat(cfp,0,dir as *u8); cl2 = er_cat(cfp,cl2,"/chap" as *u8); cl2 = cs_catn(cfp,cl2,oc); cl2 = er_cat(cfp,cl2,".txt" as *u8); cfp[cl2]=0 as u8 137 let cfd2: i64 = sys_openat_wr(cfp, 0x1a4); if cfd2 >= 0 { sys_write(cfd2, segdec, sdn); sys_close(cfd2) } 138 let tp: *u8 = (titles as i64 + oc*128) as *u8; tp[0]=0 as u8 139 cs_extract_tag((bptr as i64 + s0) as *u8, s1 - s0, "<title" as *u8, "</title" as *u8, tp, 128) 140 if tp[0] == (0 as u8) { 141 if nseg <= 1 { var q: i64=0; while title[q]!=(0 as u8) { if q<127 {tp[q]=title[q]} q=q+1 } var ql: i64=q; if ql>127{ql=127} tp[ql]=0 as u8 } 142 else { var q2: i64 = er_cat(tp,0,"Chapter " as *u8); q2 = cs_catn(tp,q2,oc+1); tp[q2]=0 as u8 } 143 } 144 oc = oc + 1 145 } 146 ci2 = ci2 + 1 147 } 148 if oc == 0 { oc=1; chchars[0]=0; var q3: i64=0; while title[q3]!=(0 as u8){if q3<127{titles[q3]=title[q3]} q3=q3+1} var ql3: i64=q3; if ql3>127{ql3=127} titles[ql3]=0 as u8 } 149 150 let jp: *u8 = sys_mmap(K_MAGIC_1024) 151 var jl: i64 = er_cat(jp, 0, dir as *u8); jl = er_cat(jp, jl, "/book.json" as *u8); jp[jl]=0 as u8 152 let jfd: i64 = sys_openat_wr(jp, 0x1a4) 153 if jfd < 0 { fb_p("FB2-FAIL open-json\n" as *u8); return 0-1 } 154 fb_w(jfd, "{\"title\":\"" as *u8); er_wjson(jfd, title) 155 fb_w(jfd, "\",\"author\":\"" as *u8); er_wjson(jfd, author) 156 fb_w(jfd, "\",\"format\":\"fb2\",\"slug\":\"" as *u8); er_wjson(jfd, slug) 157 fb_w(jfd, "\",\"chapters\":[" as *u8) 158 var ce2: i64 = 0 159 while ce2 < oc { 160 if ce2 > 0 { fb_w(jfd, "," as *u8) } 161 fb_w(jfd, "{\"idx\":" as *u8); fb_n(jfd, ce2); fb_w(jfd, ",\"title\":\"" as *u8); er_wjson(jfd, (titles as i64 + ce2*128) as *u8) 162 fb_w(jfd, "\",\"file\":\"chap" as *u8); fb_n(jfd, ce2); fb_w(jfd, ".txt\",\"chars\":" as *u8); fb_n(jfd, chchars[ce2]) 163 fb_w(jfd, ",\"is_nav\":0,\"fn\":[]}" as *u8) 164 ce2 = ce2 + 1 165 } 166 fb_w(jfd, "],\"nchapters\":" as *u8); fb_n(jfd, oc) 167 fb_w(jfd, ",\"toc\":[" as *u8) 168 ce2 = 0 169 while ce2 < oc { 170 if ce2 > 0 { fb_w(jfd, "," as *u8) } 171 fb_w(jfd, "{\"depth\":0,\"title\":\"" as *u8); er_wjson(jfd, (titles as i64 + ce2*128) as *u8) 172 fb_w(jfd, "\",\"idx\":" as *u8); fb_n(jfd, ce2); fb_w(jfd, ",\"href\":\"\"}" as *u8) 173 ce2 = ce2 + 1 174 } 175 fb_w(jfd, "],\"ntoc\":" as *u8); fb_n(jfd, oc) 176 var tsrc: *u8 = "fb2-single" as *u8; if nseg > 1 { tsrc = "fb2-section" as *u8 } 177 fb_w(jfd, ",\"toc_source\":\"" as *u8); fb_w(jfd, tsrc) 178 fb_w(jfd, "\",\"assets\":[],\"nassets\":0,\"cover\":\"\"}" as *u8) 179 sys_close(jfd) 180 181 fb_p("FB2-BOOK title=\"" as *u8); fb_w(1, title); fb_p("\" author=\"" as *u8); fb_w(1, author) 182 fb_p("\" chapters=" as *u8); fb_pn(oc); fb_p(" chars=" as *u8); fb_pn(total); fb_p(" -> " as *u8); fb_p(dir); fb_p("\n" as *u8) 183 if total < 20 { return 0-1 } 184 return 0 185} 186 187func main(argc: i64, argv: *i64) -> i64 { 188 var path: *u8 = "knowledge/fixtures/nishi_fb2_fixture.fb2" as *u8 189 var slug: *u8 = "nishi_fb2_fixture" as *u8 190 if argc >= 3 { path = argv[1] as *u8; slug = argv[2] as *u8 } 191 let r: i64 = do_fb2(path, slug) 192 if r == 0 { fb_p("FB2-BOOK-OK\n" as *u8); sys_exit(0); return 0 } 193 fb_p("FB2-BOOK-FAIL\n" as *u8); sys_exit(1); return 1 194}