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 <,> and the link rendered as DEAD ESCAPED TEXT (<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 <,> and the served bytes were DEAD ESCAPED TEXT (<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> · pure NishiLang, no third-party framework · <a href=\"/wiki/\">browse all pages</a> · 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}