code wiki / _hdl_build / nx_chapter_split.nx

nx_chapter_split.nx source

↩ module page · 201 lines · 11561 B

1// nx_chapter_split.nx -- SHARED chapter-splitting for the Kindle decoders (nx_mobi_book + nx_kf8_book). Splits the 2// decoded HTML on <mbp:pagebreak> into real chapters -> writes chap<i>.txt + extracts per-chapter <h1> titles, and 3// writes the chapters[]/toc[] JSON straight to the open book.json fd. The zero-JS reader already renders 4// chapters[]/toc[] (the EPUB path), so a multi-chapter book.json = a real navigable TOC for free. No pagebreak -> 5// 1 chapter titled with the book title (back-compat). license_tier: ORIGINAL 6import "nx_syscalls.nx" 7import "nx_html_to_text.nx" 8import "nx_charset.nx" // MOJIBAKE FIX: nx_charset_repair_utf8 -- stray Windows-1252 bytes -> clean UTF-8 9const CS_MAGIC_1252: i64 = 1252 10const CS_MAGIC_1024: i64 = 1024 11 12const CS_CAP: i64 = 16777216 13 14func cs_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 15func cs_w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, cs_slen(s)); return 0 } 16func cs_wn(fd: i64, v: i64) -> i64 { let b: *u8=sys_mmap(24); var m: i64=v; if m<0{m=0-m;sys_write(fd,"-" as *u8,1)}; let t: *u8=sys_mmap(24); 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{b[i]=t[k-1-i];i=i+1}; sys_write(fd,b,k); return 0 } 17func cs_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){d[o+i]=s[i];i=i+1} return o+i } 18func cs_catn(d: *u8, o0: i64, v: i64) -> i64 { var o: i64=o0; let t: *u8=sys_mmap(24); 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{d[o+i]=t[k-1-i];i=i+1} return o+k } 19func cs_find_sub(hay: *u8, hl: i64, needle: *u8) -> i64 { 20 let nl: i64 = cs_slen(needle); if nl == 0 { return 0-1 } 21 var i: i64 = 0 22 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 } 23 return 0-1 24} 25// JSON string emit. MOJIBAKE FIX: repair each chapter title to clean UTF-8 first (idempotent on valid UTF-8). 26func cs_wjson(fd: i64, s: *u8) -> i64 { 27 var sl: i64 = 0; while s[sl] != (0 as u8) { sl = sl + 1 } 28 let rcap: i64 = sl * 3 + 16 29 let rep: *u8 = sys_mmap(rcap) 30 let rn: i64 = nx_charset_repair_utf8(s, sl, rep, rcap) 31 var i: i64 = 0 32 while i < rn { 33 let c: i64 = rep[i] as i64 34 if c == 0x22 { sys_write(fd, "\\\"" as *u8, 2) } 35 else { if c == 0x5c { sys_write(fd, "\\\\" as *u8, 2) } 36 else { if c < 0x20 { sys_write(fd, " " as *u8, 1) } 37 else { sys_write(fd, (rep as i64 + i) as *u8, 1) } } } 38 i = i + 1 39 } 40 sys_munmap(rep, rcap) 41 return 0 42} 43func cs_utf8_put(out: *u8, o: i64, cp: i64) -> i64 { 44 if cp < 0x80 { out[o] = cp as u8; return o + 1 } 45 if cp < 0x800 { out[o] = (0xC0 | (cp >> 6)) as u8; out[o+1] = (0x80 | (cp & 0x3F)) as u8; return o + 2 } 46 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 } 47 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 48} 49func cs_decode_numeric_refs(txt: *u8, n: i64, out: *u8, outcap: i64) -> i64 { 50 var i: i64 = 0 51 var o: i64 = 0 52 while i < n { 53 let c: i64 = txt[i] as i64 54 var done: i64 = 0 55 if c == 0x26 { if i + 1 < n { if (txt[i+1] as i64) == 0x23 { 56 var j: i64 = i + 2 57 var hex: i64 = 0 58 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 } } } 59 var cp: i64 = 0 60 var ndig: i64 = 0 61 var scan: i64 = 1 62 while scan == 1 { 63 if j >= n { scan = 0 } else { 64 let dc: i64 = txt[j] as i64 65 var dv: i64 = 0-1 66 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 } } } 67 else { if dc >= 48 { if dc <= 57 { dv = dc - 48 } } } 68 if dv < 0 { scan = 0 } else { if hex == 1 { cp = cp * 16 + dv } else { cp = cp * 10 + dv } ndig = ndig + 1; j = j + 1 } 69 } 70 } 71 if ndig > 0 { if j < n { if (txt[j] as i64) == 0x3b { if cp > 0 { if cp <= 0x10FFFF { if o + 4 < outcap { o = cs_utf8_put(out, o, cp) } i = j + 1; done = 1 } } } } } 72 } } } 73 if done == 0 { if o < outcap - 1 { out[o] = txt[i]; o = o + 1 } i = i + 1 } 74 } 75 out[o] = 0 as u8 76 return o 77} 78// extract the text of the first <opentag>..</closetag> (inner tags stripped) from html[0..hl) into out; ret len 79// (0 if absent). Generic so callers pick the heading element: MOBI/EPUB use <h1>, FB2 uses <title>. 80func cs_extract_tag(html: *u8, hl: i64, opentag: *u8, closetag: *u8, out: *u8, cap: i64) -> i64 { 81 out[0] = 0 as u8 82 let h1: i64 = cs_find_sub(html, hl, opentag) 83 if h1 < 0 { return 0 } 84 var gt: i64 = 0-1; var p: i64 = h1 85 while p < hl { if html[p] == (0x3e as u8) { gt = p; p = hl } else { p = p + 1 } } 86 if gt < 0 { return 0 } 87 let cs0: i64 = gt + 1 88 let he: i64 = cs_find_sub((html as i64 + cs0) as *u8, hl - cs0, closetag) 89 var ce: i64 = hl 90 if he >= 0 { ce = cs0 + he } 91 var o: i64 = 0; var i: i64 = cs0; var intag: i64 = 0 92 while i < ce { 93 let c: i64 = html[i] as i64 94 if c == 0x3c { intag = 1 } else { if c == 0x3e { intag = 0 } else { if intag == 0 { if o < cap-1 { out[o]=html[i] as u8; o=o+1 } } } } 95 i = i + 1 96 } 97 out[o] = 0 as u8 98 return o 99} 100 101// Chapter heading: try <h1>, then <h2>, then <h3> (inner inline tags stripped by cs_extract_tag). First non-empty 102// wins. TITLE FIX: many Kindle/MOBI books head chapters with <h2>/<h3>, not <h1> -- reading only <h1> is why TOCs 103// degraded to a bare "Chapter N". ret length (0 if no heading found). 104func cs_extract_heading(html: *u8, hl: i64, out: *u8, cap: i64) -> i64 { 105 var t: i64 = cs_extract_tag(html, hl, "<h1" as *u8, "</h1" as *u8, out, cap) 106 if t <= 0 { t = cs_extract_tag(html, hl, "<h2" as *u8, "</h2" as *u8, out, cap) } 107 if t <= 0 { t = cs_extract_tag(html, hl, "<h3" as *u8, "</h3" as *u8, out, cap) } 108 return t 109} 110 111// split html[0..hl) into segments BEFORE each `marker`; returns raw segment count (caller skips empty segments). 112func cs_split_on(html: *u8, hl: i64, marker: *u8, seg_s: *i64, seg_e: *i64, maxch: i64) -> i64 { 113 let ml: i64 = cs_slen(marker) 114 var nseg: i64 = 0 115 var sc: i64 = 0; var prevp: i64 = 0 116 while sc < hl { 117 let pb: i64 = cs_find_sub((html as i64 + sc) as *u8, hl - sc, marker) 118 if pb < 0 { sc = hl } else { 119 let ap: i64 = sc + pb 120 if ap > prevp { if nseg < maxch { seg_s[nseg]=prevp; seg_e[nseg]=ap; nseg=nseg+1 } } 121 prevp = ap 122 sc = ap + ml 123 } 124 } 125 if hl > prevp { if nseg < maxch { seg_s[nseg]=prevp; seg_e[nseg]=hl; nseg=nseg+1 } } 126 if nseg == 0 { seg_s[0]=0; seg_e[0]=hl; nseg=1 } 127 return nseg 128} 129 130// Split `html` -> write chap<i>.txt into `dir` + write the chapters[]/toc[] JSON to `jfd` (no leading/trailing comma; 131// caller wraps). Prefers explicit <mbp:pagebreak>; if none, falls back to top-level <h1> headings (2+ -> chapters). 132// Skips empty segments (e.g. the bit before the first heading). total_out[0]=total chars. `srctag` prefixes 133// toc_source. Returns the chapter count. 134func chapter_split(html: *u8, htmllen: i64, dir: *u8, booktitle: *u8, jfd: i64, total_out: *i64, srctag: *u8) -> i64 { 135 let MAXCH: i64 = 512 136 let seg_s: *i64 = sys_mmap(MAXCH*8) as *i64 137 let seg_e: *i64 = sys_mmap(MAXCH*8) as *i64 138 var nseg: i64 = cs_split_on(html, htmllen, "<mbp:pagebreak" as *u8, seg_s, seg_e, MAXCH) 139 var splitkind: *u8 = "pagebreak" as *u8 140 var is_single: i64 = 0 141 if nseg <= 1 { 142 let h1raw: i64 = cs_split_on(html, htmllen, "<h1" as *u8, seg_s, seg_e, MAXCH) // no pagebreaks -> try headings 143 if h1raw >= 3 { nseg = h1raw; splitkind = "heading" as *u8 } // (leading empty + 2 real) -> 2+ chapters 144 else { 145 let h2raw: i64 = cs_split_on(html, htmllen, "<h2" as *u8, seg_s, seg_e, MAXCH) // TITLE FIX: many Kindle books head chapters with <h2> 146 if h2raw >= 3 { nseg = h2raw; splitkind = "heading-h2" as *u8 } 147 else { seg_s[0]=0; seg_e[0]=htmllen; nseg=1; splitkind = "single" as *u8; is_single = 1 } 148 } 149 } 150 151 let segtxt: *u8 = sys_mmap(CS_CAP); let segdec: *u8 = sys_mmap(CS_CAP); let segrep: *u8 = sys_mmap(CS_CAP) 152 let titles: *u8 = sys_mmap(MAXCH * 128) 153 let chchars: *i64 = sys_mmap(MAXCH*8) as *i64 154 var total: i64 = 0 155 var oc: i64 = 0 // output chapter index (skips empty segments) 156 var ci2: i64 = 0 157 while ci2 < nseg { 158 let s0: i64 = seg_s[ci2]; let s1: i64 = seg_e[ci2] 159 let stn: i64 = nx_html_to_text((html as i64 + s0) as *u8, s1 - s0, segtxt, CS_CAP) 160 let sdn0: i64 = cs_decode_numeric_refs(segtxt, stn, segdec, CS_CAP) 161 // MOJIBAKE FIX: repair stray Windows-1252 bytes -> clean UTF-8 before writing the chapter file. 162 let sdn: i64 = nx_charset_repair_utf8(segdec, sdn0, segrep, CS_CAP) 163 if sdn > 0 { 164 chchars[oc] = sdn; total = total + sdn 165 let cfp: *u8 = sys_mmap(CS_MAGIC_1024); var cl2: i64 = cs_cat(cfp,0,dir); cl2 = cs_cat(cfp,cl2,"/chap" as *u8); cl2 = cs_catn(cfp,cl2,oc); cl2 = cs_cat(cfp,cl2,".txt" as *u8); cfp[cl2]=0 as u8 166 let cfd2: i64 = sys_openat_wr(cfp, 0x1a4); if cfd2 >= 0 { sys_write(cfd2, segrep, sdn); sys_close(cfd2) } 167 let tp: *u8 = (titles as i64 + oc*128) as *u8; tp[0]=0 as u8 168 cs_extract_heading((html as i64 + s0) as *u8, s1 - s0, tp, 128) 169 if tp[0] == (0 as u8) { 170 if is_single == 1 { var q: i64=0; while booktitle[q]!=(0 as u8) { if q < 127 { tp[q]=booktitle[q] } q=q+1 } var ql: i64=q; if ql>127 {ql=127} tp[ql]=0 as u8 } 171 else { var q2: i64 = cs_cat(tp,0,"Chapter " as *u8); q2 = cs_catn(tp,q2,oc+1); tp[q2]=0 as u8 } 172 } 173 oc = oc + 1 174 } 175 ci2 = ci2 + 1 176 } 177 if oc == 0 { oc = 1; chchars[0]=0; var q3: i64=0; while booktitle[q3]!=(0 as u8){ if q3<127 {titles[q3]=booktitle[q3]} q3=q3+1 } var ql3: i64=q3; if ql3>127{ql3=127} titles[ql3]=0 as u8 } 178 179 cs_w(jfd, "\"chapters\":[" as *u8) 180 var ce2: i64 = 0 181 while ce2 < oc { 182 if ce2 > 0 { cs_w(jfd, "," as *u8) } 183 cs_w(jfd, "{\"idx\":" as *u8); cs_wn(jfd, ce2); cs_w(jfd, ",\"title\":\"" as *u8); cs_wjson(jfd, (titles as i64 + ce2*128) as *u8) 184 cs_w(jfd, "\",\"file\":\"chap" as *u8); cs_wn(jfd, ce2); cs_w(jfd, ".txt\",\"chars\":" as *u8); cs_wn(jfd, chchars[ce2]) 185 cs_w(jfd, ",\"is_nav\":0,\"fn\":[]}" as *u8) 186 ce2 = ce2 + 1 187 } 188 cs_w(jfd, "],\"nchapters\":" as *u8); cs_wn(jfd, oc) 189 cs_w(jfd, ",\"toc\":[" as *u8) 190 ce2 = 0 191 while ce2 < oc { 192 if ce2 > 0 { cs_w(jfd, "," as *u8) } 193 cs_w(jfd, "{\"depth\":0,\"title\":\"" as *u8); cs_wjson(jfd, (titles as i64 + ce2*128) as *u8) 194 cs_w(jfd, "\",\"idx\":" as *u8); cs_wn(jfd, ce2); cs_w(jfd, ",\"href\":\"\"}" as *u8) 195 ce2 = ce2 + 1 196 } 197 cs_w(jfd, "],\"ntoc\":" as *u8); cs_wn(jfd, oc) 198 cs_w(jfd, ",\"toc_source\":\"" as *u8); cs_w(jfd, srctag); cs_w(jfd, "-" as *u8); cs_w(jfd, splitkind); cs_w(jfd, "\"" as *u8) 199 total_out[0] = total 200 return oc 201}