code wiki / wiki / nx_wiki_doc_render.nx

nx_wiki_doc_render.nx source

↩ module page · 903 lines · 52572 B

1// nx_wiki_doc_render.nx -- markdown source -> HTTP-ready HTML body. 2// 3// Composes existing substrate primitives (per the duplicate-primitives 4// audit inp NISHI_PRODUCT_OPS_PLATFORM §4): 5// markdown_block CommonMark subset block-level parser 6// markdown_inline CommonMark subset inline parser 7// html_escape HTML-unsafe-byte escape 8// 9// Adds wiki-specific structural rendering on top: 10// - NISHIDOC_STANDARD section sidebar TOC (sealed: §1 - §17) 11// - [N] / [I] / [R] paragraph-tag CSS class injection 12// - [[wikilink-target]] cross-reference resolution 13// - Page chrome (header, footer, sidebar) 14// 15// Status: V1 SEED. 2026-05-27. 16// WINNER-TIER: BASELINE-C provisional (no incumbent matches the 17// NISHIDOC-aware rendering + sealed cross-reference 18// resolution; provisional pending paired render-speed 19// bench vs pandoc + commonmark.js) 20// INCUMBENTS: pandoc (Haskell), commonmark.js, markdown-it, 21// GitHub's gh-markdown, MediaWiki Parsoid 22// NUMBERS: V1 ships the render path; throughput vs pandoc on 23// 100 docs / 1MB total pending paired bench 24// GAP: pandoc covers more markdown features (footnotes, 25// definition lists, raw HTML passthrough); V1 covers 26// the subset NISHIDOC_STANDARD uses + adds the 27// NISHIDOC-specific structural rendering pandoc 28// doesn't ship 29// PLAN: M-next paired render-speed bench vs pandoc; 30// extend block syntax to footnotes when an actual 31// doc needs them 32// EXEMPTION REASON: n/a; provisional pending measurement 33// 34// Hygiene-standard compliance per NISHI_CODE_HYGIENE_STANDARD.md: 35// M1 no NULL creation: callers receive verdict + buffer-offset 36// via out-param; no *T returns 37// M2 overflow safety: all length arithmetic checked against caller cap 38// M3 bounded loops: every while has an explicit max-iter cap 39// M4 alloc safety: sys_mmap calls use sized named constants 40// M5 index safety: every src[i] access bounded by len check 41// M6 no pretend stubs: every fn delivers documented semantics 42// OR returns NX_WIKI_DOC_RENDER_NOT_IMPLEMENTED 43// M7 no magic numbers: all >=8 literals are named constants 44// M8 no silent errors: every helper return checked + propagated 45 46import "nx_syscalls.nx" 47import "markdown_block.nx" 48import "markdown_inline.nx" 49import "html_escape.nx" 50import "hub/nx_nishi_page_emit.nx" 51// Cite-render organ (inline license-aware supporting-source cards) + its 52// sovereign card CSS. Pulled in so the served page pipeline can run the 53// [[cite:<cid>]] pass alongside the [[wikilink]] preprocessor and ship the 54// card CSS in the page chrome. Its transitive chain (license/archive/seg_store) 55// is path-deduped by the resolver -- no duplicate-symbol (the daemon already 56// links these via the archive router). 57import "wiki/nx_wiki_cite_render.nx" 58// wiki R4 auto-TOC + heading anchor ids, wired into the standalone render path 59// too (the served handler nx_wiki_doc_handle wires these + backlinks itself). 60// nx_wiki_toc only depends on nx_syscalls; the resolver canonicalizes the path 61// so this dedupes with the handler's import (no rc6 double-import). 62import "nx_wiki_toc.nx" 63 64// ===== Sealed verdict surface ================================================= 65const NX_WIKI_DOC_OK: i64 = 0 66const NX_WIKI_DOC_BAD_INPUT: i64 = 1 67const NX_WIKI_DOC_OUTPUT_OVERFLOW: i64 = 2 68const NX_WIKI_DOC_PARSE_FAILED: i64 = 3 69const NX_WIKI_DOC_BROKEN_WIKILINK: i64 = 4 70const NX_WIKI_DOC_LOOP_BUDGET_EXCEEDED: i64 = 5 71const NX_WIKI_DOC_RENDER_NOT_IMPLEMENTED: i64 = 6 72 73// ===== Named sizing constants (M7) ================================================= 74const NX_WIKI_DOC_MAX_SECTIONS: i64 = 32 // NISHIDOC §1-§17 + headroom 75const NX_WIKI_DOC_MAX_TITLE_LEN: i64 = 256 76const NX_WIKI_DOC_MAX_WIKILINKS: i64 = 256 77const NX_WIKI_DOC_MAX_WIKILINK_LEN: i64 = 128 78const NX_WIKI_DOC_MAX_PARSE_ITER: i64 = 100000 // hard cap per render call 79const NX_WIKI_DOC_HTML_SCRATCH_BYTES: i64 = 32 // for itoa + small temp writes 80const NX_WIKI_DOC_TITLE_SCRATCH_BYTES: i64 = 256 81const NX_WIKI_DOC_CITE_CAP: i64 = 1048576 // 1 MB cite-expanded body buffer (standalone render path) 82const NX_WIKI_DOC_MD_TMP_CAP: i64 = 1048576 // 1 MB markdown-HTML temp (R4 TOC/id-inject staging) 83const NX_WIKI_DOC_INJECT_CAP: i64 = 1310720 // 1.25 MB id-injected copy (headroom for id= attrs) 84const NX_WIKI_DOC_TOC_CAP: i64 = 131072 // 128 KB TOC nav 85 86// Common ASCII byte values used as literals throughout (M7). 87const NX_ASCII_LT: i64 = 0x3C // '<' 88const NX_ASCII_GT: i64 = 0x3E // '>' 89const NX_ASCII_QUOT: i64 = 0x22 // '"' 90const NX_ASCII_LBRACKET: i64 = 0x5B // '[' 91const NX_ASCII_RBRACKET: i64 = 0x5D // ']' 92const NX_ASCII_LF: i64 = 0x0A // '\n' 93const NX_ASCII_CR: i64 = 0x0D // '\r' 94const NX_ASCII_HASH: i64 = 0x23 // '#' 95const NX_ASCII_SP: i64 = 0x20 // ' ' 96const NX_ASCII_SECTION: i64 = 0xA7 // '§' first UTF-8 byte (sentinel for §<NN>) 97 98// ===== Render context (caller-allocated; M1 no NULL creation) ================================================= 99// 100// Caller passes a NxWikiDocCtx with all buffers pre-allocated; render 101// fns operate on it without dynamic allocation that could fail. 102 103struct NxWikiDocCtx { 104 // Output buffer (HTML response body destination) 105 out_buf: *u8 106 out_cap: i64 107 out_off: i64 // updated as we write; final length on success 108 109 // Source markdown 110 src_buf: *u8 111 src_len: i64 112 113 // Doc-name index (caller-provided) for [[wikilink]] resolution 114 // Each row: 64-byte slot; first byte = name_len (0..63); rest = name. 115 // V2 replaces with nx_kv_store-based lookup. 116 doc_names_buf: *u8 117 doc_names_cap: i64 // bytes; capacity = cap / 64 entries 118 doc_names_count: i64 // populated entries 119 120 // Scratch buffers for inline parsing (provided by caller) 121 scratch_buf: *u8 122 scratch_cap: i64 123 124 // Counters 125 wikilinks_resolved: i64 126 wikilinks_broken: i64 127 valid: i64 128 129 // V1 NISHI PAGE FORMAT metadata (caller stamps via 130 // nx_wiki_doc_ctx_set_meta; zero = no V1 metadata, fallback to 131 // legacy chrome). When set, nx_wiki_doc_write_header_v1 emits 132 // conforming <meta name="nishi-*"> tags per NISHI_PAGE_FORMAT_V1. 133 meta_canonical: *u8 134 meta_canonical_n: i64 135 meta_summary: *u8 136 meta_summary_n: i64 137 meta_tags: *u8 138 meta_tags_n: i64 139 meta_license: *u8 140 meta_license_n: i64 141 meta_author: *u8 142 meta_author_n: i64 143 meta_last_modified: *u8 144 meta_last_modified_n: i64 145} 146 147// Sealed-init: stamps every field; refuses to start half-initialized 148// (M1 + M8: explicit verdict + no half-state). 149func nx_wiki_doc_ctx_init(ctx: *NxWikiDocCtx, 150 out_buf: *u8, out_cap: i64, 151 src_buf: *u8, src_len: i64, 152 doc_names_buf: *u8, doc_names_cap: i64, 153 doc_names_count: i64, 154 scratch_buf: *u8, scratch_cap: i64) -> i64 { 155 if (ctx as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 156 if (out_buf as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 157 if out_cap < 256 { return 0 - NX_WIKI_DOC_BAD_INPUT } 158 if (src_buf as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 159 if src_len < 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 160 if (scratch_buf as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 161 if scratch_cap < NX_WIKI_DOC_TITLE_SCRATCH_BYTES { return 0 - NX_WIKI_DOC_BAD_INPUT } 162 ctx.out_buf = out_buf 163 ctx.out_cap = out_cap 164 ctx.out_off = 0 165 ctx.src_buf = src_buf 166 ctx.src_len = src_len 167 ctx.doc_names_buf = doc_names_buf 168 ctx.doc_names_cap = doc_names_cap 169 ctx.doc_names_count = doc_names_count 170 ctx.scratch_buf = scratch_buf 171 ctx.scratch_cap = scratch_cap 172 ctx.wikilinks_resolved = 0 173 ctx.wikilinks_broken = 0 174 // V1 page format metadata defaults to unset; caller calls 175 // nx_wiki_doc_ctx_set_meta to populate before write_header_v1. 176 ctx.meta_canonical = 0 as *u8 177 ctx.meta_canonical_n = 0 178 ctx.meta_summary = 0 as *u8 179 ctx.meta_summary_n = 0 180 ctx.meta_tags = 0 as *u8 181 ctx.meta_tags_n = 0 182 ctx.meta_license = 0 as *u8 183 ctx.meta_license_n = 0 184 ctx.meta_author = 0 as *u8 185 ctx.meta_author_n = 0 186 ctx.meta_last_modified = 0 as *u8 187 ctx.meta_last_modified_n = 0 188 ctx.valid = 1 189 return NX_WIKI_DOC_OK 190} 191 192// ===== Low-level write helpers (M5 + M8: bounds + propagation) ================================================= 193 194// Write a single byte; verdict on overflow. 195func nx_wiki_doc_write_byte(ctx: *NxWikiDocCtx, b: i64) -> i64 { 196 if ctx.out_off >= ctx.out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 197 ctx.out_buf[ctx.out_off] = (b & 0xff) as u8 198 ctx.out_off = ctx.out_off + 1 199 return NX_WIKI_DOC_OK 200} 201 202// Write a counted byte run; verdict on overflow. 203func nx_wiki_doc_write_bytes(ctx: *NxWikiDocCtx, src: *u8, n: i64) -> i64 { 204 if n < 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 205 if ctx.out_off + n > ctx.out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 206 var i: i64 = 0 207 while i < n { 208 // Loop bound: n < NX_WIKI_DOC_MAX_TITLE_LEN guaranteed by caller for typical uses; 209 // hard cap below catches misuse (M3). 210 if i >= NX_WIKI_DOC_MAX_PARSE_ITER { return 0 - NX_WIKI_DOC_LOOP_BUDGET_EXCEEDED } 211 ctx.out_buf[ctx.out_off + i] = src[i] 212 i = i + 1 213 } 214 ctx.out_off = ctx.out_off + n 215 return NX_WIKI_DOC_OK 216} 217 218// Write a large counted byte run (rendered body / TOC) into out_buf, bounded by 219// the real out_cap (M5) with a loop cap sized to the 1 MB-class output buffer 220// (M3) -- distinct from nx_wiki_doc_write_bytes whose NX_WIKI_DOC_MAX_PARSE_ITER 221// guard (32-class, 100000) would falsely trip on a >100 KB rendered body. 222func nx_wiki_doc_write_run(ctx: *NxWikiDocCtx, src: *u8, n: i64) -> i64 { 223 if n < 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 224 if ctx.out_off + n > ctx.out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 225 var i: i64 = 0 226 while i < n { 227 if i >= NX_WIKI_DOC_CITE_CAP { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 228 ctx.out_buf[ctx.out_off + i] = src[i] 229 i = i + 1 230 } 231 ctx.out_off = ctx.out_off + n 232 return NX_WIKI_DOC_OK 233} 234 235// Write a NUL-terminated literal (compile-time string). Length computed 236// via bounded scan. 237func nx_wiki_doc_write_z(ctx: *NxWikiDocCtx, s: *u8) -> i64 { 238 if (s as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 239 var n: i64 = 0 240 while n < NX_WIKI_DOC_MAX_PARSE_ITER { 241 if s[n] == (0 as u8) { return nx_wiki_doc_write_bytes(ctx, s, n) } 242 n = n + 1 243 } 244 return 0 - NX_WIKI_DOC_LOOP_BUDGET_EXCEEDED 245} 246 247// Write an HTML-escaped byte run (composes html_escape). 248func nx_wiki_doc_write_escaped(ctx: *NxWikiDocCtx, src: *u8, n: i64) -> i64 { 249 if n < 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 250 // html_escape's API: html_escape(out, cap, src, n) -> bytes written or HE_ERR_SHORT 251 let avail: i64 = ctx.out_cap - ctx.out_off 252 if avail <= 0 { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 253 let out_tail: *u8 = (ctx.out_buf as i64 + ctx.out_off) as *u8 254 let written: i64 = html_escape(out_tail, avail, src, n) 255 if written < 0 { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 256 ctx.out_off = ctx.out_off + written 257 return NX_WIKI_DOC_OK 258} 259 260// ===== Doc-name lookup for [[wikilink]] resolution (M5 bounds-checked) ================================================= 261// 262// V1: linear scan over the caller's name buffer. Each entry is 263// 64 bytes: byte 0 = length (1..63); bytes 1..length = UTF-8 name. 264// V2 replaces with nx_kv_store-backed FNV-1a hashed lookup. 265 266const NX_WIKI_DOC_NAME_ENTRY_BYTES: i64 = 64 267const NX_WIKI_DOC_NAME_MAX_LEN: i64 = 63 268// M3 bounded-loop cap for the doc-names scan. Sized to the doc-store cap (1000) 269// so every real page's slug is reachable -- the prior 256 cap (NX_WIKI_DOC_MAX_ 270// WIKILINKS) silently treated page #257+ as a MISS (broken-span) once the handler 271// began deriving the FULL known-page set from the store. 272const NX_WIKI_DOC_NAME_SCAN_CAP: i64 = 1000 273 274func nx_wiki_doc_name_resolves(ctx: *NxWikiDocCtx, name: *u8, name_n: i64) -> i64 { 275 if name_n < 1 { return 0 } 276 if name_n > NX_WIKI_DOC_NAME_MAX_LEN { return 0 } 277 if ctx.doc_names_count <= 0 { return 0 } 278 var i: i64 = 0 279 while i < ctx.doc_names_count { 280 if i >= NX_WIKI_DOC_NAME_SCAN_CAP { return 0 } // M3 281 let entry_off: i64 = i * NX_WIKI_DOC_NAME_ENTRY_BYTES 282 if entry_off >= ctx.doc_names_cap { return 0 } // M5 283 let entry_len: i64 = ctx.doc_names_buf[entry_off] as i64 284 if entry_len == name_n { 285 // Compare body. M5 bounds: entry_off + 1 + entry_len <= cap. 286 if (entry_off + 1 + entry_len) > ctx.doc_names_cap { return 0 } 287 var matched: i64 = 1 288 var j: i64 = 0 289 while j < name_n { 290 if name[j] != ctx.doc_names_buf[entry_off + 1 + j] { matched = 0; j = name_n } 291 j = j + 1 292 } 293 if matched == 1 { return 1 } 294 } 295 i = i + 1 296 } 297 return 0 298} 299 300// ===== [[wikilink]] preprocessor (VERBATIM COPY src -> scratch) =============== 301// 302// ROOT-CAUSE FIX (the "escaped wikilink" bug): the prior preprocessor expanded 303// [[name]] into raw <a ...>/<span ...> HTML BEFORE markdown_block ran. But 304// markdown sends paragraph/heading/list text through markdown_inline, whose 305// every non-construct byte goes through html_escape -- so the injected '<','>' 306// became &lt;,&gt; and the link rendered as DEAD ESCAPED TEXT (&lt;a href=...). 307// (Cite cards "survived" only because the gates checked the cite-render output 308// BEFORE markdown, never the served HTML through markdown_block.) 309// 310// THE FIX is a two-phase split that mirrors how html_escape leaves '[' and ']' 311// untouched (they are not among the five unsafe bytes): this pass now copies the 312// (cite-expanded) body to scratch VERBATIM, leaving every literal [[name]] token 313// in place. markdown_block then renders the body and passes the [[name]] tokens 314// through unchanged (they are not markdown syntax, and '['/'']' are not escaped). 315// nx_wiki_doc_postpass_wikilinks (below) runs OVER THE RENDERED HTML, replacing 316// each surviving [[name]] with a LIVE <a>/<span> emitted RAW -- so the anchor is 317// never re-escaped. The cite-card HTML produced by the earlier cite pass copies 318// through both passes verbatim (no [[cite:...]] tokens remain after it), so cite 319// cards are NOT regressed. 320// 321// out_scratch_used carries the byte count written to ctx.scratch_buf. 322 323func nx_wiki_doc_preprocess_wikilinks(ctx: *NxWikiDocCtx, 324 out_scratch_used: *i64) -> i64 { 325 if ctx.valid != 1 { return 0 - NX_WIKI_DOC_BAD_INPUT } 326 if (out_scratch_used as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 327 out_scratch_used[0] = 0 328 if ctx.src_len > ctx.scratch_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 329 var i: i64 = 0 330 while i < ctx.src_len { 331 if i >= NX_WIKI_DOC_MAX_PARSE_ITER { return 0 - NX_WIKI_DOC_LOOP_BUDGET_EXCEEDED } 332 ctx.scratch_buf[i] = ctx.src_buf[i] 333 i = i + 1 334 } 335 out_scratch_used[0] = ctx.src_len 336 return NX_WIKI_DOC_OK 337} 338 339// ===== [[wikilink]] POST-markdown pass (the live-link resolver) =============== 340// 341// Scans rendered-markdown HTML in[0..in_n] for literal [[name]] tokens and 342// writes a resolved copy into out[0..]. Each [[name]] becomes: 343// resolved (name in the doc-names set) -> <a href="/wiki/<name>">name</a> 344// missing -> <span class="wikilink-broken">[[name]]</span> 345// A [[cite:...]] token is copied through verbatim (those are handled by the 346// cite pass; none normally survive to here). The emitted <a>/<span> is written 347// RAW (never through html_escape), so the link is LIVE in the served bytes. 348// 349// Single-exit `while done==0` scan for the closing ]] (the break-via-flag idiom 350// is broken on this compiler -- per the landmines note). out_used carries the 351// byte count; ctx.wikilinks_resolved / wikilinks_broken are updated for the 352// audit reporters. 353func nx_wiki_doc_postpass_wikilinks(ctx: *NxWikiDocCtx, 354 in_html: *u8, in_n: i64, 355 out_html: *u8, out_cap: i64, 356 out_used: *i64) -> i64 { 357 if ctx.valid != 1 { return 0 - NX_WIKI_DOC_BAD_INPUT } 358 if (in_html as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 359 if (out_html as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 360 if (out_used as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 361 out_used[0] = 0 362 if in_n < 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 363 var i: i64 = 0 364 var w: i64 = 0 365 var iter: i64 = 0 366 while i < in_n { 367 if iter >= NX_WIKI_DOC_MAX_PARSE_ITER { return 0 - NX_WIKI_DOC_LOOP_BUDGET_EXCEEDED } 368 iter = iter + 1 369 var consumed: i64 = 0 370 371 // ---- detect "[[" at i ---- 372 if i + 1 < in_n { 373 if in_html[i] == (NX_ASCII_LBRACKET as u8) { 374 if in_html[i + 1] == (NX_ASCII_LBRACKET as u8) { 375 // single-exit scan for the FIRST "]]" at/after the name start 376 let start_name: i64 = i + 2 377 var cur: i64 = start_name 378 var end_name: i64 = start_name 379 var found: i64 = 0 380 var done: i64 = 0 381 while done == 0 { 382 if cur >= in_n - 1 { done = 1 } 383 if cur - start_name >= NX_WIKI_DOC_MAX_WIKILINK_LEN { done = 1 } 384 if done == 0 { 385 if in_html[cur] == (NX_ASCII_RBRACKET as u8) { 386 if in_html[cur + 1] == (NX_ASCII_RBRACKET as u8) { 387 end_name = cur 388 found = 1 389 done = 1 390 } 391 } 392 if found == 0 { cur = cur + 1 } 393 } 394 } 395 if found == 1 { 396 let name_ptr: *u8 = (in_html as i64 + start_name) as *u8 397 let name_n: i64 = end_name - start_name 398 // is it a [[cite:...]] ? (copy through verbatim; not a xref) 399 var is_cite: i64 = 0 400 if name_n >= 5 { 401 if name_ptr[0] == (0x63 as u8) { // 'c' 402 if name_ptr[1] == (0x69 as u8) { // 'i' 403 if name_ptr[2] == (0x74 as u8) { // 't' 404 if name_ptr[3] == (0x65 as u8) { // 'e' 405 if name_ptr[4] == (0x3A as u8) { is_cite = 1 } // ':' 406 } 407 } 408 } 409 } 410 } 411 if name_n > 0 { 412 if name_n <= NX_WIKI_DOC_MAX_WIKILINK_LEN { 413 if is_cite == 1 { 414 // verbatim "[[<name>]]" 415 let need: i64 = 4 + name_n 416 if w + need > out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 417 out_html[w] = NX_ASCII_LBRACKET as u8 418 out_html[w + 1] = NX_ASCII_LBRACKET as u8 419 w = w + 2 420 var ci: i64 = 0 421 while ci < name_n { out_html[w + ci] = name_ptr[ci]; ci = ci + 1 } 422 w = w + name_n 423 out_html[w] = NX_ASCII_RBRACKET as u8 424 out_html[w + 1] = NX_ASCII_RBRACKET as u8 425 w = w + 2 426 i = end_name + 2 427 consumed = 1 428 } 429 if is_cite == 0 { 430 let resolves: i64 = nx_wiki_doc_name_resolves(ctx, name_ptr, name_n) 431 if resolves == 1 { 432 ctx.wikilinks_resolved = ctx.wikilinks_resolved + 1 433 // <a href="/wiki/<name>"><name></a> (RAW, not escaped) 434 let need: i64 = 23 + (name_n * 2) 435 if w + need > out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 436 let pfx: *u8 = "<a href=\"/wiki/" as *u8 437 var pi: i64 = 0 438 while pi < 15 { out_html[w + pi] = pfx[pi]; pi = pi + 1 } 439 w = w + 15 440 var ni: i64 = 0 441 while ni < name_n { out_html[w + ni] = name_ptr[ni]; ni = ni + 1 } 442 w = w + name_n 443 let mid: *u8 = "\">" as *u8 444 var mi: i64 = 0 445 while mi < 2 { out_html[w + mi] = mid[mi]; mi = mi + 1 } 446 w = w + 2 447 var n2: i64 = 0 448 while n2 < name_n { out_html[w + n2] = name_ptr[n2]; n2 = n2 + 1 } 449 w = w + name_n 450 let suf: *u8 = "</a>" as *u8 451 var si: i64 = 0 452 while si < 4 { out_html[w + si] = suf[si]; si = si + 1 } 453 w = w + 4 454 } 455 if resolves == 0 { 456 ctx.wikilinks_broken = ctx.wikilinks_broken + 1 457 // <span class="wikilink-broken">[[<name>]]</span> (RAW) 458 let need: i64 = 47 + name_n 459 if w + need > out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 460 let pfx: *u8 = "<span class=\"wikilink-broken\">[[" as *u8 461 var pi: i64 = 0 462 while pi < 32 { out_html[w + pi] = pfx[pi]; pi = pi + 1 } 463 w = w + 32 464 var ni: i64 = 0 465 while ni < name_n { out_html[w + ni] = name_ptr[ni]; ni = ni + 1 } 466 w = w + name_n 467 let suf: *u8 = "]]</span>" as *u8 468 var si: i64 = 0 469 while si < 9 { out_html[w + si] = suf[si]; si = si + 1 } 470 w = w + 9 471 } 472 i = end_name + 2 473 consumed = 1 474 } 475 } 476 } 477 if name_n == 0 { 478 // empty [[]] -- keep literally 479 if w + 4 > out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 480 out_html[w] = NX_ASCII_LBRACKET as u8 481 out_html[w + 1] = NX_ASCII_LBRACKET as u8 482 out_html[w + 2] = NX_ASCII_RBRACKET as u8 483 out_html[w + 3] = NX_ASCII_RBRACKET as u8 484 w = w + 4 485 i = end_name + 2 486 consumed = 1 487 } 488 } 489 } 490 } 491 } 492 493 // ---- default: copy one byte verbatim ---- 494 if consumed == 0 { 495 if w + 1 > out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 496 out_html[w] = in_html[i] 497 w = w + 1 498 i = i + 1 499 } 500 } 501 out_used[0] = w 502 return NX_WIKI_DOC_OK 503} 504 505// ===== [[cite:<cid>]] POST-markdown pass (the live supporting-source card) ===== 506// 507// ROOT-CAUSE FIX (the "escaped cite card" bug -- the SAME shape as the escaped 508// [[wikilink]] bug): the cite pass USED to expand [[cite:<cid>]] into raw 509// <details class="nx-cite"> card HTML BEFORE markdown_block ran. markdown then 510// sent every non-construct byte through html_escape, so the card's '<','>' 511// became &lt;,&gt; and the served bytes were DEAD ESCAPED TEXT (&lt;details...), 512// never a real expandable card. The old cite gates "passed" only because they 513// asserted the substring in the PRE-markdown cite-render buffer -- a measurement 514// artifact; the SERVED output was escaped. 515// 516// THE FIX mirrors nx_wiki_doc_postpass_wikilinks: leave the literal [[cite:<cid>]] 517// token in the body (markdown leaves '['/'']' untouched -- they are not among the 518// five html_escape-unsafe bytes, and "[[cite:...]]" is not markdown syntax), let 519// markdown render, then THIS pass runs OVER the rendered HTML and replaces each 520// surviving [[cite:<cid>]] with the card HTML emitted RAW by nx_wiki_cite_one -- 521// never re-escaped. nx_wiki_cite_one performs the REAL archive lookup + license 522// gate, so THE LIAR-KILL IS PRESERVED POST-MARKDOWN: a proprietary/unknown source 523// gets a link + license-note only (its bytes are NEVER inlined), and only a 524// hostable (PD/CC) source's bytes are inlined (already html-escaped inside the 525// card by cite_one). This pass runs alongside / before the wikilink post-pass; 526// the wikilink pass copies [[cite:...]] through verbatim, so ordering is free. 527// 528// Single-exit `while done==0` scan for the closing ]] (the break-via-flag idiom 529// is broken on this compiler). prefix = the archive store prefix (production: 530// WAR_PREFIX; the gate passes a fresh per-run prefix). out_used carries the byte 531// count. Graceful (Cardinal 14): on overflow the caller falls back to the input. 532func nx_wiki_doc_postpass_cite(ctx: *NxWikiDocCtx, prefix: *u8, 533 in_html: *u8, in_n: i64, 534 out_html: *u8, out_cap: i64, 535 out_used: *i64) -> i64 { 536 if ctx.valid != 1 { return 0 - NX_WIKI_DOC_BAD_INPUT } 537 if (in_html as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 538 if (out_html as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 539 if (out_used as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 540 out_used[0] = 0 541 if in_n < 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 542 // NUL-terminated cid scratch (a CID is ~69 bytes; this is headroom). 543 let cidbuf: *u8 = sys_mmap(NX_WIKI_DOC_MAX_WIKILINK_LEN + 16) 544 var i: i64 = 0 545 var w: i64 = 0 546 var iter: i64 = 0 547 while i < in_n { 548 if iter >= NX_WIKI_DOC_MAX_PARSE_ITER { return 0 - NX_WIKI_DOC_LOOP_BUDGET_EXCEEDED } 549 iter = iter + 1 550 var consumed: i64 = 0 551 552 // ---- detect "[[cite:" at i (need "[[cite:" (7) + "]]" (2) headroom) ---- 553 if i + 7 < in_n { 554 if in_html[i] == (NX_ASCII_LBRACKET as u8) { 555 if in_html[i + 1] == (NX_ASCII_LBRACKET as u8) { 556 if in_html[i + 2] == (0x63 as u8) { // 'c' 557 if in_html[i + 3] == (0x69 as u8) { // 'i' 558 if in_html[i + 4] == (0x74 as u8) { // 't' 559 if in_html[i + 5] == (0x65 as u8) { // 'e' 560 if in_html[i + 6] == (0x3A as u8) { // ':' 561 // single-exit scan for the FIRST "]]" at/after the cid start 562 let cid_start: i64 = i + 7 563 var cur: i64 = cid_start 564 var end_cid: i64 = cid_start 565 var found: i64 = 0 566 var done: i64 = 0 567 while done == 0 { 568 if cur >= in_n - 1 { done = 1 } 569 if cur - cid_start >= NX_WIKI_DOC_MAX_WIKILINK_LEN { done = 1 } 570 if done == 0 { 571 if in_html[cur] == (NX_ASCII_RBRACKET as u8) { 572 if in_html[cur + 1] == (NX_ASCII_RBRACKET as u8) { 573 end_cid = cur 574 found = 1 575 done = 1 576 } 577 } 578 if found == 0 { cur = cur + 1 } 579 } 580 } 581 if found == 1 { 582 let cid_n: i64 = end_cid - cid_start 583 if cid_n > 0 { 584 if cid_n < NX_WIKI_DOC_MAX_WIKILINK_LEN { 585 // copy the cid out NUL-terminated for cite_one 586 let cid_ptr: *u8 = (in_html as i64 + cid_start) as *u8 587 var c: i64 = 0 588 while c < cid_n { cidbuf[c] = cid_ptr[c]; c = c + 1 } 589 cidbuf[cid_n] = 0 as u8 590 // emit this citation's card HTML RAW (never escaped). 591 // cite_one does the archive lookup + license gate + 592 // the LIAR-KILL: proprietary/unknown -> link+note only 593 // (bytes suppressed); hostable -> inline escaped card. 594 let nw: i64 = nx_wiki_cite_one(prefix, cidbuf, out_html, out_cap, w) 595 if nw < 0 { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 596 w = nw 597 i = end_cid + 2 // resume after ]] 598 consumed = 1 599 } 600 } 601 } 602 } 603 } 604 } 605 } 606 } 607 } 608 } 609 } 610 611 // ---- default: copy one byte verbatim ---- 612 if consumed == 0 { 613 if w + 1 > out_cap { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 614 out_html[w] = in_html[i] 615 w = w + 1 616 i = i + 1 617 } 618 } 619 out_used[0] = w 620 return NX_WIKI_DOC_OK 621} 622 623// ===== Page chrome: header + sidebar + footer ================================================= 624// 625// Writes the static surrounding HTML for any wiki doc. Caller provides 626// the rendered body (between header and footer). 627 628// ===== V1 NISHI PAGE FORMAT metadata setter ================================================= 629// 630// Caller stamps per-page metadata BEFORE calling 631// nx_wiki_doc_write_header_v1 (or nx_wiki_doc_render_v1). 632// Idempotent; clears all 6 metadata fields if any is null. 633 634func nx_wiki_doc_ctx_set_meta(ctx: *NxWikiDocCtx, 635 canonical: *u8, canonical_n: i64, 636 summary: *u8, summary_n: i64, 637 tags: *u8, tags_n: i64, 638 license: *u8, license_n: i64, 639 author: *u8, author_n: i64, 640 last_modified: *u8, last_modified_n: i64) -> i64 { 641 if ctx.valid != 1 { return 0 - NX_WIKI_DOC_BAD_INPUT } 642 if (canonical as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 643 if (summary as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 644 if (tags as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 645 if (license as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 646 if (author as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 647 if (last_modified as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 648 ctx.meta_canonical = canonical 649 ctx.meta_canonical_n = canonical_n 650 ctx.meta_summary = summary 651 ctx.meta_summary_n = summary_n 652 ctx.meta_tags = tags 653 ctx.meta_tags_n = tags_n 654 ctx.meta_license = license 655 ctx.meta_license_n = license_n 656 ctx.meta_author = author 657 ctx.meta_author_n = author_n 658 ctx.meta_last_modified = last_modified 659 ctx.meta_last_modified_n = last_modified_n 660 return NX_WIKI_DOC_OK 661} 662 663// ===== V1 page-format-conformant header emit ================================================= 664// 665// Composes hub/nx_nishi_page_emit to emit the 9 required 666// <meta name="nishi-*"> tags + <title> + chrome. Cross-validated 667// by nx_nishi_emit_full_page's internal validator. 668// 669// Caller MUST have called nx_wiki_doc_ctx_set_meta first; if metadata 670// fields are unset, returns NX_WIKI_DOC_BAD_INPUT (defensive at 671// boundary per Cardinal 12; fail-fast vs silently emitting non-V1 page). 672 673// World-class sovereign CSS for the served wiki page. Emitted by THIS organ 674// (no third-party framework): a readable ~66ch measure, a modern system font 675// stack, generous line-height + a clear heading scale, styled code/tables/ 676// blockquotes, a clean sticky top nav + search box, and full dark-mode via 677// prefers-color-scheme. Split across several write_z calls so each string 678// literal stays modest for the sovereign compiler. CSS custom properties carry 679// the palette so the dark-mode override is a single block. 680func nx_wiki_doc_emit_css(ctx: *NxWikiDocCtx) -> i64 { 681 let r1: i64 = nx_wiki_doc_write_z(ctx, "<style>\n:root{--bg:#ffffff;--fg:#1a202c;--muted:#5a6573;--line:#e2e8f0;--card:#f7fafc;--code-bg:#f1f5f9;--accent:#2c7a7b;--accent-fg:#234e52;--link:#2b6cb0;--sel:#b2f5ea;--measure:66ch}\n*{box-sizing:border-box}\nhtml{-webkit-text-size-adjust:100%}\nbody{margin:0;background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif;font-size:18px;line-height:1.7;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}\n::selection{background:var(--sel)}\n" as *u8) 682 if r1 != NX_WIKI_DOC_OK { return r1 } 683 let r2: i64 = nx_wiki_doc_write_z(ctx, "header.nx-top{position:sticky;top:0;z-index:10;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:saturate(1.6) blur(8px);border-bottom:1px solid var(--line)}\n.nx-nav{max-width:1080px;margin:0 auto;display:flex;align-items:center;gap:1.1em;padding:.65em 1.2em;flex-wrap:wrap}\n.nx-brand{font-weight:800;letter-spacing:-.02em;color:var(--fg);text-decoration:none;font-size:1.05em;margin-right:.2em}\n.nx-brand span{color:var(--accent)}\n.nx-nav a{color:var(--muted);text-decoration:none;font-size:.92em;font-weight:600;padding:.25em 0;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}\n.nx-nav a:hover{color:var(--fg);border-bottom-color:var(--accent)}\n.nx-search{margin-left:auto;display:flex}\n.nx-search input{font:inherit;font-size:.9em;padding:.42em .8em;border:1px solid var(--line);border-radius:999px;background:var(--card);color:var(--fg);min-width:11em;outline:none;transition:border-color .15s,box-shadow .15s}\n.nx-search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--sel)}\n" as *u8) 684 if r2 != NX_WIKI_DOC_OK { return r2 } 685 let r3: i64 = nx_wiki_doc_write_z(ctx, "main{display:block}\narticle{max-width:var(--measure);margin:0 auto;padding:2.4em 1.3em 4em}\narticle>h1:first-child{margin-top:.2em}\nh1,h2,h3,h4,h5,h6{line-height:1.25;letter-spacing:-.018em;font-weight:750;margin:2em 0 .6em}\nh1{font-size:2.15em;margin-top:.3em}\nh2{font-size:1.6em;padding-bottom:.25em;border-bottom:1px solid var(--line)}\nh3{font-size:1.3em}\nh4{font-size:1.1em}\nh5,h6{font-size:1em;color:var(--muted)}\np{margin:0 0 1.15em}\na{color:var(--link);text-decoration:none}\na:hover{text-decoration:underline;text-underline-offset:2px}\nimg{max-width:100%;height:auto}\nhr{border:0;border-top:1px solid var(--line);margin:2.4em 0}\nstrong{font-weight:700}\n" as *u8) 686 if r3 != NX_WIKI_DOC_OK { return r3 } 687 let r4: i64 = nx_wiki_doc_write_z(ctx, "code{font-family:ui-monospace,SFMono-Regular,\"SF Mono\",Menlo,Consolas,\"Liberation Mono\",monospace;font-size:.88em;background:var(--code-bg);padding:.15em .4em;border-radius:5px;border:1px solid var(--line)}\npre{background:var(--code-bg);border:1px solid var(--line);border-radius:10px;padding:1.05em 1.2em;overflow-x:auto;line-height:1.55;margin:0 0 1.4em}\npre code{background:none;border:0;padding:0;font-size:.86em}\nblockquote{margin:0 0 1.4em;padding:.5em 1.1em;border-left:4px solid var(--accent);background:var(--card);border-radius:0 8px 8px 0;color:var(--muted)}\nblockquote p:last-child{margin-bottom:0}\nul,ol{margin:0 0 1.2em;padding-left:1.5em}\nli{margin:.3em 0}\nli::marker{color:var(--accent)}\n" as *u8) 688 if r4 != NX_WIKI_DOC_OK { return r4 } 689 let r5: i64 = nx_wiki_doc_write_z(ctx, "table{border-collapse:collapse;width:100%;margin:0 0 1.5em;font-size:.94em;display:block;overflow-x:auto}\nth,td{border:1px solid var(--line);padding:.55em .9em;text-align:left;vertical-align:top}\nthead th{background:var(--card);font-weight:700}\ntbody tr:nth-child(even){background:color-mix(in srgb,var(--card) 55%,transparent)}\n.normative{border-left:4px solid var(--accent);padding:.4em .9em;background:var(--card);border-radius:0 8px 8px 0;margin:0 0 1.2em}\n.informative{border-left:4px solid #8aa0c8;padding:.4em .9em;color:var(--muted);margin:0 0 1.2em}\n.rationale{border-left:4px solid #c9923a;padding:.4em .9em;font-style:italic;margin:0 0 1.2em}\n.wikilink-broken{color:#c53030;text-decoration:line-through;cursor:help}\nfooter.nx-foot{max-width:var(--measure);margin:0 auto;padding:1.6em 1.3em 2.6em;border-top:1px solid var(--line);color:var(--muted);font-size:.86em;line-height:1.6}\nfooter.nx-foot a{color:var(--muted);font-weight:600}\n" as *u8) 690 if r5 != NX_WIKI_DOC_OK { return r5 } 691 // Dark mode: a single prefers-color-scheme block re-points the palette. 692 let r6: i64 = nx_wiki_doc_write_z(ctx, "@media (prefers-color-scheme:dark){:root{--bg:#0f1419;--fg:#e6edf3;--muted:#9aa7b4;--line:#222b36;--card:#161d26;--code-bg:#11171f;--accent:#5eb5ad;--accent-fg:#9decd9;--link:#6cb6ff;--sel:#1f3a3a}}\n@media (max-width:680px){body{font-size:16.5px}article{padding:1.6em 1.05em 3em}h1{font-size:1.85em}.nx-search input{min-width:8.5em}}\n</style>\n" as *u8) 693 if r6 != NX_WIKI_DOC_OK { return r6 } 694 return NX_WIKI_DOC_OK 695} 696 697// Emit one V1 nishi-* meta tag through the hub's per-tag primitive (which 698// html-escapes the value). Advances ctx.out_off. name_z is the tag name and 699// name_n its length; value/value_n the (already validated) value. 700func nx_wiki_doc_meta(ctx: *NxWikiDocCtx, name_z: *u8, name_n: i64, 701 value: *u8, value_n: i64) -> i64 { 702 let o2: i64 = nx_nishi_emit_meta(ctx.out_buf, ctx.out_cap, ctx.out_off, 703 name_z, name_n, value, value_n) 704 if o2 < 0 { return 0 - NX_WIKI_DOC_OUTPUT_OVERFLOW } 705 ctx.out_off = o2 706 return NX_WIKI_DOC_OK 707} 708 709func nx_wiki_doc_write_header_v1(ctx: *NxWikiDocCtx, 710 doc_title: *u8, doc_title_n: i64) -> i64 { 711 if ctx.valid != 1 { return 0 - NX_WIKI_DOC_BAD_INPUT } 712 if (ctx.meta_canonical as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 713 if (ctx.meta_summary as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 714 if (ctx.meta_tags as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 715 if (ctx.meta_license as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 716 if (ctx.meta_author as i64) == 0 { return 0 - NX_WIKI_DOC_BAD_INPUT } 717 718 // ----- <!DOCTYPE> + <html> + <head> (built HERE, not via the monolithic 719 // nx_nishi_emit_head_block, so we can place <meta viewport>, the world-class 720 // <style>, and the inline-cite card CSS INSIDE <head> while still emitting 721 // the 9 V1 nishi-* meta tags via the hub's per-tag primitive). ----- 722 let rc_d: i64 = nx_wiki_doc_write_z(ctx, "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"color-scheme\" content=\"light dark\">\n" as *u8) 723 if rc_d != NX_WIKI_DOC_OK { return rc_d } 724 725 // <title> = CLEAN heading text (html-escaped) + site suffix. 726 let rc_t1: i64 = nx_wiki_doc_write_z(ctx, "<title>" as *u8) 727 if rc_t1 != NX_WIKI_DOC_OK { return rc_t1 } 728 let rc_t2: i64 = nx_wiki_doc_write_escaped(ctx, doc_title, doc_title_n) 729 if rc_t2 != NX_WIKI_DOC_OK { return rc_t2 } 730 let rc_t3: i64 = nx_wiki_doc_write_z(ctx, " | Nishi Wiki</title>\n" as *u8) 731 if rc_t3 != NX_WIKI_DOC_OK { return rc_t3 } 732 733 // The 9 required V1 nishi-* meta tags (per NISHI_PAGE_FORMAT_V1 §2), via the 734 // hub's per-tag primitive (it html-escapes each value). 735 let rc_m1: i64 = nx_wiki_doc_meta(ctx, "nishi-canonical" as *u8, 15, ctx.meta_canonical, ctx.meta_canonical_n) 736 if rc_m1 != NX_WIKI_DOC_OK { return rc_m1 } 737 let rc_m2: i64 = nx_wiki_doc_meta(ctx, "nishi-title" as *u8, 11, doc_title, doc_title_n) 738 if rc_m2 != NX_WIKI_DOC_OK { return rc_m2 } 739 let rc_m3: i64 = nx_wiki_doc_meta(ctx, "nishi-summary" as *u8, 13, ctx.meta_summary, ctx.meta_summary_n) 740 if rc_m3 != NX_WIKI_DOC_OK { return rc_m3 } 741 let rc_m4: i64 = nx_wiki_doc_meta(ctx, "nishi-tags" as *u8, 10, ctx.meta_tags, ctx.meta_tags_n) 742 if rc_m4 != NX_WIKI_DOC_OK { return rc_m4 } 743 if (ctx.meta_last_modified as i64) != 0 { 744 let rc_m5: i64 = nx_wiki_doc_meta(ctx, "nishi-last-modified" as *u8, 19, ctx.meta_last_modified, ctx.meta_last_modified_n) 745 if rc_m5 != NX_WIKI_DOC_OK { return rc_m5 } 746 } 747 // content-hash (FNV-1a 64 of the body) via the hub. 748 let hash_hex: *u8 = (sys_mmap(NX_WIKI_DOC_HTML_SCRATCH_BYTES + 1)) as *u8 749 let rc_ch: i64 = nx_nishi_compute_content_hash(ctx.src_buf, ctx.src_len, hash_hex) 750 if rc_ch == NX_NPE_OK { 751 let rc_m6: i64 = nx_wiki_doc_meta(ctx, "nishi-content-hash" as *u8, 18, hash_hex, 16) 752 if rc_m6 != NX_WIKI_DOC_OK { return rc_m6 } 753 } 754 let rc_m7: i64 = nx_wiki_doc_meta(ctx, "nishi-page-version" as *u8, 18, "1.0.0" as *u8, 5) 755 if rc_m7 != NX_WIKI_DOC_OK { return rc_m7 } 756 let rc_m8: i64 = nx_wiki_doc_meta(ctx, "nishi-license" as *u8, 13, ctx.meta_license, ctx.meta_license_n) 757 if rc_m8 != NX_WIKI_DOC_OK { return rc_m8 } 758 let rc_m9: i64 = nx_wiki_doc_meta(ctx, "nishi-author" as *u8, 12, ctx.meta_author, ctx.meta_author_n) 759 if rc_m9 != NX_WIKI_DOC_OK { return rc_m9 } 760 761 // world-class page CSS + the inline-cite card CSS (both sovereign-emitted). 762 let rc_css: i64 = nx_wiki_doc_emit_css(ctx) 763 if rc_css != NX_WIKI_DOC_OK { return rc_css } 764 let cite_avail: i64 = ctx.out_cap - ctx.out_off 765 if cite_avail > 0 { 766 let cite_tail: *u8 = (ctx.out_buf as i64 + ctx.out_off) as *u8 767 let cw: i64 = nx_wiki_cite_css(cite_tail, cite_avail) 768 if cw > 0 { ctx.out_off = ctx.out_off + cw } 769 } 770 771 // ----- close head; open <body> + semantic chrome (sticky top nav + brand + 772 // search box GET /wiki/search) + <main><article>. ----- 773 let body_open: *u8 = "</head>\n<body>\n<header class=\"nx-top\"><nav class=\"nx-nav\"><a class=\"nx-brand\" href=\"/wiki/\">Nishi<span>Wiki</span></a><a href=\"/wiki/\">Home</a><a href=\"/wiki/dashboard\">Dashboard</a><a href=\"/wiki/pipeline\">Pipeline</a><a href=\"/wiki/admin\">Admin</a><form class=\"nx-search\" method=\"get\" action=\"/wiki/search\"><input type=\"search\" name=\"q\" placeholder=\"Search the wiki\" aria-label=\"Search the wiki\"></form></nav></header>\n<main><article>\n" as *u8 774 return nx_wiki_doc_write_z(ctx, body_open) 775} 776 777// ===== LEGACY (V0) header emit -- kept for callers that haven't migrated ================================================= 778 779func nx_wiki_doc_write_header(ctx: *NxWikiDocCtx, doc_title: *u8, doc_title_n: i64) -> i64 { 780 let pre: *u8 = "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">" as *u8 781 let rc1: i64 = nx_wiki_doc_write_z(ctx, pre) 782 if rc1 != NX_WIKI_DOC_OK { return rc1 } 783 let title_pre: *u8 = "<title>" as *u8 784 let rc2: i64 = nx_wiki_doc_write_z(ctx, title_pre) 785 if rc2 != NX_WIKI_DOC_OK { return rc2 } 786 let rc3: i64 = nx_wiki_doc_write_escaped(ctx, doc_title, doc_title_n) 787 if rc3 != NX_WIKI_DOC_OK { return rc3 } 788 let title_post: *u8 = " | Nishi Wiki</title>" as *u8 789 let rc4: i64 = nx_wiki_doc_write_z(ctx, title_post) 790 if rc4 != NX_WIKI_DOC_OK { return rc4 } 791 let style: *u8 = "<style>body{font-family:system-ui,sans-serif;max-width:920px;margin:2em auto;padding:0 1em;line-height:1.5}h1,h2,h3,h4,h5,h6{margin-top:1.5em}code{background:#f4f4f4;padding:.1em .3em;border-radius:3px}pre{background:#f4f4f4;padding:1em;overflow-x:auto}table{border-collapse:collapse;margin:1em 0}th,td{border:1px solid #ddd;padding:.4em .8em;text-align:left}.normative{border-left:3px solid #2c7;padding-left:.6em}.informative{border-left:3px solid #88a;padding-left:.6em;color:#555}.rationale{border-left:3px solid #c92;padding-left:.6em;font-style:italic}.wikilink-broken{color:#c00;text-decoration:line-through}header{border-bottom:1px solid #ddd;padding-bottom:.5em;margin-bottom:1em}header a{margin-right:1em;text-decoration:none;color:#27c}footer{border-top:1px solid #ddd;padding-top:.5em;margin-top:2em;color:#888;font-size:.9em}</style>" as *u8 792 let rc5: i64 = nx_wiki_doc_write_z(ctx, style) 793 if rc5 != NX_WIKI_DOC_OK { return rc5 } 794 let body_open: *u8 = "</head><body><header class=\"nx-top\"><nav class=\"nx-nav\"><a class=\"nx-brand\" href=\"/wiki/\">Nishi<span>Wiki</span></a><a href=\"/wiki/\">Home</a><a href=\"/wiki/dashboard\">Dashboard</a><a href=\"/wiki/search\">Search</a><a href=\"/wiki/admin\">Admin</a></nav></header><main><article>" as *u8 795 return nx_wiki_doc_write_z(ctx, body_open) 796} 797 798// Footer closes the semantic <article>/<main> opened by BOTH header paths 799// (legacy + V1) and emits a clean, muted site footer. 800func nx_wiki_doc_write_footer(ctx: *NxWikiDocCtx) -> i64 { 801 let footer: *u8 = "\n</article></main>\n<footer class=\"nx-foot\">Rendered by the <strong>Nishi sovereign substrate wiki</strong> &middot; pure NishiLang, no third-party framework &middot; <a href=\"/wiki/\">browse all pages</a> &middot; per NISHIDOC_STANDARD v1.0.0</footer>\n</body>\n</html>\n" as *u8 802 return nx_wiki_doc_write_z(ctx, footer) 803} 804 805// ===== Top-level render ================================================= 806// 807// 1. Write page header (chrome + opening <main>) 808// 2. Preprocess [[wikilinks]] inp source -> scratch 809// 3. Run markdown_block on scratch -> append to out_buf 810// 4. Write page footer 811// 812// Returns NX_WIKI_DOC_OK on success; final HTML length inp ctx.out_off. 813 814func nx_wiki_doc_render(ctx: *NxWikiDocCtx, 815 doc_title: *u8, doc_title_n: i64) -> i64 { 816 if ctx.valid != 1 { return 0 - NX_WIKI_DOC_BAD_INPUT } 817 if doc_title_n > NX_WIKI_DOC_MAX_TITLE_LEN { return 0 - NX_WIKI_DOC_BAD_INPUT } 818 819 let rc_hdr: i64 = nx_wiki_doc_write_header(ctx, doc_title, doc_title_n) 820 if rc_hdr != NX_WIKI_DOC_OK { return rc_hdr } 821 822 // NO pre-markdown cite pass: the [[cite:<cid>]] tokens are left LITERAL in the 823 // body so they survive markdown unescaped ('['/'']' are not html_escape-unsafe 824 // and "[[cite:...]]" is not markdown syntax) and are expanded by the POST- 825 // markdown cite pass below -- emitting the card RAW (never re-escaped). This 826 // mirrors the [[wikilink]] split that killed the escaped-anchor bug. 827 let used_out: *i64 = sys_mmap(8) as *i64 828 let rc_pre: i64 = nx_wiki_doc_preprocess_wikilinks(ctx, used_out) 829 if rc_pre != NX_WIKI_DOC_OK { return rc_pre } 830 831 // wiki R4: render markdown into a TEMP buffer so the auto-TOC can be 832 // prepended and heading anchor ids injected before the body reaches out_buf. 833 // The TOC is built from the (cite-expanded) markdown source -- the SAME byte 834 // source the renderer reads -- so "#section-a" links and id="section-a" on 835 // the <h2> agree. Graceful (Cardinal 14): a TOC/inject step that overflows 836 // falls back to the plain markdown HTML. 837 let md_tmp: *u8 = sys_mmap(NX_WIKI_DOC_MD_TMP_CAP) 838 let md_rc: i64 = markdown_block(ctx.scratch_buf, used_out[0], md_tmp, NX_WIKI_DOC_MD_TMP_CAP) 839 if md_rc < 0 { return 0 - NX_WIKI_DOC_PARSE_FAILED } 840 841 let toc_buf: *u8 = sys_mmap(NX_WIKI_DOC_TOC_CAP) 842 let toc_used: *i64 = sys_mmap(8) as *i64 843 toc_used[0] = 0 844 let rc_toc: i64 = nx_wiki_toc_build(ctx.src_buf, ctx.src_len, 845 toc_buf, NX_WIKI_DOC_TOC_CAP, toc_used) 846 let inj_buf: *u8 = sys_mmap(NX_WIKI_DOC_INJECT_CAP) 847 let inj_used: *i64 = sys_mmap(8) as *i64 848 inj_used[0] = 0 849 let rc_inj: i64 = nx_wiki_toc_inject_ids(md_tmp, md_rc, 850 inj_buf, NX_WIKI_DOC_INJECT_CAP, inj_used) 851 852 if rc_toc == NX_WTOC_OK { 853 if toc_used[0] > 0 { 854 let rc_wt: i64 = nx_wiki_doc_write_run(ctx, toc_buf, toc_used[0]) 855 if rc_wt != NX_WIKI_DOC_OK { return rc_wt } 856 } 857 } 858 var body_src: *u8 = md_tmp 859 var body_n2: i64 = md_rc 860 if rc_inj == NX_WTOC_OK { body_src = inj_buf; body_n2 = inj_used[0] } 861 862 // POST-markdown [[wikilink]] pass: replace each literal [[name]] surviving in 863 // the rendered HTML with a LIVE <a>/<span> emitted RAW (never re-escaped). 864 // Graceful (Cardinal 14): on overflow we fall back to the pre-pass body. 865 let wl_buf: *u8 = sys_mmap(NX_WIKI_DOC_INJECT_CAP) 866 let wl_used: *i64 = sys_mmap(8) as *i64 867 wl_used[0] = 0 868 let rc_wl: i64 = nx_wiki_doc_postpass_wikilinks(ctx, body_src, body_n2, 869 wl_buf, NX_WIKI_DOC_INJECT_CAP, wl_used) 870 if rc_wl == NX_WIKI_DOC_OK { body_src = wl_buf; body_n2 = wl_used[0] } 871 872 // POST-markdown [[cite:<cid>]] pass: replace each literal [[cite:<cid>]] 873 // surviving in the rendered HTML (the wikilink pass copies them through 874 // verbatim) with its license-aware supporting-source card emitted RAW (never 875 // re-escaped) -- cite_one does the archive lookup + license gate + liar-kill. 876 // Graceful (Cardinal 14): on overflow we fall back to the wikilink-pass body. 877 let ct_buf: *u8 = sys_mmap(NX_WIKI_DOC_INJECT_CAP) 878 let ct_used: *i64 = sys_mmap(8) as *i64 879 ct_used[0] = 0 880 let rc_ct: i64 = nx_wiki_doc_postpass_cite(ctx, WAR_PREFIX, body_src, body_n2, 881 ct_buf, NX_WIKI_DOC_INJECT_CAP, ct_used) 882 if rc_ct == NX_WIKI_DOC_OK { body_src = ct_buf; body_n2 = ct_used[0] } 883 884 let rc_wb: i64 = nx_wiki_doc_write_run(ctx, body_src, body_n2) 885 if rc_wb != NX_WIKI_DOC_OK { return rc_wb } 886 887 return nx_wiki_doc_write_footer(ctx) 888} 889 890// ===== Reporter: counts of resolved + broken wikilinks ================================================= 891// 892// Caller queries after render to surface broken-wikilink audit warnings 893// (per NISHI_WIKI_CHARTER §S3). 894 895func nx_wiki_doc_wikilinks_resolved(ctx: *NxWikiDocCtx) -> i64 { 896 if ctx.valid != 1 { return 0 } 897 return ctx.wikilinks_resolved 898} 899 900func nx_wiki_doc_wikilinks_broken(ctx: *NxWikiDocCtx) -> i64 { 901 if ctx.valid != 1 { return 0 } 902 return ctx.wikilinks_broken 903}