code wiki / wiki / nx_wiki_cite_render.nx

nx_wiki_cite_render.nx source

↩ module page · 267 lines · 14865 B

1// nx_wiki_cite_render.nx -- INLINE SUPPORTING-SOURCE RENDER (the anti-pushout). 2// 3// THE WIKIPEDIA-EXCEED DIFFERENTIATOR: 4// Wikipedia's awful pushout = you click a citation and LEAVE the wiki to read 5// a source that may be paywalled, rotted, or gone. We kill that: a citation 6// token [[cite:<cid>]] in a page body renders the cited supporting source 7// INLINE -- an expandable "read source here" card pulled from the no-link-rot 8// content-addressed archive -- so the reader reads the evidence IN PLACE. 9// 10// ...BUT ONLY WHEN LICENSING PERMITS HOSTING IT. This is the critical 11// liar-kill: public-domain / CC0 / every Creative-Commons variant = we may 12// redistribute the bytes, so we inline them (with attribution). Proprietary / 13// unknown = we are NOT licensed to host the content, so we render a LINK + 14// license note ONLY and the source bytes NEVER appear in the output. We never 15// inline content we are not licensed to host. 16// 17// CITATION SYNTAX (page-body token): [[cite:<cid>]] 18// where <cid> is an archive content id, e.g. nxc1-<64hex> (or any 19// NUL-safe slug the archive resolves via war_get_by_cid). The double-bracket 20// shape mirrors the existing [[wikilink]] preprocessor; the "cite:" prefix 21// disambiguates a supporting-source citation from a cross-reference link. 22// 23// THREE OUTCOMES per citation (decided from REAL archive + license lookups): 24// 1. archived AND hostable -> INLINE EXPANDABLE card containing the 25// HTML-ESCAPED source bytes (read in place). 26// 2. archived, NOT hostable -> LINK + license note only; source bytes OMITTED. 27// 3. not archived -> "(source archiving pending)" placeholder. 28// 29// XSS: every inlined source byte goes through html_escape (the sealed escaper) 30// before reaching the output -- a source can never inject markup. 31// 32// FRONTEND: nx_wiki_cite_css() emits clean, self-contained CSS for the card 33// (sovereign-emitted by this organ; no third-party JS/CSS framework). 34// 35// IMPORTS: nx_wiki_license.nx (license decision + getters; transitively the 36// archive + seg_store + canon_cid, each spliced ONCE by module-identity dedup) 37// and nx_html_escape.nx (the escaper; a DISTINCT module from the store chain, 38// so no double-import). Pure NishiLang, NO SQL, NO .sh/.py/.js, no new 39// .tsv/.conf. license_tier: ORIGINAL 40import "nx_wiki_license.nx" 41import "nx_html_escape.nx" 42 43// ===== sealed verdict / sizing constants ===================================== 44const NXCITE_OK: i64 = 0 45const NXCITE_OUTPUT_OVERFLOW: i64 = 1 46const NXCITE_BAD_INPUT: i64 = 2 47 48const NXCITE_CAP: i64 = 64 // archive segment-scan cap (>> our commits) 49const NXCITE_MAX_CID: i64 = 128 // a CID is ~69 bytes; this is headroom 50const NXCITE_MAX_ITER: i64 = 1000000 // hard outer-walk cap (M3 bounded loops) 51 52// ASCII byte literals used as named constants (no magic numbers). 53const NXCITE_LBRACKET: i64 = 0x5B // '[' 54const NXCITE_RBRACKET: i64 = 0x5D // ']' 55const NXCITE_COLON: i64 = 0x3A // ':' 56 57// ---- tiny local helpers (cr_ namespace; no clash with ss_/war_/he_) ---- 58func cr_len(s: *u8) -> i64 { 59 var n: i64 = 0 60 while s[n] != (0 as u8) { n = n + 1 } 61 return n 62} 63 64// concat NUL-terminated s into dst at off (no terminator); returns new off. 65// THIS is how literal prefixes are built (NOT by indexing a const *u8). 66func cr_cat(dst: *u8, off: i64, s: *u8) -> i64 { 67 var i: i64 = 0 68 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 } 69 return off + i 70} 71 72// copy a counted byte run src[0..n] into dst at off; returns new off. 73func cr_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { 74 var i: i64 = 0 75 while i < n { dst[off + i] = src[i]; i = i + 1 } 76 return off + i 77} 78 79// ===== FRONTEND: self-contained CSS for the inline "read source here" card ==== 80// Emitted by this organ (sovereign; no framework). A tasteful expandable card: 81// a subtle bordered block, a clickable summary that reads "Read source (...)", 82// a readable monospace-ish body, and a muted attribution footer. The point is 83// the inline-read UX that beats the push-out. Returns bytes written into out, or 84// NXCITE_OUTPUT_OVERFLOW. Idempotent (pure literal emit). 85func nx_wiki_cite_css(out: *u8, cap: i64) -> i64 { 86 let css: *u8 = "<style>\n.nx-cite{display:block;margin:1em 0;border:1px solid #d4dbe3;border-left:4px solid #2c7a7b;border-radius:8px;background:#f7fafc;overflow:hidden;font-family:system-ui,-apple-system,Segoe UI,sans-serif}\n.nx-cite>summary{cursor:pointer;padding:.6em .9em;font-weight:600;color:#234e52;background:#e6fffa;list-style:none;user-select:none;display:flex;align-items:center;gap:.5em;transition:background .15s}\n.nx-cite>summary::before{content:\"\\1F4D6\";font-weight:400}\n.nx-cite>summary::-webkit-details-marker{display:none}\n.nx-cite>summary:hover{background:#b2f5ea}\n.nx-cite[open]>summary{border-bottom:1px solid #d4dbe3}\n.nx-cite-body{padding:.9em 1.1em;white-space:pre-wrap;word-wrap:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em;line-height:1.55;color:#1a202c;background:#fff;max-height:30em;overflow:auto}\n.nx-cite-attr{padding:.5em .9em;font-size:.8em;color:#718096;background:#edf2f7;border-top:1px solid #e2e8f0}\n.nx-cite-attr::before{content:\"\\2713 archived \";color:#2c7a7b;font-weight:600}\n.nx-cite-link{display:inline-block;padding:.15em .5em;border-left:3px solid #a0aec0;background:#f7fafc;color:#4a5568;font-size:.92em}\n.nx-cite-link a{color:#2b6cb0;text-decoration:none;font-weight:600}\n.nx-cite-link a:hover{text-decoration:underline}\n.nx-cite-pending{display:inline-block;padding:.1em .45em;border-radius:4px;background:#fefcbf;color:#744210;font-size:.85em;font-style:italic}\n</style>\n" as *u8 87 let n: i64 = cr_len(css) 88 if n > cap { return NXCITE_OUTPUT_OVERFLOW } 89 cr_catb(out, 0, css, n) 90 return n 91} 92 93// ===== render ONE citation's HTML for cid into out at off ==================== 94// Decides inline / link-only / pending from REAL lookups (war_get_by_cid + 95// nx_wiki_license_get) and writes the corresponding HTML. cid is NUL-terminated. 96// prefix = archive store prefix. Returns the new off (>=0), or a negative 97// NXCITE_* verdict on overflow/bad-input. The source bytes are emitted ONLY on 98// the hostable path, and ONLY after html_escape (XSS + the liar-kill). 99func nx_wiki_cite_one(prefix: *u8, cid: *u8, out: *u8, cap: i64, off: i64) -> i64 { 100 if (cid as i64) == 0 { return 0 - NXCITE_BAD_INPUT } 101 var o: i64 = off 102 let cidn: i64 = cr_len(cid) 103 104 // ---- archive lookup: is this source archived? ---- 105 let pp: *i64 = sys_mmap(16) as *i64 106 let slen: i64 = war_get_by_cid(prefix, cid, pp, NXCITE_CAP) 107 // war_get_by_cid: >0 len = present, 0 = tombstoned, -1 = absent. 108 109 if slen <= 0 { 110 // ---- OUTCOME 3: not archived -> pending placeholder ---- 111 let s3: *u8 = "<span class=\"nx-cite-pending\">(source archiving pending)</span>" as *u8 112 let n3: i64 = cr_len(s3) 113 if o + n3 > cap { return 0 - NXCITE_OUTPUT_OVERFLOW } 114 o = cr_cat(out, o, s3) 115 return o 116 } 117 118 // archived -> consult the license layer (the hostable decision). 119 let cls: i64 = nx_wiki_license_get(prefix, cid) 120 let licname: *u8 = nx_wiki_license_name(cls) 121 let hostable: i64 = nx_wiki_license_hostable(cls) 122 123 if hostable != 1 { 124 // ---- OUTCOME 2: archived but NOT hostable -> LINK + license note ---- 125 // The source BYTES are deliberately NOT emitted. We render a wayback 126 // link to the archived blob plus the licensing reason. 127 // <span class="nx-cite-link"><a href="/wiki/wayback/<cid>">source</a> 128 // - licensing restricts inline hosting (<license-name>)</span> 129 let pre: *u8 = "<span class=\"nx-cite-link\"><a href=\"/wiki/wayback/" as *u8 130 let mid: *u8 = "\">source</a> &mdash; licensing restricts inline hosting (" as *u8 131 let suf: *u8 = ")</span>" as *u8 132 let need: i64 = cr_len(pre) + cidn + cr_len(mid) + cr_len(licname) + cr_len(suf) 133 if o + need > cap { return 0 - NXCITE_OUTPUT_OVERFLOW } 134 o = cr_cat(out, o, pre) 135 o = cr_catb(out, o, cid, cidn) // cid is content-addressed -> URL-safe 136 o = cr_cat(out, o, mid) 137 o = cr_cat(out, o, licname) 138 o = cr_cat(out, o, suf) 139 return o 140 } 141 142 // ---- OUTCOME 1: archived AND hostable -> INLINE EXPANDABLE card ---- 143 // <details class="nx-cite"><summary>Read source (<license-name>)</summary> 144 // <div class="nx-cite-body"><HTML-ESCAPED source bytes></div> 145 // <div class="nx-cite-attr">Source archived <cid> &middot; <license-name></div> 146 // </details> 147 let src: *u8 = pp[0] as *u8 148 let d_open: *u8 = "<details class=\"nx-cite\"><summary>Read source (" as *u8 149 let d_sum2: *u8 = ")</summary><div class=\"nx-cite-body\">" as *u8 150 // fixed-overhead bound (the escaped body is bounded by slen*6 per HE1). 151 let fixed: i64 = cr_len(d_open) + cr_len(licname) + cr_len(d_sum2) 152 if o + fixed > cap { return 0 - NXCITE_OUTPUT_OVERFLOW } 153 o = cr_cat(out, o, d_open) 154 o = cr_cat(out, o, licname) 155 o = cr_cat(out, o, d_sum2) 156 157 // HTML-ESCAPE the source bytes straight into the output tail (the XSS + 158 // liar-kill boundary -- nothing reaches the page un-escaped). 159 let avail: i64 = cap - o 160 if avail <= 0 { return 0 - NXCITE_OUTPUT_OVERFLOW } 161 let tail: *u8 = (out as i64 + o) as *u8 162 let wrote: i64 = html_escape(tail, avail, src, slen) 163 if wrote < 0 { return 0 - NXCITE_OUTPUT_OVERFLOW } 164 o = o + wrote 165 166 // close body + attribution footer (with the cid + license name). 167 let a_open: *u8 = "</div><div class=\"nx-cite-attr\">" as *u8 168 let a_mid: *u8 = " &middot; " as *u8 169 let a_close: *u8 = "</div></details>" as *u8 170 let need2: i64 = cr_len(a_open) + cidn + cr_len(a_mid) + cr_len(licname) + cr_len(a_close) 171 if o + need2 > cap { return 0 - NXCITE_OUTPUT_OVERFLOW } 172 o = cr_cat(out, o, a_open) 173 o = cr_catb(out, o, cid, cidn) 174 o = cr_cat(out, o, a_mid) 175 o = cr_cat(out, o, licname) 176 o = cr_cat(out, o, a_close) 177 return o 178} 179 180// ===== render a page body: replace every [[cite:<cid>]] with its HTML ======== 181// Walks src once; non-citation bytes are copied verbatim; each [[cite:<cid>]] 182// token is replaced by nx_wiki_cite_one's HTML (inline / link / pending). 183// Other [[...]] tokens (plain wikilinks etc.) are copied through untouched so 184// this can run as a pre/post pass alongside the existing wikilink preprocessor. 185// Writes into out; final length via out_used[0]. Returns NXCITE_OK or a 186// negative verdict. prefix = archive store prefix. 187func nx_wiki_cite_render(prefix: *u8, src: *u8, src_n: i64, 188 out: *u8, out_cap: i64, out_used: *i64) -> i64 { 189 if (src as i64) == 0 { return 0 - NXCITE_BAD_INPUT } 190 if (out as i64) == 0 { return 0 - NXCITE_BAD_INPUT } 191 out_used[0] = 0 192 let cidbuf: *u8 = sys_mmap(NXCITE_MAX_CID + 16) 193 var i: i64 = 0 194 var w: i64 = 0 195 var iter: i64 = 0 196 while i < src_n { 197 if iter >= NXCITE_MAX_ITER { return 0 - NXCITE_BAD_INPUT } 198 iter = iter + 1 199 var consumed: i64 = 0 200 201 // ---- detect "[[cite:" at i ---- 202 // Need at least "[[cite:" (7) + "]]" (2) headroom to be a citation. 203 if i + 7 < src_n { 204 if src[i] == (NXCITE_LBRACKET as u8) { 205 if src[i + 1] == (NXCITE_LBRACKET as u8) { 206 if src[i + 2] == (99 as u8) { // 'c' 207 if src[i + 3] == (105 as u8) { // 'i' 208 if src[i + 4] == (116 as u8) { // 't' 209 if src[i + 5] == (101 as u8) { // 'e' 210 if src[i + 6] == (NXCITE_COLON as u8) { // ':' 211 // PHASE 1: pure scan for the closing "]]" 212 // (no side effects -- separating scan 213 // from processing avoids break-in-loop 214 // codegen-desync, per the doc-render note). 215 let cid_start: i64 = i + 7 216 var end: i64 = cid_start 217 var found: i64 = 0 218 var scan: i64 = cid_start 219 while scan < src_n - 1 { 220 if found == 0 { 221 if src[scan] == (NXCITE_RBRACKET as u8) { 222 if src[scan + 1] == (NXCITE_RBRACKET as u8) { 223 found = 1 224 end = scan 225 } 226 } 227 } 228 scan = scan + 1 229 } 230 // PHASE 2: process once, after the scan. 231 if found == 1 { 232 let cid_n: i64 = end - cid_start 233 if cid_n > 0 { 234 if cid_n < NXCITE_MAX_CID { 235 // copy cid out NUL-terminated 236 var c: i64 = 0 237 while c < cid_n { cidbuf[c] = src[cid_start + c]; c = c + 1 } 238 cidbuf[cid_n] = 0 as u8 239 // render this citation's HTML 240 let nw: i64 = nx_wiki_cite_one(prefix, cidbuf, out, out_cap, w) 241 if nw < 0 { return nw } 242 w = nw 243 i = end + 2 // resume after ]] 244 consumed = 1 245 } 246 } 247 } 248 } 249 } 250 } 251 } 252 } 253 } 254 } 255 } 256 257 // ---- default: copy one byte verbatim ---- 258 if consumed == 0 { 259 if w + 1 > out_cap { return 0 - NXCITE_OUTPUT_OVERFLOW } 260 out[w] = src[i] 261 w = w + 1 262 i = i + 1 263 } 264 } 265 out_used[0] = w 266 return NXCITE_OK 267}