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> — 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> · <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 = " · " 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}