code wiki / _hdl_build / nx_epub_nav.nx
nx_epub_nav.nx source
↩ module page · 201 lines · 9047 B
1// nx_epub_nav.nx -- Parses EPUB navigation documents into a flat, depth-tagged list of chapters for accurate table of contents rendering.
2const K_MAGIC_2048: i64 = 2048
3// nx_epub_nav.nx -- SOVEREIGN EPUB navigation-document parser (Reader-arc R-TOC).
4//
5// The "table of contents chapters wrong" root: nx_epub_book builds chapters[] from the OPF <spine> (reading
6// order), which is NOT the table of contents. The real TOC lives in a separate, usually HIERARCHICAL navigation
7// document: EPUB3 nav.xhtml (<nav epub:type="toc"> ... nested <ol>/<li>/<a>) or EPUB2 toc.ncx (nested <navPoint>).
8// This library parses EITHER into a flat, DEPTH-TAGGED entry list {depth, label, href} preserving nesting, so a
9// reader can render the true nested TOC and jump to the right chapter. Spine stays the reading order; this is the
10// navigation tree on top of it.
11//
12// Helpers are nav_-prefixed so this composes into nx_epub_book.nx (which has its own find_sub/attr_val) with NO
13// symbol clash. Pure Nishi + the caller's arenas; no syscalls of its own. license_tier: ORIGINAL
14
15func nav_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
16
17// does buf[i..] (within [0,n)) start with literal s?
18func nav_at(buf: *u8, i: i64, n: i64, s: *u8) -> i64 {
19 var k: i64 = 0
20 while s[k] != (0 as u8) {
21 if i + k >= n { return 0 }
22 if buf[i+k] != s[k] { return 0 }
23 k = k + 1
24 }
25 return 1
26}
27
28// first index >= start in buf[0,n) where needle matches; -1 if none.
29func nav_find_from(buf: *u8, n: i64, start: i64, needle: *u8) -> i64 {
30 let nl: i64 = nav_slen(needle)
31 if nl == 0 { return 0-1 }
32 var i: i64 = start
33 if i < 0 { i = 0 }
34 while i + nl <= n {
35 if nav_at(buf, i, n, needle) == 1 { return i }
36 i = i + 1
37 }
38 return 0-1
39}
40
41// '<a' followed by whitespace (a real anchor open tag), not '<abbr'/'<article'.
42func nav_is_anchor(buf: *u8, i: i64, n: i64) -> i64 {
43 if i + 2 >= n { return 0 }
44 if buf[i] != (0x3c as u8) { return 0 } // '<'
45 if buf[i+1] != (0x61 as u8) { return 0 } // 'a'
46 let c: i64 = buf[i+2] as i64
47 if c == 0x20 { return 1 } // space
48 if c == 0x09 { return 1 } // tab
49 if c == 0x0a { return 1 } // newline
50 if c == 0x0d { return 1 } // cr
51 return 0
52}
53
54// find attr (e.g. `href="`) within buf[start,limit), copy value until the next '"' into out; ret length or -1.
55func nav_attr_in(buf: *u8, start: i64, limit: i64, attr: *u8, out: *u8, outcap: i64) -> i64 {
56 let al: i64 = nav_slen(attr)
57 var i: i64 = start
58 var at: i64 = 0-1
59 while i + al <= limit {
60 var k: i64 = 0
61 var hit: i64 = 1
62 while k < al { if buf[i+k] != attr[k] { hit = 0; k = al } else { k = k + 1 } }
63 if hit == 1 { at = i; i = limit } else { i = i + 1 }
64 }
65 if at < 0 { return 0-1 }
66 var s: i64 = at + al
67 var o: i64 = 0
68 while s < limit { if buf[s] == (0x22 as u8) { break } if o < outcap-1 { out[o] = buf[s]; o = o + 1 } s = s + 1 }
69 out[o] = 0 as u8
70 return o
71}
72
73// copy text buf[start,end) into out, trimming leading/trailing ASCII whitespace; NUL-term; ret length.
74func nav_copy_trim(buf: *u8, start: i64, end: i64, out: *u8, outcap: i64) -> i64 {
75 var s: i64 = start
76 while s < end { let c: i64 = buf[s] as i64; if c==0x20 { s=s+1 } else { if c==0x0a { s=s+1 } else { if c==0x09 { s=s+1 } else { if c==0x0d { s=s+1 } else { break } } } } }
77 var e: i64 = end
78 while e > s { let c: i64 = buf[e-1] as i64; if c==0x20 { e=e-1 } else { if c==0x0a { e=e-1 } else { if c==0x09 { e=e-1 } else { if c==0x0d { e=e-1 } else { break } } } } }
79 var o: i64 = 0
80 var i: i64 = s
81 while i < e { if o < outcap-1 { out[o] = buf[i]; o = o + 1 } i = i + 1 }
82 out[o] = 0 as u8
83 return o
84}
85
86// store one entry into the caller arenas at slot cnt. larena/harena are byte arenas; loffs/hoffs the per-entry
87// start offsets; aoff[0]=label arena cursor, aoff[1]=href arena cursor (mutated). returns 0.
88func nav_store(depths: *i64, larena: *u8, loffs: *i64, harena: *u8, hoffs: *i64, aoff: *i64, cnt: i64, d: i64, label: *u8, href: *u8) -> i64 {
89 depths[cnt] = d
90 var la: i64 = aoff[0]
91 loffs[cnt] = la
92 var i: i64 = 0
93 while label[i] != (0 as u8) { larena[la] = label[i]; la = la + 1; i = i + 1 }
94 larena[la] = 0 as u8; la = la + 1
95 aoff[0] = la
96 var ha: i64 = aoff[1]
97 hoffs[cnt] = ha
98 i = 0
99 while href[i] != (0 as u8) { harena[ha] = href[i]; ha = ha + 1; i = i + 1 }
100 harena[ha] = 0 as u8; ha = ha + 1
101 aoff[1] = ha
102 return 0
103}
104
105// ===== EPUB3 nav.xhtml: <nav epub:type="toc"> nested <ol>/<li>/<a href>label</a> =====
106// Writes depth-tagged entries; depth 0 = top-level. Returns count (<= cap).
107func nav_parse_xhtml(buf: *u8, n: i64, depths: *i64, larena: *u8, loffs: *i64, harena: *u8, hoffs: *i64, cap: i64) -> i64 {
108 var tpos: i64 = nav_find_from(buf, n, 0, "epub:type=\"toc\"" as *u8)
109 if tpos < 0 { tpos = nav_find_from(buf, n, 0, "epub:type='toc'" as *u8) }
110 if tpos < 0 { return 0 }
111 var i: i64 = nav_find_from(buf, n, tpos, "<ol" as *u8)
112 if i < 0 { return 0 }
113 let label: *u8 = sys_mmap(K_MAGIC_2048)
114 let href: *u8 = sys_mmap(K_MAGIC_2048)
115 let aoff: *i64 = sys_mmap(16) as *i64
116 aoff[0] = 0; aoff[1] = 0
117 var oldepth: i64 = 0
118 var cnt: i64 = 0
119 while i < n {
120 var adv: i64 = 1
121 if nav_at(buf, i, n, "<ol" as *u8) == 1 { oldepth = oldepth + 1; adv = 3 }
122 else { if nav_at(buf, i, n, "</ol>" as *u8) == 1 {
123 oldepth = oldepth - 1; adv = 5
124 if oldepth <= 0 { i = n }
125 }
126 else { if nav_is_anchor(buf, i, n) == 1 {
127 // tag end '>'
128 var te: i64 = i
129 while te < n { if buf[te] == (0x3e as u8) { break } te = te + 1 }
130 let hn: i64 = nav_attr_in(buf, i, te, "href=\"" as *u8, href, K_MAGIC_2048)
131 // label text between te+1 and next "</a>"
132 let ae: i64 = nav_find_from(buf, n, te, "</a>" as *u8)
133 var lend: i64 = ae
134 if lend < 0 { lend = te + 1 }
135 nav_copy_trim(buf, te + 1, lend, label, K_MAGIC_2048)
136 if hn > 0 { if cnt < cap {
137 var d: i64 = oldepth - 1
138 if d < 0 { d = 0 }
139 nav_store(depths, larena, loffs, harena, hoffs, aoff, cnt, d, label, href)
140 cnt = cnt + 1
141 } }
142 if ae >= 0 { adv = (ae + 4) - i } else { adv = 2 }
143 } } }
144 i = i + adv
145 }
146 return cnt
147}
148
149// ===== EPUB2 toc.ncx: <navMap> nested <navPoint><navLabel><text>L</text></navLabel><content src="H"/> =====
150// Writes depth-tagged entries; depth 0 = top-level. Returns count (<= cap).
151func ncx_parse(buf: *u8, n: i64, depths: *i64, larena: *u8, loffs: *i64, harena: *u8, hoffs: *i64, cap: i64) -> i64 {
152 var s: i64 = nav_find_from(buf, n, 0, "<navMap" as *u8)
153 if s < 0 { return 0 }
154 let label: *u8 = sys_mmap(K_MAGIC_2048)
155 let href: *u8 = sys_mmap(K_MAGIC_2048)
156 let aoff: *i64 = sys_mmap(16) as *i64
157 aoff[0] = 0; aoff[1] = 0
158 var i: i64 = s
159 var npdepth: i64 = 0
160 var cnt: i64 = 0
161 var pending: i64 = 0
162 var have_l: i64 = 0
163 var have_h: i64 = 0
164 var pend_depth: i64 = 0
165 while i < n {
166 var adv: i64 = 1
167 if nav_at(buf, i, n, "<navPoint" as *u8) == 1 {
168 pend_depth = npdepth
169 npdepth = npdepth + 1
170 pending = 1; have_l = 0; have_h = 0
171 adv = 9
172 }
173 else { if nav_at(buf, i, n, "</navPoint>" as *u8) == 1 { npdepth = npdepth - 1; adv = 11; if npdepth < 0 { i = n } }
174 else { if nav_at(buf, i, n, "</navMap>" as *u8) == 1 { i = n }
175 else { if pending == 1 { if have_l == 0 { if nav_at(buf, i, n, "<text>" as *u8) == 1 {
176 let ls: i64 = i + 6
177 let le: i64 = nav_find_from(buf, n, ls, "</text>" as *u8)
178 var lend: i64 = le
179 if lend < 0 { lend = ls }
180 nav_copy_trim(buf, ls, lend, label, K_MAGIC_2048)
181 have_l = 1
182 if le >= 0 { adv = (le + 7) - i } else { adv = 6 }
183 } } }
184 if adv == 1 { if pending == 1 { if have_h == 0 { if nav_at(buf, i, n, "<content" as *u8) == 1 {
185 var te: i64 = i
186 while te < n { if buf[te] == (0x3e as u8) { break } te = te + 1 }
187 nav_attr_in(buf, i, te, "src=\"" as *u8, href, K_MAGIC_2048)
188 have_h = 1
189 adv = (te + 1) - i
190 } } } }
191 } } }
192 // emit when this navPoint has both label + href
193 if pending == 1 { if have_l == 1 { if have_h == 1 { if cnt < cap {
194 nav_store(depths, larena, loffs, harena, hoffs, aoff, cnt, pend_depth, label, href)
195 cnt = cnt + 1
196 pending = 0
197 } } } }
198 i = i + adv
199 }
200 return cnt
201}