nx_unidoc_lib.nx source
↩ module page · 518 lines · 28920 B
1// nx_unidoc_lib.nx -- THE UNIFIED DOCUMENT PAGE (rung UD1 of /compare/unidoc, 2026-09-03).
2//
3// Operator (2026-09-03): "we want nishi browser to handle documents like webpages securely but with full
4// interactivity so we dont have to have a file browser and a web browser etc but a unified portal."
5//
6// ONE page per document, whatever it was born as. A .docx, .odt, .pdf, .md, .csv or .txt becomes the SAME
7// zero-script HTML page: the office's own document model (the H/P/T/B/I spec lines that nx_office_serve
8// edits, versions and signs) rendered as a page, with a Content-Security-Policy that forbids every script
9// and every foreign fetch, every byte of document text escaped BY CONSTRUCTION, and the portal's actions
10// (original bytes, versions, annotate, sign) as plain HTML forms -- no client script anywhere, so the page
11// is native in the Nishi browser and NishiOS first and degrades to nothing in a third-party browser.
12//
13// A web page is NOT a document: .html and .htm are REFUSED here by name (UD_ERR_REFUSED_HTML). The browser
14// already serves a page as itself; wrapping one as a document would be the exact confusion this organ ends.
15//
16// Composition, never re-implementation: .docx and .odt go through the promoted format organs' `spec` verbs
17// (_offc/nx_docx.elf, _offc/nx_odt.elf -- the same verbs the office import uses), .pdf through the proven
18// text extractor _offc/nx_pdf_text.elf, all captured with nx_tool_run. Markdown, CSV and text are folded
19// to the spec here because their grammar IS the spec's grammar.
20//
21// Every refusal is NAMED (ud_err_name) and the input ceiling is ANNOUNCED, never a silent truncation.
22// license_tier: ORIGINAL
23import "nx_syscalls.nx"
24import "nx_itoa_lib.nx"
25import "nx_tool_run.nx"
26
27// document kinds (the value the registry dispatch and the page label both key on)
28const UD_KIND_UNKNOWN: i64 = 0
29const UD_KIND_DOCX: i64 = 1
30const UD_KIND_ODT: i64 = 2
31const UD_KIND_PDF: i64 = 3
32const UD_KIND_MD: i64 = 4
33const UD_KIND_CSV: i64 = 5
34const UD_KIND_TXT: i64 = 6
35const UD_KIND_HTML: i64 = 7 // recognised so it can be REFUSED by name, never rendered as a document
36
37// named refusals (negative, distinct, each with a name in ud_err_name)
38const UD_ERR_UNKNOWN_KIND: i64 = 0 - 1
39const UD_ERR_REFUSED_HTML: i64 = 0 - 2
40const UD_ERR_RENDERER: i64 = 0 - 3 // the format organ failed or handed back no spec
41const UD_ERR_TOO_BIG: i64 = 0 - 4 // above UD_IN_CAP: refused by name, never cut
42const UD_ERR_READ: i64 = 0 - 5 // the input could not be read
43const UD_ERR_OUT_CAP: i64 = 0 - 6 // the page would not fit the caller's buffer: refused, never truncated
44
45// ceilings -- each one NAMED and ANNOUNCED on refusal (rule 11: a ceiling that truncates in silence is a defect)
46const UD_IN_CAP: i64 = 8388608 // 8 MiB of document bytes per page
47const UD_SPEC_CAP: i64 = 4194304 // the folded spec (H/P/T lines)
48const UD_CAPTURE_CAP: i64 = 4194320 // a format organ's captured stdout (spec + its BEGIN/END markers)
49const UD_PATH_CAP: i64 = 4096
50const UD_EXT_MAX: i64 = 12 // the browser's own br_doc_ext bound
51const UD_ARGV_SLOTS: i64 = 6
52const UD_PTR_BYTES: i64 = 8
53const UD_SLACK: i64 = 4096 // headroom the page emitter keeps before refusing UD_ERR_OUT_CAP
54
55const UD_DOCX_ELF: *u8 = "_offc/nx_docx.elf"
56const UD_ODT_ELF: *u8 = "_offc/nx_odt.elf"
57const UD_PDFTEXT_ELF: *u8 = "_offc/nx_pdf_text.elf"
58const UD_PDF_TXT_SCRATCH: *u8 = "/tmp/nx_unidoc_pdf.txt"
59
60// ASCII the folders key on
61const UD_NL: i64 = 10
62const UD_CR: i64 = 13
63const UD_TAB: i64 = 9
64const UD_SP: i64 = 32
65const UD_HASH: i64 = 35
66const UD_DASH: i64 = 45
67const UD_STAR: i64 = 42
68const UD_PLUS: i64 = 43
69const UD_PIPE: i64 = 124
70const UD_SLASH: i64 = 47
71const UD_COMMA: i64 = 44
72const UD_QUOTE: i64 = 34
73const UD_DOT: i64 = 46
74const UD_LT: i64 = 60
75const UD_GT: i64 = 62
76const UD_AMP: i64 = 38
77const UD_APOS: i64 = 39
78const UD_BACKTICK: i64 = 96
79const UD_H: i64 = 72
80const UD_P: i64 = 80
81const UD_T: i64 = 84
82const UD_B: i64 = 66
83const UD_I: i64 = 73
84const UD_MD_HEADING_MAX: i64 = 6
85
86func ud_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
87func ud_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; var o: i64 = off; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o }
88func ud_catn(dst: *u8, off: i64, v: i64) -> i64 { return nxi_buf(dst, off, v) }
89func ud_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
90func ud_streq(a: *u8, b: *u8) -> i64 {
91 var i: i64 = 0
92 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
93 if b[i] != (0 as u8) { return 0 }
94 return 1
95}
96// does hay[0..n) contain needle (NUL-terminated)? -1 if not, else the offset
97func ud_find(hay: *u8, n: i64, needle: *u8) -> i64 {
98 let nl: i64 = ud_slen(needle)
99 if nl == 0 { return 0 }
100 var i: i64 = 0
101 while i + nl <= n {
102 var j: i64 = 0
103 var m: i64 = 1
104 while j < nl { if hay[i + j] != needle[j] { m = 0; j = nl } else { j = j + 1 } }
105 if m == 1 { return i }
106 i = i + 1
107 }
108 return 0 - 1
109}
110func ud_count(hay: *u8, n: i64, needle: *u8) -> i64 {
111 let nl: i64 = ud_slen(needle)
112 if nl == 0 { return 0 }
113 var c: i64 = 0
114 var i: i64 = 0
115 while i + nl <= n {
116 var j: i64 = 0
117 var m: i64 = 1
118 while j < nl { if hay[i + j] != needle[j] { m = 0; j = nl } else { j = j + 1 } }
119 if m == 1 { c = c + 1; i = i + nl } else { i = i + 1 }
120 }
121 return c
122}
123
124// ---- THE SECURITY PRIMITIVE: every byte of document text passes through here before it is HTML -------------
125// Five characters can change the meaning of a page; all five are turned into references. Everything else is
126// copied as-is (UTF-8 passes untouched). A document that CONTAINS the text "<script>" renders as that text.
127func ud_esc_n(out: *u8, off: i64, src: *u8, a: i64, b: i64) -> i64 {
128 var o: i64 = off
129 var i: i64 = a
130 while i < b {
131 let c: i64 = src[i] as i64
132 if c == UD_AMP { o = ud_cat(out, o, "&" as *u8) } else {
133 if c == UD_LT { o = ud_cat(out, o, "<" as *u8) } else {
134 if c == UD_GT { o = ud_cat(out, o, ">" as *u8) } else {
135 if c == UD_QUOTE { o = ud_cat(out, o, """ as *u8) } else {
136 if c == UD_APOS { o = ud_cat(out, o, "'" as *u8) } else { out[o] = src[i]; o = o + 1 } } } } }
137 i = i + 1
138 }
139 return o
140}
141func ud_esc(out: *u8, off: i64, s: *u8) -> i64 { return ud_esc_n(out, off, s, 0, ud_slen(s)) }
142
143// ---- kind detection: the lowercased extension of the last path segment ------------------------------------
144func ud_ext(path: *u8, ext: *u8) -> i64 {
145 let n: i64 = ud_slen(path)
146 var dot: i64 = 0 - 1
147 var slash: i64 = 0 - 1
148 var i: i64 = 0
149 while i < n {
150 let c: i64 = path[i] as i64
151 if c == UD_SLASH { slash = i; dot = 0 - 1 } else { if c == UD_DOT { dot = i } }
152 i = i + 1
153 }
154 ext[0] = 0 as u8
155 if dot < 0 { return 0 }
156 if dot <= slash { return 0 }
157 var k: i64 = 0
158 i = dot + 1
159 while i < n {
160 if k >= UD_EXT_MAX { ext[0] = 0 as u8; return 0 }
161 ext[k] = ud_lower(path[i] as i64) as u8
162 k = k + 1
163 i = i + 1
164 }
165 ext[k] = 0 as u8
166 return k
167}
168func ud_kind_of_ext(ext: *u8) -> i64 {
169 if ud_streq(ext, "docx" as *u8) == 1 { return UD_KIND_DOCX }
170 if ud_streq(ext, "odt" as *u8) == 1 { return UD_KIND_ODT }
171 if ud_streq(ext, "pdf" as *u8) == 1 { return UD_KIND_PDF }
172 if ud_streq(ext, "md" as *u8) == 1 { return UD_KIND_MD }
173 if ud_streq(ext, "markdown" as *u8) == 1 { return UD_KIND_MD }
174 if ud_streq(ext, "csv" as *u8) == 1 { return UD_KIND_CSV }
175 if ud_streq(ext, "txt" as *u8) == 1 { return UD_KIND_TXT }
176 if ud_streq(ext, "text" as *u8) == 1 { return UD_KIND_TXT }
177 if ud_streq(ext, "html" as *u8) == 1 { return UD_KIND_HTML }
178 if ud_streq(ext, "htm" as *u8) == 1 { return UD_KIND_HTML }
179 return UD_KIND_UNKNOWN
180}
181func ud_kind_of_path(path: *u8) -> i64 {
182 let ext: *u8 = sys_mmap(UD_EXT_MAX + 4)
183 let k: i64 = ud_ext(path, ext)
184 if k == 0 { return UD_KIND_UNKNOWN }
185 return ud_kind_of_ext(ext)
186}
187func ud_kind_label(kind: i64) -> *u8 {
188 if kind == UD_KIND_DOCX { return "Word document" as *u8 }
189 if kind == UD_KIND_ODT { return "OpenDocument text" as *u8 }
190 if kind == UD_KIND_PDF { return "PDF document (text layer)" as *u8 }
191 if kind == UD_KIND_MD { return "Markdown" as *u8 }
192 if kind == UD_KIND_CSV { return "CSV table" as *u8 }
193 if kind == UD_KIND_TXT { return "Plain text" as *u8 }
194 if kind == UD_KIND_HTML { return "web page (refused as a document)" as *u8 }
195 return "unknown" as *u8
196}
197func ud_kind_name(kind: i64) -> *u8 {
198 if kind == UD_KIND_DOCX { return "docx" as *u8 }
199 if kind == UD_KIND_ODT { return "odt" as *u8 }
200 if kind == UD_KIND_PDF { return "pdf" as *u8 }
201 if kind == UD_KIND_MD { return "md" as *u8 }
202 if kind == UD_KIND_CSV { return "csv" as *u8 }
203 if kind == UD_KIND_TXT { return "txt" as *u8 }
204 if kind == UD_KIND_HTML { return "html" as *u8 }
205 return "unknown" as *u8
206}
207func ud_err_name(code: i64) -> *u8 {
208 if code == UD_ERR_UNKNOWN_KIND { return "UNKNOWN-KIND (no renderer for this extension; add a kind, never guess)" as *u8 }
209 if code == UD_ERR_REFUSED_HTML { return "REFUSED-HTML (a web page is served as itself, never wrapped as a document)" as *u8 }
210 if code == UD_ERR_RENDERER { return "RENDERER-FAILED (the format organ exited non-zero or handed back no spec)" as *u8 }
211 if code == UD_ERR_TOO_BIG { return "TOO-BIG (above the announced UD_IN_CAP of 8388608 bytes; refused, never cut)" as *u8 }
212 if code == UD_ERR_READ { return "READ-FAILED (the input path could not be read)" as *u8 }
213 if code == UD_ERR_OUT_CAP { return "OUT-CAP (the page would not fit the caller's buffer; refused, never truncated)" as *u8 }
214 return "OK" as *u8
215}
216// the input ceiling as a decidable predicate (0 = admitted, UD_ERR_TOO_BIG = refused)
217func ud_check_size(n: i64) -> i64 { if n > UD_IN_CAP { return UD_ERR_TOO_BIG } return 0 }
218
219// ---- spec folders: every kind becomes H/P/T lines (the office's document model) ------------------------------
220// write one spec line: <tag> SP <text a..b with NL/TAB folded to SP and, in a table, PIPE folded to SLASH> NL
221func ud_spec_line(out: *u8, o0: i64, cap: i64, tag: i64, src: *u8, a: i64, b: i64, in_tbl: i64) -> i64 {
222 if o0 + (b - a) + 3 >= cap { return 0 - 1 }
223 var o: i64 = o0
224 out[o] = tag as u8; out[o + 1] = UD_SP as u8; o = o + 2
225 var k: i64 = a
226 while k < b {
227 var c: i64 = src[k] as i64
228 if c == UD_NL { c = UD_SP }
229 if c == UD_CR { c = UD_SP }
230 if c == UD_TAB { c = UD_SP }
231 if in_tbl == 1 { if c == UD_PIPE { c = UD_SLASH } }
232 out[o] = c as u8; o = o + 1; k = k + 1
233 }
234 out[o] = UD_NL as u8
235 return o + 1
236}
237// plain text: paragraphs separated by one or more blank lines
238func ud_spec_from_text(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
239 var o: i64 = 0
240 var ps: i64 = 0 - 1 // paragraph start, -1 = none open
241 var i: i64 = 0
242 while i <= n {
243 var e: i64 = i
244 while e < n { if src[e] == (UD_NL as u8) { break } e = e + 1 }
245 var end: i64 = e
246 if end > i { if src[end - 1] == (UD_CR as u8) { end = end - 1 } }
247 var blank: i64 = 1
248 var q: i64 = i
249 while q < end { let c: i64 = src[q] as i64; if c != UD_SP { if c != UD_TAB { blank = 0; q = end } } q = q + 1 }
250 if blank == 1 {
251 if ps >= 0 { let no: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if no < 0 { return UD_ERR_OUT_CAP } o = no; ps = 0 - 1 }
252 } else { if ps < 0 { ps = i } }
253 i = e + 1
254 }
255 if ps >= 0 { let no2: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, n, 0); if no2 < 0 { return UD_ERR_OUT_CAP } o = no2 }
256 out[o] = 0 as u8
257 return o
258}
259// markdown: '#' headings -> H, list items -> P prefixed "- ", fenced code lines -> P, blank line ends a paragraph
260func ud_spec_from_md(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
261 var o: i64 = 0
262 var ps: i64 = 0 - 1
263 var infence: i64 = 0
264 var i: i64 = 0
265 while i < n {
266 var e: i64 = i
267 while e < n { if src[e] == (UD_NL as u8) { break } e = e + 1 }
268 var end: i64 = e
269 if end > i { if src[end - 1] == (UD_CR as u8) { end = end - 1 } }
270 var fence: i64 = 0
271 if end >= i + 3 { if src[i] == (UD_BACKTICK as u8) { if src[i + 1] == (UD_BACKTICK as u8) { if src[i + 2] == (UD_BACKTICK as u8) { fence = 1 } } } }
272 if fence == 1 {
273 if ps >= 0 { let f0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if f0 < 0 { return UD_ERR_OUT_CAP } o = f0; ps = 0 - 1 }
274 if infence == 1 { infence = 0 } else { infence = 1 }
275 } else { if infence == 1 {
276 let f1: i64 = ud_spec_line(out, o, cap, UD_P, src, i, end, 0); if f1 < 0 { return UD_ERR_OUT_CAP } o = f1
277 } else {
278 var hn: i64 = 0
279 while i + hn < end { if src[i + hn] == (UD_HASH as u8) { hn = hn + 1 } else { break } }
280 var heading: i64 = 0
281 if hn >= 1 { if hn <= UD_MD_HEADING_MAX { if i + hn < end { if src[i + hn] == (UD_SP as u8) { heading = 1 } } } }
282 var list: i64 = 0
283 if end >= i + 2 { if src[i + 1] == (UD_SP as u8) { let c0: i64 = src[i] as i64; if c0 == UD_DASH { list = 1 } else { if c0 == UD_STAR { list = 1 } else { if c0 == UD_PLUS { list = 1 } } } } }
284 var blank: i64 = 1
285 var q: i64 = i
286 while q < end { let c: i64 = src[q] as i64; if c != UD_SP { if c != UD_TAB { blank = 0; q = end } } q = q + 1 }
287 if heading == 1 {
288 if ps >= 0 { let h0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if h0 < 0 { return UD_ERR_OUT_CAP } o = h0; ps = 0 - 1 }
289 let h1: i64 = ud_spec_line(out, o, cap, UD_H, src, i + hn + 1, end, 0); if h1 < 0 { return UD_ERR_OUT_CAP } o = h1
290 } else { if list == 1 {
291 if ps >= 0 { let l0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if l0 < 0 { return UD_ERR_OUT_CAP } o = l0; ps = 0 - 1 }
292 // "- " kept as the visible marker: the office model has no list block yet (the list-block watch on the unidoc board, rung UD5)
293 let l1: i64 = ud_spec_line(out, o, cap, UD_P, src, i, end, 0); if l1 < 0 { return UD_ERR_OUT_CAP } o = l1
294 } else { if blank == 1 {
295 if ps >= 0 { let b0: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, i - 1, 0); if b0 < 0 { return UD_ERR_OUT_CAP } o = b0; ps = 0 - 1 }
296 } else { if ps < 0 { ps = i } } } }
297 } }
298 i = e + 1
299 }
300 if ps >= 0 { let z: i64 = ud_spec_line(out, o, cap, UD_P, src, ps, n, 0); if z < 0 { return UD_ERR_OUT_CAP } o = z }
301 out[o] = 0 as u8
302 return o
303}
304// csv: one T row per line, cells joined by PIPE; a quoted cell keeps its commas, "" inside quotes is one quote
305func ud_spec_from_csv(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
306 var o: i64 = 0
307 var i: i64 = 0
308 while i < n {
309 var e: i64 = i
310 while e < n { if src[e] == (UD_NL as u8) { break } e = e + 1 }
311 var end: i64 = e
312 if end > i { if src[end - 1] == (UD_CR as u8) { end = end - 1 } }
313 if end > i {
314 if o + (end - i) * 2 + 3 >= cap { return UD_ERR_OUT_CAP }
315 out[o] = UD_T as u8; out[o + 1] = UD_SP as u8; o = o + 2
316 var inq: i64 = 0
317 var k: i64 = i
318 while k < end {
319 let c: i64 = src[k] as i64
320 if c == UD_QUOTE {
321 if inq == 1 { if k + 1 < end { if src[k + 1] == (UD_QUOTE as u8) { out[o] = UD_QUOTE as u8; o = o + 1; k = k + 1 } else { inq = 0 } } else { inq = 0 } } else { inq = 1 }
322 } else { if c == UD_COMMA { if inq == 0 { out[o] = UD_PIPE as u8; o = o + 1 } else { out[o] = UD_COMMA as u8; o = o + 1 } }
323 else { if c == UD_PIPE { out[o] = UD_SLASH as u8; o = o + 1 } else { if c == UD_TAB { out[o] = UD_SP as u8; o = o + 1 } else { out[o] = c as u8; o = o + 1 } } } }
324 k = k + 1
325 }
326 out[o] = UD_NL as u8; o = o + 1
327 }
328 i = e + 1
329 }
330 out[o] = 0 as u8
331 return o
332}
333// the promoted format organs' `spec` verb, captured: everything between "-SPEC-BEGIN\n" and the "-SPEC-END len=" line
334func ud_spec_from_fork(elf: *u8, path: *u8, out: *u8, cap: i64) -> i64 {
335 let argv: *i64 = sys_mmap(UD_PTR_BYTES * UD_ARGV_SLOTS) as *i64
336 argv[0] = elf as i64
337 argv[1] = ("spec" as *u8) as i64
338 argv[2] = path as i64
339 argv[3] = 0
340 let capbuf: *u8 = sys_mmap(UD_CAPTURE_CAP)
341 let lenp: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64
342 lenp[0] = 0
343 let rc: i64 = tr_run_capture(elf, argv, capbuf, UD_CAPTURE_CAP - 16, lenp)
344 let n: i64 = lenp[0]
345 if rc != 0 { return UD_ERR_RENDERER }
346 let b: i64 = ud_find(capbuf, n, "-SPEC-BEGIN\n" as *u8)
347 if b < 0 { return UD_ERR_RENDERER }
348 let s: i64 = b + 12
349 var e: i64 = ud_find(capbuf, n, "-SPEC-END len=" as *u8)
350 if e < 0 { return UD_ERR_RENDERER }
351 // the END marker starts its own line ("DOCX-SPEC-END" / "ODT-SPEC-END"): back up to the previous NL
352 while e > s { if capbuf[e - 1] == (UD_NL as u8) { break } e = e - 1 }
353 if e < s { return UD_ERR_RENDERER }
354 if e - s >= cap { return UD_ERR_OUT_CAP }
355 var o: i64 = 0
356 var i: i64 = s
357 while i < e { out[o] = capbuf[i]; o = o + 1; i = i + 1 }
358 out[o] = 0 as u8
359 return o
360}
361// pdf: the proven extractor writes a text file; control bytes other than NL/TAB/CR mark a non-text line, dropped
362func ud_spec_from_pdf(path: *u8, out: *u8, cap: i64) -> i64 {
363 let argv: *i64 = sys_mmap(UD_PTR_BYTES * UD_ARGV_SLOTS) as *i64
364 argv[0] = UD_PDFTEXT_ELF as i64
365 argv[1] = path as i64
366 argv[2] = UD_PDF_TXT_SCRATCH as i64
367 argv[3] = 0
368 let capbuf: *u8 = sys_mmap(UD_CAPTURE_CAP)
369 let lenp: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64
370 lenp[0] = 0
371 let rc: i64 = tr_run_capture(UD_PDFTEXT_ELF, argv, capbuf, UD_CAPTURE_CAP - 16, lenp)
372 if rc != 0 { return UD_ERR_RENDERER }
373 let tl: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64
374 tl[0] = 0
375 let txt: *u8 = sys_read_file(UD_PDF_TXT_SCRATCH, tl)
376 if (txt as i64) == 0 { return UD_ERR_RENDERER }
377 let tn: i64 = tl[0]
378 if tn <= 0 { return UD_ERR_RENDERER }
379 // fold: keep only lines free of control bytes, then paragraph on blank lines
380 let clean: *u8 = sys_mmap(tn + 16)
381 var co: i64 = 0
382 var i: i64 = 0
383 while i < tn {
384 var e: i64 = i
385 while e < tn { if txt[e] == (UD_NL as u8) { break } e = e + 1 }
386 var ok: i64 = 1
387 var q: i64 = i
388 while q < e { let c: i64 = txt[q] as i64; if c < UD_SP { if c != UD_TAB { if c != UD_CR { ok = 0; q = e } } } q = q + 1 }
389 if ok == 1 { var k: i64 = i; while k < e { clean[co] = txt[k]; co = co + 1; k = k + 1 } }
390 clean[co] = UD_NL as u8; co = co + 1
391 i = e + 1
392 }
393 return ud_spec_from_text(clean, co, out, cap)
394}
395// dispatch by kind; the caller has already read the bytes (so the size ceiling is decided once, here)
396func ud_spec_of(kind: i64, path: *u8, bytes: *u8, n: i64, out: *u8, cap: i64) -> i64 {
397 if kind == UD_KIND_DOCX { return ud_spec_from_fork(UD_DOCX_ELF, path, out, cap) }
398 if kind == UD_KIND_ODT { return ud_spec_from_fork(UD_ODT_ELF, path, out, cap) }
399 if kind == UD_KIND_PDF { return ud_spec_from_pdf(path, out, cap) }
400 if kind == UD_KIND_MD { return ud_spec_from_md(bytes, n, out, cap) }
401 if kind == UD_KIND_CSV { return ud_spec_from_csv(bytes, n, out, cap) }
402 if kind == UD_KIND_TXT { return ud_spec_from_text(bytes, n, out, cap) }
403 if kind == UD_KIND_HTML { return UD_ERR_REFUSED_HTML }
404 return UD_ERR_UNKNOWN_KIND
405}
406
407// ---- the page: spec -> zero-script HTML with a fail-closed CSP and the portal's actions as plain forms ----------
408func ud_spec_to_html(out: *u8, off: i64, spec: *u8, sl: i64) -> i64 {
409 var o: i64 = off
410 var intable: i64 = 0
411 var i: i64 = 0
412 while i < sl {
413 var e: i64 = i
414 while e < sl { if spec[e] == (UD_NL as u8) { break } e = e + 1 }
415 let c0: i64 = spec[i] as i64
416 var cs: i64 = i
417 if e >= i + 2 { if spec[i + 1] == (UD_SP as u8) { cs = i + 2 } }
418 if c0 == UD_T {
419 if intable == 0 { o = ud_cat(out, o, "<div class=tbl><table><tbody>" as *u8); intable = 1 }
420 o = ud_cat(out, o, "<tr>" as *u8)
421 var cstart: i64 = cs
422 var k: i64 = cs
423 while k <= e {
424 if k == e { o = ud_cat(out, o, "<td>" as *u8); o = ud_esc_n(out, o, spec, cstart, k); o = ud_cat(out, o, "</td>" as *u8); k = k + 1 }
425 else { if spec[k] == (UD_PIPE as u8) { o = ud_cat(out, o, "<td>" as *u8); o = ud_esc_n(out, o, spec, cstart, k); o = ud_cat(out, o, "</td>" as *u8); cstart = k + 1; k = k + 1 } else { k = k + 1 } }
426 }
427 o = ud_cat(out, o, "</tr>\n" as *u8)
428 } else {
429 if intable == 1 { o = ud_cat(out, o, "</tbody></table></div>\n" as *u8); intable = 0 }
430 if c0 == UD_H { o = ud_cat(out, o, "<h2>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</h2>\n" as *u8) }
431 else { if c0 == UD_B { o = ud_cat(out, o, "<p><b>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</b></p>\n" as *u8) }
432 else { if c0 == UD_I { o = ud_cat(out, o, "<p><i>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</i></p>\n" as *u8) }
433 else { o = ud_cat(out, o, "<p>" as *u8); o = ud_esc_n(out, o, spec, cs, e); o = ud_cat(out, o, "</p>\n" as *u8) } } }
434 }
435 i = e + 1
436 }
437 if intable == 1 { o = ud_cat(out, o, "</tbody></table></div>\n" as *u8) }
438 return o
439}
440// house tokens on :root, referenced (never merely defined) below; mobile-first: one column, 24px targets,
441// wide tables scroll in their OWN container so the page never scrolls sideways
442func ud_css(out: *u8, off: i64) -> i64 {
443 var o: i64 = off
444 o = ud_cat(out, o, ":root{--nx-ink:#1a1d21;--nx-paper:#ffffff;--nx-ground:#eef1f5;--nx-band:#0f172a;--nx-band-ink:#ffffff;--nx-line:#cbd5e1;--nx-accent:#2563eb;--nx-muted:#64748b;--nx-wrap:46rem;--nx-target:44px}\n" as *u8)
445 o = ud_cat(out, o, "body{margin:0;background:var(--nx-ground);color:var(--nx-ink);font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}\n" as *u8)
446 o = ud_cat(out, o, "header{background:var(--nx-band);color:var(--nx-band-ink);padding:14px 18px}header .eyebrow{font-size:12px;letter-spacing:.08em;text-transform:uppercase;opacity:.8}header h1{margin:4px 0 0;font-size:19px;font-weight:600;word-break:break-word}\n" as *u8)
447 o = ud_cat(out, o, ".meta{max-width:var(--nx-wrap);margin:10px auto 0;padding:0 14px;color:var(--nx-muted);font-size:13px;word-break:break-all}\n" as *u8)
448 o = ud_cat(out, o, ".actions{max-width:var(--nx-wrap);margin:10px auto;padding:0 14px;display:flex;flex-wrap:wrap;gap:8px}.actions a,.actions button{display:inline-flex;align-items:center;min-height:var(--nx-target);padding:0 16px;border-radius:8px;border:1px solid var(--nx-line);background:var(--nx-paper);color:var(--nx-ink);text-decoration:none;font-size:15px}.actions button.primary{background:var(--nx-accent);color:var(--nx-band-ink);border-color:var(--nx-accent)}\n" as *u8)
449 o = ud_cat(out, o, ".actions form{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.actions textarea{min-height:var(--nx-target);width:100%;max-width:var(--nx-wrap);border:1px solid var(--nx-line);border-radius:8px;padding:8px;font:inherit}.actions label{display:inline-flex;align-items:center;gap:8px;min-height:var(--nx-target)}.actions input[type=checkbox]{width:24px;height:24px}\n" as *u8)
450 o = ud_cat(out, o, "main.doc{max-width:var(--nx-wrap);margin:16px auto 40px;background:var(--nx-paper);border:1px solid var(--nx-line);border-radius:12px;padding:22px 18px}main.doc h2{font-size:20px;margin:18px 0 8px;border-bottom:1px solid var(--nx-line);padding-bottom:5px}main.doc h2:first-child{margin-top:0}main.doc p{margin:9px 0;overflow-wrap:anywhere}\n" as *u8)
451 o = ud_cat(out, o, ".tbl{overflow-x:auto;margin:14px 0}.tbl table{border-collapse:collapse;min-width:100%}.tbl td{border:1px solid var(--nx-line);padding:7px 10px;vertical-align:top;font-size:15px}\n" as *u8)
452 o = ud_cat(out, o, "footer{max-width:var(--nx-wrap);margin:0 auto 40px;padding:0 14px;color:var(--nx-muted);font-size:13px;text-align:center}\n" as *u8)
453 return o
454}
455// the page. title/kind/dlurl/base/sha are NUL-terminated; base "" = no portal chrome (pure viewer); sha "" = omitted.
456// Refuses UD_ERR_OUT_CAP rather than emitting a cut page.
457func ud_page(out: *u8, cap: i64, title: *u8, kind: i64, nbytes: i64, spec: *u8, sl: i64, dlurl: *u8, base: *u8, sha: *u8) -> i64 {
458 // a spec line can grow ~6x under escaping (every char a reference) plus tags; bound before writing, never after
459 if sl * 8 + UD_SLACK * 4 >= cap { return UD_ERR_OUT_CAP }
460 var o: i64 = 0
461 o = ud_cat(out, o, "<!DOCTYPE html>\n<html lang=en><head><meta charset=utf-8>\n<meta name=viewport content=\"width=device-width,initial-scale=1\">\n" as *u8)
462 o = ud_cat(out, o, "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; form-action 'self'; base-uri 'none'\">\n" as *u8)
463 o = ud_cat(out, o, "<title>" as *u8); o = ud_esc(out, o, title); o = ud_cat(out, o, " - Nishi Docs</title>\n<style>\n" as *u8)
464 o = ud_css(out, o)
465 o = ud_cat(out, o, "</style></head>\n<body>\n<header><div class=eyebrow>Nishi Docs</div><h1>" as *u8)
466 o = ud_esc(out, o, title)
467 o = ud_cat(out, o, "</h1></header>\n<div class=meta>" as *u8)
468 o = ud_esc(out, o, ud_kind_label(kind))
469 o = ud_cat(out, o, " · " as *u8); o = ud_catn(out, o, nbytes); o = ud_cat(out, o, " bytes" as *u8)
470 if sha[0] != (0 as u8) { o = ud_cat(out, o, " · sha256 " as *u8); o = ud_esc(out, o, sha) }
471 o = ud_cat(out, o, " · rendered by nx_unidoc, no script</div>\n<div class=actions>\n" as *u8)
472 if dlurl[0] != (0 as u8) { o = ud_cat(out, o, "<a href=\"" as *u8); o = ud_esc(out, o, dlurl); o = ud_cat(out, o, "\">Original bytes</a>\n" as *u8) }
473 if base[0] != (0 as u8) {
474 o = ud_cat(out, o, "<a href=\"" as *u8); o = ud_esc(out, o, base); o = ud_cat(out, o, "/versions\">Versions</a>\n" as *u8)
475 o = ud_cat(out, o, "<form method=post action=\"" as *u8); o = ud_esc(out, o, base); o = ud_cat(out, o, "/annotate\"><textarea name=note placeholder=\"Add a note for the firm\" aria-label=\"Note\"></textarea><button type=submit>Add note</button></form>\n" as *u8)
476 o = ud_cat(out, o, "<form method=post action=\"" as *u8); o = ud_esc(out, o, base); o = ud_cat(out, o, "/sign\"><label><input type=checkbox name=consent value=yes> I intend to sign and consent to sign electronically</label><button type=submit class=primary>Sign this version</button></form>\n" as *u8)
477 }
478 o = ud_cat(out, o, "</div>\n<main class=doc>\n" as *u8)
479 o = ud_spec_to_html(out, o, spec, sl)
480 o = ud_cat(out, o, "</main>\n<footer>The document is shown as a page: no script runs, nothing is fetched from elsewhere, and the original bytes are always one link away.</footer>\n</body></html>\n" as *u8)
481 if o + UD_SLACK >= cap { return UD_ERR_OUT_CAP }
482 out[o] = 0 as u8
483 return o
484}
485// title = the last path segment
486func ud_title_of(path: *u8, out: *u8) -> i64 {
487 let n: i64 = ud_slen(path)
488 var s: i64 = 0
489 var i: i64 = 0
490 while i < n { if path[i] == (UD_SLASH as u8) { s = i + 1 } i = i + 1 }
491 var o: i64 = 0
492 i = s
493 while i < n { out[o] = path[i]; o = o + 1; i = i + 1 }
494 out[o] = 0 as u8
495 return o
496}
497// THE WHOLE PIPELINE: path -> kind -> bytes (ceiling decided once) -> spec -> page. Returns the page length,
498// or a NAMED negative code. kind_out[0] receives the detected kind even on refusal (so a caller can say why).
499func ud_render_path(path: *u8, dlurl: *u8, base: *u8, out: *u8, cap: i64, kind_out: *i64) -> i64 {
500 let kind: i64 = ud_kind_of_path(path)
501 kind_out[0] = kind
502 if kind == UD_KIND_UNKNOWN { return UD_ERR_UNKNOWN_KIND }
503 if kind == UD_KIND_HTML { return UD_ERR_REFUSED_HTML }
504 let lenp: *i64 = sys_mmap(UD_PTR_BYTES * 2) as *i64
505 lenp[0] = 0
506 let bytes: *u8 = sys_read_file(path, lenp)
507 if (bytes as i64) == 0 { return UD_ERR_READ }
508 let n: i64 = lenp[0]
509 if n < 0 { return UD_ERR_READ }
510 let sz: i64 = ud_check_size(n)
511 if sz != 0 { return sz }
512 let spec: *u8 = sys_mmap(UD_SPEC_CAP + 16)
513 let sl: i64 = ud_spec_of(kind, path, bytes, n, spec, UD_SPEC_CAP)
514 if sl < 0 { return sl }
515 let title: *u8 = sys_mmap(UD_PATH_CAP)
516 ud_title_of(path, title)
517 return ud_page(out, cap, title, kind, n, spec, sl, dlurl, base, "" as *u8)
518}