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}