code wiki / wiki / nx_wiki_toc.nx

nx_wiki_toc.nx source

↩ module page · 330 lines · 14180 B

1// nx_wiki_toc.nx -- wiki R4: auto table-of-contents + heading anchor ids. 2// 3// COMPOSES: nx_syscalls (sys_mmap scratch). No markdown library needed -- it 4// reads ATX heading LINES directly from the markdown source (the same #/##/### 5// the renderer turns into <hN>), so the TOC and the rendered anchors share one 6// definition of "what a heading is". 7// 8// TWO halves: 9// 1. nx_wiki_toc_build(src,n) -> <nav class="nx-toc"><ol>...<li><a 10// href="#<anchor>">heading text</a></li>...</ol></nav> (one <li> per 11// ATX heading, in document order). Empty <nav> elided -> "" when a doc 12// has no headings. 13// 2. nx_wiki_toc_inject_ids(html,n) -> a COPY of rendered HTML with 14// id="<anchor>" added to every <hN ...> open tag that lacks one. The 15// anchor for an <hN> is derived from that tag's INNER TEXT, computed by 16// the SAME nx_wiki_toc_slugify the TOC links use -> the "#section-a" 17// links in (1) jump to the id="section-a" stamped here. 18// 19// ANCHOR RULE: lowercase the heading text; every run of non-[a-z0-9] bytes -> 20// a single '-'; trim leading/trailing '-'. Deterministic + collision-tolerant 21// (duplicate headings get duplicate ids -- browsers jump to the first, which 22// is the documented, acceptable behavior; a -2/-3 disambiguator is a follow-on). 23// 24// Hygiene: M1 out-params; M3 capped loops; M5 bounded indexing; M6 real 25// semantics; M7 named constants; M8 propagated verdicts. 26// 27// Status: V1 (wiki R4). 2026-06-15. license_tier: ORIGINAL 28import "nx_syscalls.nx" 29 30// ===== Sealed verdict surface (codes 2660-2679) ============================== 31const NX_WTOC_OK: i64 = 0 32const NX_WTOC_BAD_INPUT: i64 = 2660 33const NX_WTOC_OVERFLOW: i64 = 2661 34const NX_WTOC_LOOP_BUDGET: i64 = 2662 35 36// ===== Named sizing constants (M7) =========================================== 37const NX_WTOC_MAX_HEADINGS: i64 = 256 // headings per document cap 38const NX_WTOC_MAX_HEAD_LEN: i64 = 512 // single heading text cap 39const NX_WTOC_SCAN_BUDGET: i64 = 8000000 // per-doc byte-scan cap (M3) 40const NX_WTOC_ANCHOR_DASH: i64 = 0x2D // '-' 41 42const NX_WTOC_HASH: i64 = 0x23 // '#' 43const NX_WTOC_SP: i64 = 0x20 // ' ' 44const NX_WTOC_LF: i64 = 0x0A // '\n' 45const NX_WTOC_CR: i64 = 0x0D // '\r' 46const NX_WTOC_LT: i64 = 0x3C // '<' 47const NX_WTOC_GT: i64 = 0x3E // '>' 48 49// ===== anchor slugify ======================================================== 50// 51// Writes the anchor for heading text (src,n) into out (cap), NUL-terminated. 52// Returns anchor byte length (>=0, excluding NUL) or -verdict. Rule: lowercase 53// a..z, keep 0..9, everything else collapses to a single '-', no leading or 54// trailing '-'. Empty/symbol-only heading -> "" (length 0). 55func nx_wiki_toc_slugify(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 56 if (out as i64) == 0 { return 0 - NX_WTOC_BAD_INPUT } 57 if cap < 2 { return 0 - NX_WTOC_BAD_INPUT } 58 var o: i64 = 0 59 var i: i64 = 0 60 var pending_dash: i64 = 0 // emit a '-' before next kept char (if any emitted) 61 var iter: i64 = 0 62 while i < n { 63 if iter >= NX_WTOC_SCAN_BUDGET { return 0 - NX_WTOC_LOOP_BUDGET } 64 iter = iter + 1 65 let c: i64 = src[i] as i64 66 var lc: i64 = c 67 // uppercase A..Z -> lowercase 68 if c >= 0x41 { if c <= 0x5A { lc = c + 32 } } 69 var is_alnum: i64 = 0 70 if lc >= 0x61 { if lc <= 0x7A { is_alnum = 1 } } // a..z 71 if lc >= 0x30 { if lc <= 0x39 { is_alnum = 1 } } // 0..9 72 if is_alnum == 1 { 73 // flush a pending separator only if we've already emitted a char 74 if pending_dash == 1 { 75 if o > 0 { 76 if o + 1 >= cap { return 0 - NX_WTOC_OVERFLOW } 77 out[o] = NX_WTOC_ANCHOR_DASH as u8 78 o = o + 1 79 } 80 pending_dash = 0 81 } 82 if o + 1 >= cap { return 0 - NX_WTOC_OVERFLOW } 83 out[o] = lc as u8 84 o = o + 1 85 } 86 if is_alnum == 0 { pending_dash = 1 } 87 i = i + 1 88 } 89 out[o] = 0 as u8 90 return o 91} 92 93// ===== TOC build ============================================================= 94// 95// Scans src line by line; a line matching ^#{1,6} ' ' is a heading. Emits a 96// nav/ol list of links to each heading's anchor. Writes NUL-terminated HTML 97// into out (cap). Returns bytes written (>=0, excluding NUL) or -verdict. 98// When the doc has no headings, writes "" (length 0). 99 100func nx_wtoc_emit(out: *u8, off: i64, cap: i64, s: *u8) -> i64 { 101 var i: i64 = 0 102 while s[i] != (0 as u8) { 103 if off + i >= cap { return 0 - NX_WTOC_OVERFLOW } 104 out[off + i] = s[i] 105 i = i + 1 106 } 107 return off + i 108} 109func nx_wtoc_emit_bytes(out: *u8, off: i64, cap: i64, src: *u8, n: i64) -> i64 { 110 var i: i64 = 0 111 while i < n { 112 if i >= NX_WTOC_MAX_HEAD_LEN { return 0 - NX_WTOC_OVERFLOW } 113 if off + i >= cap { return 0 - NX_WTOC_OVERFLOW } 114 out[off + i] = src[i] 115 i = i + 1 116 } 117 return off + i 118} 119 120func nx_wiki_toc_build(src: *u8, n: i64, out: *u8, cap: i64, 121 out_used: *i64) -> i64 { 122 if (out_used as i64) == 0 { return 0 - NX_WTOC_BAD_INPUT } 123 out_used[0] = 0 124 if (out as i64) == 0 { return 0 - NX_WTOC_BAD_INPUT } 125 if cap < 64 { return 0 - NX_WTOC_BAD_INPUT } 126 if (src as i64) == 0 { out[0] = 0 as u8; return NX_WTOC_OK } 127 if n < 0 { return 0 - NX_WTOC_BAD_INPUT } 128 129 let anchor: *u8 = sys_mmap(NX_WTOC_MAX_HEAD_LEN + 2) 130 131 var o: i64 = 0 132 var headings: i64 = 0 133 var ls: i64 = 0 // current line start 134 var iter: i64 = 0 135 var emitted_open: i64 = 0 // have we written <nav>...<ol> yet? 136 137 while ls < n { 138 if iter >= NX_WTOC_MAX_HEADINGS { return 0 - NX_WTOC_LOOP_BUDGET } 139 iter = iter + 1 140 // find line end (index of '\n' or n) -- clean single-exit scan 141 var le: i64 = ls 142 var ldone: i64 = 0 143 while ldone == 0 { 144 if le >= n { ldone = 1 } 145 if ldone == 0 { 146 if src[le] == (NX_WTOC_LF as u8) { ldone = 1 } 147 if src[le] != (NX_WTOC_LF as u8) { le = le + 1 } 148 } 149 } 150 // count leading '#' -- stop AT the first non-'#' (leave h there) 151 var h: i64 = ls 152 var level: i64 = 0 153 var hdone: i64 = 0 154 while hdone == 0 { 155 if h >= le { hdone = 1 } 156 if hdone == 0 { 157 if src[h] == (NX_WTOC_HASH as u8) { level = level + 1; h = h + 1 } 158 if src[h] != (NX_WTOC_HASH as u8) { hdone = 1 } 159 } 160 } 161 // a heading needs 1..6 '#' then a single space 162 var is_heading: i64 = 0 163 var text_start: i64 = h 164 if level >= 1 { 165 if level <= 6 { 166 if h < le { 167 if src[h] == (NX_WTOC_SP as u8) { is_heading = 1; text_start = h + 1 } 168 } 169 } 170 } 171 if is_heading == 1 { 172 // text end = le, minus a trailing CR 173 var text_end: i64 = le 174 if text_end > text_start { 175 if src[text_end - 1] == (NX_WTOC_CR as u8) { text_end = text_end - 1 } 176 } 177 let text_ptr: *u8 = (src as i64 + text_start) as *u8 178 let text_n: i64 = text_end - text_start 179 let an: i64 = nx_wiki_toc_slugify(text_ptr, text_n, anchor, NX_WTOC_MAX_HEAD_LEN + 2) 180 if an > 0 { 181 if emitted_open == 0 { 182 o = nx_wtoc_emit(out, o, cap, "<nav class=\"nx-toc\" aria-label=\"On this page\"><ol>" as *u8) 183 if o < 0 { return o } 184 emitted_open = 1 185 } 186 o = nx_wtoc_emit(out, o, cap, "<li><a href=\"#" as *u8) 187 if o < 0 { return o } 188 o = nx_wtoc_emit_bytes(out, o, cap, anchor, an) 189 if o < 0 { return o } 190 o = nx_wtoc_emit(out, o, cap, "\">" as *u8) 191 if o < 0 { return o } 192 o = nx_wtoc_emit_bytes(out, o, cap, text_ptr, text_n) 193 if o < 0 { return o } 194 o = nx_wtoc_emit(out, o, cap, "</a></li>" as *u8) 195 if o < 0 { return o } 196 headings = headings + 1 197 } 198 } 199 // advance to next line 200 ls = le + 1 201 } 202 203 if emitted_open == 1 { 204 o = nx_wtoc_emit(out, o, cap, "</ol></nav>" as *u8) 205 if o < 0 { return o } 206 } 207 out[o] = 0 as u8 208 out_used[0] = o 209 return NX_WTOC_OK 210} 211 212// ===== Heading anchor-id injection =========================================== 213// 214// Reads rendered HTML (html,n); writes a COPY into out (cap) with id="<anchor>" 215// inserted into every <hN ...> open tag (N in 1..6) that does not already carry 216// an id=. The anchor is slugified from the tag's INNER TEXT (bytes between the 217// '>' that closes the open tag and the next '<'), tags stripped. Returns bytes 218// written (>=0, NUL-terminated) or -verdict. 219// 220// This is what makes the TOC links live: <h2>Section A</h2> becomes 221// <h2 id="section-a">Section A</h2>, the target of the TOC's "#section-a" link. 222 223func nx_wiki_toc_inject_ids(html: *u8, n: i64, out: *u8, cap: i64, 224 out_used: *i64) -> i64 { 225 if (out_used as i64) == 0 { return 0 - NX_WTOC_BAD_INPUT } 226 out_used[0] = 0 227 if (out as i64) == 0 { return 0 - NX_WTOC_BAD_INPUT } 228 if (html as i64) == 0 { return 0 - NX_WTOC_BAD_INPUT } 229 if cap < n + 1 { return 0 - NX_WTOC_OVERFLOW } 230 if n < 0 { return 0 - NX_WTOC_BAD_INPUT } 231 232 let anchor: *u8 = sys_mmap(NX_WTOC_MAX_HEAD_LEN + 2) 233 234 var i: i64 = 0 235 var o: i64 = 0 236 var iter: i64 = 0 237 while i < n { 238 if iter >= NX_WTOC_SCAN_BUDGET { return 0 - NX_WTOC_LOOP_BUDGET } 239 iter = iter + 1 240 var handled: i64 = 0 241 // detect "<h" then a digit 1..6 at i 242 if i + 2 < n { 243 if html[i] == (NX_WTOC_LT as u8) { 244 let c1: i64 = html[i + 1] as i64 245 let c2: i64 = html[i + 2] as i64 246 var is_h: i64 = 0 247 if c1 == 0x68 { if c2 >= 0x31 { if c2 <= 0x36 { is_h = 1 } } } // 'h' '1'..'6' 248 if is_h == 1 { 249 // find end of this open tag '>' -- clean single-exit scan 250 var tag_end: i64 = i + 2 251 var found_gt: i64 = 0 252 var gdone: i64 = 0 253 while gdone == 0 { 254 if tag_end >= n { gdone = 1 } 255 if tag_end - i >= NX_WTOC_MAX_HEAD_LEN { gdone = 1 } 256 if gdone == 0 { 257 if html[tag_end] == (NX_WTOC_GT as u8) { found_gt = 1; gdone = 1 } 258 if found_gt == 0 { tag_end = tag_end + 1 } 259 } 260 } 261 if found_gt == 1 { 262 // does the open tag already contain id= ? (scan i..tag_end) 263 var has_id: i64 = 0 264 var p: i64 = i + 2 265 while p < tag_end - 2 { 266 if has_id == 0 { 267 if html[p] == (0x69 as u8) { // 'i' 268 if html[p + 1] == (0x64 as u8) { // 'd' 269 if html[p + 2] == (0x3D as u8) { has_id = 1 } // '=' 270 } 271 } 272 } 273 p = p + 1 274 } 275 // inner text = bytes after '>' up to next '<' -- clean scan 276 let inner_start: i64 = tag_end + 1 277 var inner_end: i64 = inner_start 278 var idone: i64 = 0 279 while idone == 0 { 280 if inner_end >= n { idone = 1 } 281 if inner_end - inner_start >= NX_WTOC_MAX_HEAD_LEN { idone = 1 } 282 if idone == 0 { 283 if html[inner_end] == (NX_WTOC_LT as u8) { idone = 1 } 284 if html[inner_end] != (NX_WTOC_LT as u8) { inner_end = inner_end + 1 } 285 } 286 } 287 let inner_ptr: *u8 = (html as i64 + inner_start) as *u8 288 let inner_n: i64 = inner_end - inner_start 289 let an: i64 = nx_wiki_toc_slugify(inner_ptr, inner_n, anchor, NX_WTOC_MAX_HEAD_LEN + 2) 290 // copy the open tag verbatim up to (but not including) '>' 291 var t: i64 = i 292 while t < tag_end { 293 if o >= cap { return 0 - NX_WTOC_OVERFLOW } 294 out[o] = html[t] 295 o = o + 1 296 t = t + 1 297 } 298 // inject id="<anchor>" before '>' (only if absent + non-empty) 299 if has_id == 0 { 300 if an > 0 { 301 o = nx_wtoc_emit(out, o, cap, " id=\"" as *u8) 302 if o < 0 { return o } 303 o = nx_wtoc_emit_bytes(out, o, cap, anchor, an) 304 if o < 0 { return o } 305 if o >= cap { return 0 - NX_WTOC_OVERFLOW } 306 out[o] = (0x22 as u8) // '"' 307 o = o + 1 308 } 309 } 310 // now write the '>' and resume after it 311 if o >= cap { return 0 - NX_WTOC_OVERFLOW } 312 out[o] = NX_WTOC_GT as u8 313 o = o + 1 314 i = tag_end + 1 315 handled = 1 316 } 317 } 318 } 319 } 320 if handled == 0 { 321 if o >= cap { return 0 - NX_WTOC_OVERFLOW } 322 out[o] = html[i] 323 o = o + 1 324 i = i + 1 325 } 326 } 327 out[o] = 0 as u8 328 out_used[0] = o 329 return NX_WTOC_OK 330}