code wiki / _hdl_build / nx_docx.nx
nx_docx.nx source
↩ module page · 569 lines · 36095 B
1// nx_docx.nx -- SOVEREIGN MS-Word (.docx / OOXML) interop for the Nishi Office suite. A .docx is an OPC package
2// = a ZIP of XML parts (framed by the shared nx_opc). This organ READS and WRITES real .docx with ZERO 3rd-party
3// code:
4// write : paragraphs -> a minimal valid OPC package ([Content_Types].xml + _rels/.rels + word/document.xml)
5// -> a file Microsoft Word / unzip can open.
6// read : a .docx -> the text of word/document.xml (STORED and DEFLATE entries both handled via nx_opc),
7// extracting each <w:t> run and joining runs with newlines, unescaping XML entities.
8// This is the "lawyers make native edits / interop with Microsoft Office" keystone; nx_mailmerge composes on top.
9// Sovereignty = our own ZIP+OOXML; rigor (non-novel-format doctrine) = the harness ALSO proves a 3rd-party
10// unzip/zipfile opens what we write and that we read what it writes.
11// nx_docx write <path.docx> <paragraph1> [paragraph2 ...]
12// nx_docx read <path.docx>
13// license_tier: ORIGINAL
14import "nx_syscalls.nx"
15import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
16import "nx_opc.nx"
17const K_MAGIC_1048592: i64 = 1048592
18const K_MAGIC_1048576: i64 = 1048576
19const K_MAGIC_262160: i64 = 262160
20const K_MAGIC_2097168: i64 = 2097168
21const K_MAGIC_2097152: i64 = 2097152
22
23func dx_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
24// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
25// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
26// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
27// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
28func dx_num(v: i64) -> i64 { nxi_out(v); return 0 }
29func dx_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
30func dx_streq(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8){ if a[i]!=b[i]{return 0} i=i+1 } if b[i]!=(0 as u8){return 0} return 1 }
31func dx_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ dst[off+i]=s[i]; i=i+1 } return off+i }
32
33// pat (NUL-terminated) matches buf at pos, within [pos,n)?
34func dx_match(buf: *u8, pos: i64, n: i64, pat: *u8) -> i64 {
35 var k: i64=0
36 while pat[k]!=(0 as u8) { if pos+k>=n { return 0 } if buf[pos+k]!=pat[k] { return 0 } k=k+1 }
37 return 1
38}
39// copy s into dst escaping XML & < > ; return new offset
40func dx_xml_escape(dst: *u8, off: i64, s: *u8) -> i64 {
41 var o: i64=off; var i: i64=0
42 while s[i]!=(0 as u8) {
43 let c: i64=s[i] as i64
44 if c==38 { o=dx_cat(dst,o,"&" as *u8) } else { if c==60 { o=dx_cat(dst,o,"<" as *u8) } else { if c==62 { o=dx_cat(dst,o,">" as *u8) } else { dst[o]=s[i]; o=o+1 } } }
45 i=i+1
46 }
47 return o
48}
49
50// build word/document.xml from nparas paragraphs into out; return length
51func dx_build_document(paras: *i64, nparas: i64, out: *u8) -> i64 {
52 var o: i64=0
53 o=dx_cat(out, o, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:body>" as *u8)
54 var i: i64=0
55 while i<nparas {
56 o=dx_cat(out, o, "<w:p><w:r><w:t xml:space=\"preserve\">" as *u8)
57 o=dx_xml_escape(out, o, paras[i] as *u8)
58 o=dx_cat(out, o, "</w:t></w:r></w:p>" as *u8)
59 i=i+1
60 }
61 o=dx_cat(out, o, "</w:body></w:document>" as *u8)
62 return o
63}
64
65// write a .docx whose word/document.xml is the given raw bytes (+ fixed Content_Types/.rels); return total bytes
66func docx_write_document_xml(path: *u8, doc: *u8, doclen: i64) -> i64 {
67 let ct: *u8="<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"><Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/><Default Extension=\"xml\" ContentType=\"application/xml\"/><Override PartName=\"/word/document.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/></Types>" as *u8
68 let rels: *u8="<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"word/document.xml\"/></Relationships>" as *u8
69 let names: *i64=sys_mmap(64) as *i64
70 let datap: *i64=sys_mmap(64) as *i64
71 let lens: *i64=sys_mmap(64) as *i64
72 names[0]="[Content_Types].xml" as *u8 as i64; datap[0]=ct as i64; lens[0]=dx_strlen(ct)
73 names[1]="_rels/.rels" as *u8 as i64; datap[1]=rels as i64; lens[1]=dx_strlen(rels)
74 names[2]="word/document.xml" as *u8 as i64; datap[2]=doc as i64; lens[2]=doclen
75 return opc_write(path, names, datap, lens, 3)
76}
77
78// write a .docx from paragraph strings (builds document.xml, then frames the OPC ZIP)
79func docx_write(path: *u8, paras: *i64, nparas: i64) -> i64 {
80 let doc: *u8=sys_mmap(K_MAGIC_1048592)
81 let doclen: i64=dx_build_document(paras, nparas, doc)
82 return docx_write_document_xml(path, doc, doclen)
83}
84
85
86// ================= OOXML FEATURE EMITTERS =========================================================
87// The office compare-matrix claimed five DOCX capabilities whose emitters did not exist anywhere in the
88// tree (dx_emit_drawing / sectPr / dx_emit_track / dx_cm_add / dx_fn_add): the axes read PRESENT while
89// nothing implemented them. Implemented here for real rather than withdrawing the claims -- and note the
90// grounding ruler only counts SYMBOL PRESENCE, so a stub would have "grounded" them while proving
91// nothing. Each emitter below produces genuine WordprocessingML and is exercised by the `feat` verb,
92// which writes a package a third-party unzip can open (the organ's own non-novel-format doctrine).
93const DX_EMU_PER_PX: i64 = 9525 // 914400 EMU/inch / 96 px/inch (ECMA-376 drawing units)
94
95// append a decimal integer into a buffer (dx_num prints to stdout; emitters need it IN the XML)
96func dx_catn(dst: *u8, off: i64, v: i64) -> i64 {
97 if v == 0 { dst[off] = 48 as u8; return off + 1 }
98 let t: *u8 = sys_mmap(32)
99 var m: i64 = v
100 var k: i64 = 0
101 while m > 0 { t[k] = (48 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 }
102 var o: i64 = off
103 while k > 0 { k = k - 1; dst[o] = t[k]; o = o + 1 }
104 return o
105}
106
107// REAL dimensions: PNG IHDR carries width/height as big-endian u32 at byte 16 and 20 (ECMA/PNG spec).
108// Returns 1 and fills out_wh[0]=w out_wh[1]=h, or 0 if this is not a PNG.
109func dx_png_dim(png: *u8, n: i64, out_wh: *i64) -> i64 {
110 if n < 24 { return 0 }
111 if png[0] != (137 as u8) { return 0 }
112 if png[1] != (80 as u8) { return 0 }
113 if png[2] != (78 as u8) { return 0 }
114 if png[3] != (71 as u8) { return 0 }
115 var w: i64 = 0
116 var i: i64 = 0
117 while i < 4 { w = w * 256 + (png[16 + i] as i64); i = i + 1 }
118 var h: i64 = 0
119 i = 0
120 while i < 4 { h = h * 256 + (png[20 + i] as i64); i = i + 1 }
121 out_wh[0] = w
122 out_wh[1] = h
123 return 1
124}
125
126// Inline image sized from the PNG's ACTUAL pixels -> EMU. rid must match a rel to word/media/imageN.png.
127func dx_emit_drawing(out: *u8, off: i64, rid: *u8, wpx: i64, hpx: i64, pid: i64, name: *u8) -> i64 {
128 var o: i64 = off
129 o = dx_cat(out, o, "<w:p><w:r><w:drawing><wp:inline distT=\"0\" distB=\"0\" distL=\"0\" distR=\"0\"><wp:extent cx=\"" as *u8)
130 o = dx_catn(out, o, wpx * DX_EMU_PER_PX)
131 o = dx_cat(out, o, "\" cy=\"" as *u8)
132 o = dx_catn(out, o, hpx * DX_EMU_PER_PX)
133 o = dx_cat(out, o, "\"/><wp:docPr id=\"" as *u8)
134 o = dx_catn(out, o, pid)
135 o = dx_cat(out, o, "\" name=\"" as *u8)
136 o = dx_xml_escape(out, o, name)
137 o = dx_cat(out, o, "\"/><a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"><a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\"><pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\"><pic:nvPicPr><pic:cNvPr id=\"" as *u8)
138 o = dx_catn(out, o, pid)
139 o = dx_cat(out, o, "\" name=\"" as *u8)
140 o = dx_xml_escape(out, o, name)
141 o = dx_cat(out, o, "\"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed=\"" as *u8)
142 o = dx_cat(out, o, rid)
143 o = dx_cat(out, o, "\"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"" as *u8)
144 o = dx_catn(out, o, wpx * DX_EMU_PER_PX)
145 o = dx_cat(out, o, "\" cy=\"" as *u8)
146 o = dx_catn(out, o, hpx * DX_EMU_PER_PX)
147 o = dx_cat(out, o, "\"/></a:xfrm><a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>" as *u8)
148 return o
149}
150
151// Page layout: a real w:sectPr with pgSz + pgMar, referencing header1/footer1 parts.
152// Letter portrait = 12240 x 15840 twips; 1-inch margins = 1440 twips.
153func dx_emit_sectpr(out: *u8, off: i64, hdr_rid: *u8, ftr_rid: *u8) -> i64 {
154 var o: i64 = off
155 o = dx_cat(out, o, "<w:sectPr><w:headerReference w:type=\"default\" r:id=\"" as *u8)
156 o = dx_cat(out, o, hdr_rid)
157 o = dx_cat(out, o, "\"/><w:footerReference w:type=\"default\" r:id=\"" as *u8)
158 o = dx_cat(out, o, ftr_rid)
159 o = dx_cat(out, o, "\"/><w:pgSz w:w=\"12240\" w:h=\"15840\"/><w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" w:left=\"1440\" w:header=\"720\" w:footer=\"720\" w:gutter=\"0\"/></w:sectPr>" as *u8)
160 return o
161}
162
163// Tracked revisions: w:ins wraps inserted runs; w:del wraps deleted runs and the text becomes w:delText
164// (a plain w:t inside w:del is what makes Word silently drop the deletion on accept/reject).
165func dx_emit_track(out: *u8, off: i64, rev: i64, author: *u8, date: *u8, ins_txt: *u8, del_txt: *u8) -> i64 {
166 var o: i64 = off
167 o = dx_cat(out, o, "<w:p>" as *u8)
168 o = dx_cat(out, o, "<w:ins w:id=\"" as *u8)
169 o = dx_catn(out, o, rev)
170 o = dx_cat(out, o, "\" w:author=\"" as *u8)
171 o = dx_xml_escape(out, o, author)
172 o = dx_cat(out, o, "\" w:date=\"" as *u8)
173 o = dx_cat(out, o, date)
174 o = dx_cat(out, o, "\"><w:r><w:t xml:space=\"preserve\">" as *u8)
175 o = dx_xml_escape(out, o, ins_txt)
176 o = dx_cat(out, o, "</w:t></w:r></w:ins>" as *u8)
177 o = dx_cat(out, o, "<w:del w:id=\"" as *u8)
178 o = dx_catn(out, o, rev + 1)
179 o = dx_cat(out, o, "\" w:author=\"" as *u8)
180 o = dx_xml_escape(out, o, author)
181 o = dx_cat(out, o, "\" w:date=\"" as *u8)
182 o = dx_cat(out, o, date)
183 o = dx_cat(out, o, "\"><w:r><w:delText xml:space=\"preserve\">" as *u8)
184 o = dx_xml_escape(out, o, del_txt)
185 o = dx_cat(out, o, "</w:delText></w:r></w:del>" as *u8)
186 o = dx_cat(out, o, "</w:p>" as *u8)
187 return o
188}
189
190// Comment ANCHOR in the body: the range must be opened and closed around the commented run, and the
191// reference run carries the same id -- an orphan commentReference is what makes Word drop the comment.
192func dx_cm_add(out: *u8, off: i64, cid: i64, txt: *u8) -> i64 {
193 var o: i64 = off
194 o = dx_cat(out, o, "<w:p><w:commentRangeStart w:id=\"" as *u8)
195 o = dx_catn(out, o, cid)
196 o = dx_cat(out, o, "\"/><w:r><w:t xml:space=\"preserve\">" as *u8)
197 o = dx_xml_escape(out, o, txt)
198 o = dx_cat(out, o, "</w:t></w:r><w:commentRangeEnd w:id=\"" as *u8)
199 o = dx_catn(out, o, cid)
200 o = dx_cat(out, o, "\"/><w:r><w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr><w:commentReference w:id=\"" as *u8)
201 o = dx_catn(out, o, cid)
202 o = dx_cat(out, o, "\"/></w:r></w:p>" as *u8)
203 return o
204}
205
206// word/comments.xml for ONE comment (the part the anchor above points at).
207func dx_cm_part(out: *u8, off: i64, cid: i64, author: *u8, initials: *u8, date: *u8, body: *u8) -> i64 {
208 var o: i64 = off
209 o = dx_cat(out, o, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:comments xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:comment w:id=\"" as *u8)
210 o = dx_catn(out, o, cid)
211 o = dx_cat(out, o, "\" w:author=\"" as *u8)
212 o = dx_xml_escape(out, o, author)
213 o = dx_cat(out, o, "\" w:initials=\"" as *u8)
214 o = dx_xml_escape(out, o, initials)
215 o = dx_cat(out, o, "\" w:date=\"" as *u8)
216 o = dx_cat(out, o, date)
217 o = dx_cat(out, o, "\"><w:p><w:r><w:t xml:space=\"preserve\">" as *u8)
218 o = dx_xml_escape(out, o, body)
219 o = dx_cat(out, o, "</w:t></w:r></w:p></w:comment></w:comments>" as *u8)
220 return o
221}
222
223// Footnote REFERENCE in the body. Authored notes start at id 2 because 0 and 1 are reserved for the
224// separator and continuationSeparator -- a note authored at id 0/1 collides with them and Word drops it.
225func dx_fn_add(out: *u8, off: i64, fid: i64, txt: *u8) -> i64 {
226 var o: i64 = off
227 o = dx_cat(out, o, "<w:p><w:r><w:t xml:space=\"preserve\">" as *u8)
228 o = dx_xml_escape(out, o, txt)
229 o = dx_cat(out, o, "</w:t></w:r><w:r><w:rPr><w:rStyle w:val=\"FootnoteReference\"/></w:rPr><w:footnoteReference w:id=\"" as *u8)
230 o = dx_catn(out, o, fid)
231 o = dx_cat(out, o, "\"/></w:r></w:p>" as *u8)
232 return o
233}
234
235// word/footnotes.xml with the two REQUIRED special notes pinned at ids 0 and 1, then the authored note.
236func dx_fn_part(out: *u8, off: i64, fid: i64, body: *u8) -> i64 {
237 var o: i64 = off
238 o = dx_cat(out, o, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:footnotes xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:footnote w:type=\"separator\" w:id=\"0\"><w:p><w:r><w:separator/></w:r></w:p></w:footnote><w:footnote w:type=\"continuationSeparator\" w:id=\"1\"><w:p><w:r><w:continuationSeparator/></w:r></w:p></w:footnote><w:footnote w:id=\"" as *u8)
239 o = dx_catn(out, o, fid)
240 o = dx_cat(out, o, "\"><w:p><w:r><w:rPr><w:rStyle w:val=\"FootnoteReference\"/></w:rPr><w:footnoteRef/></w:r><w:r><w:t xml:space=\"preserve\"> " as *u8)
241 o = dx_xml_escape(out, o, body)
242 o = dx_cat(out, o, "</w:t></w:r></w:p></w:footnote></w:footnotes>" as *u8)
243 return o
244}
245
246// ---- RICH .docx: formatted runs (bold/italic + heading size) + TABLES ----
247// spec = one directive per line: "H <text>"=heading(bold,larger) "B <text>"=bold "I <text>"=italic
248// "P <text>"=plain "T c1|c2|c3"=a table row (consecutive T lines = ONE bordered table). Other lines = plain.
249// escape spec[s,e) (XML & < >) into dst; return new offset
250func dx_esc_span(dst: *u8, off: i64, spec: *u8, s: i64, e: i64) -> i64 {
251 var o: i64=off; var i: i64=s
252 while i<e {
253 let c: i64=spec[i] as i64
254 if c==38 { o=dx_cat(dst,o,"&" as *u8) } else { if c==60 { o=dx_cat(dst,o,"<" as *u8) } else { if c==62 { o=dx_cat(dst,o,">" as *u8) } else { dst[o]=spec[i]; o=o+1 } } }
255 i=i+1
256 }
257 return o
258}
259// emit one <w:p> paragraph; rpr (NUL-term, may be "") = the run-property block content (e.g. "<w:b/>")
260func dx_emit_para(out: *u8, o: i64, rpr: *u8, spec: *u8, s: i64, e: i64) -> i64 {
261 var oo: i64=o
262 oo=dx_cat(out,oo,"<w:p><w:r>" as *u8)
263 if rpr[0]!=(0 as u8) { oo=dx_cat(out,oo,"<w:rPr>" as *u8); oo=dx_cat(out,oo,rpr); oo=dx_cat(out,oo,"</w:rPr>" as *u8) }
264 oo=dx_cat(out,oo,"<w:t xml:space=\"preserve\">" as *u8)
265 oo=dx_esc_span(out,oo,spec,s,e)
266 oo=dx_cat(out,oo,"</w:t></w:r></w:p>" as *u8)
267 return oo
268}
269func dx_build_body_rich(spec: *u8, slen: i64, out: *u8) -> i64 {
270 var o: i64=0
271 // NOTE: never pass the "" string literal as the empty run-properties sentinel -- the nx_cc constant pool
272 // ALIASES the empty literal onto another literal (here "</w:tbl><w:p/>"), which would inject stray table
273 // closes into every plain paragraph. Use a real zeroed byte instead.
274 let EMPTY: *u8=sys_mmap(8); EMPTY[0]=0 as u8
275 o=dx_cat(out,o,"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:body>" as *u8)
276 let TBLOPEN: *u8 = "<w:tbl><w:tblPr><w:tblW w:w=\"0\" w:type=\"auto\"/><w:tblBorders><w:top w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"auto\"/><w:left w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"auto\"/><w:bottom w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"auto\"/><w:right w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"auto\"/><w:insideH w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"auto\"/><w:insideV w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"auto\"/></w:tblBorders></w:tblPr>" as *u8
277 var intable: i64=0
278 var i: i64=0
279 while i<slen {
280 var e: i64=i
281 while e<slen { if spec[e]==(10 as u8) { break } e=e+1 }
282 var end: i64=e
283 if end>i { if spec[end-1]==(13 as u8) { end=end-1 } }
284 var c0: i64=0-1
285 if end>i { c0=spec[i] as i64 }
286 var typed: i64=0
287 var content: i64=i
288 if end>=i+2 { if spec[i+1]==(32 as u8) {
289 if c0==84 { typed=84 }
290 if c0==72 { typed=72 }
291 if c0==66 { typed=66 }
292 if c0==73 { typed=73 }
293 if c0==80 { typed=80 }
294 if typed!=0 { content=i+2 }
295 } }
296 if typed==84 {
297 if intable==0 { o=dx_cat(out,o,TBLOPEN); intable=1 }
298 o=dx_cat(out,o,"<w:tr>" as *u8)
299 var cs: i64=content
300 var p: i64=content
301 while p<=end {
302 var atbar: i64=0
303 if p==end { atbar=1 } else { if spec[p]==(124 as u8) { atbar=1 } }
304 if atbar==1 {
305 o=dx_cat(out,o,"<w:tc><w:tcPr><w:tcW w:w=\"0\" w:type=\"auto\"/></w:tcPr><w:p><w:r><w:t xml:space=\"preserve\">" as *u8)
306 o=dx_esc_span(out,o,spec,cs,p)
307 o=dx_cat(out,o,"</w:t></w:r></w:p></w:tc>" as *u8)
308 cs=p+1
309 }
310 p=p+1
311 }
312 o=dx_cat(out,o,"</w:tr>" as *u8)
313 } else {
314 if intable==1 { o=dx_cat(out,o,"</w:tbl>" as *u8); intable=0 }
315 if end>i {
316 if typed==72 { o=dx_emit_para(out,o,"<w:b/><w:sz w:val=\"32\"/><w:szCs w:val=\"32\"/>" as *u8, spec, content, end) } else { if typed==66 { o=dx_emit_para(out,o,"<w:b/>" as *u8, spec, content, end) } else { if typed==73 { o=dx_emit_para(out,o,"<w:i/>" as *u8, spec, content, end) } else { o=dx_emit_para(out,o,EMPTY, spec, content, end) } } }
317 }
318 }
319 i=e+1
320 }
321 if intable==1 { o=dx_cat(out,o,"</w:tbl><w:p/>" as *u8); intable=0 }
322 o=dx_cat(out,o,"</w:body></w:document>" as *u8)
323 return o
324}
325func docx_write_rich(path: *u8, spec: *u8, slen: i64) -> i64 {
326 let doc: *u8=sys_mmap(K_MAGIC_1048592)
327 let doclen: i64=dx_build_body_rich(spec, slen, doc)
328 return docx_write_document_xml(path, doc, doclen)
329}
330func dx_readfile(path: *u8, buf: *u8, cap: i64) -> i64 {
331 let fd: i64=sys_openat_rd(path); if fd<0 { return 0-1 }
332 var tot: i64=0
333 while tot<cap { let r: i64=sys_read(fd,(buf as i64+tot) as *u8,cap-tot); if r<=0 { break } tot=tot+r }
334 sys_close(fd); return tot
335}
336
337// extract a named OPC part's RAW bytes (delegates to the shared nx_opc reader)
338func docx_read_part(path: *u8, want: *u8, out: *u8, cap: i64) -> i64 {
339 return opc_read_part(path, want, out, cap)
340}
341
342// read word/document.xml text from a .docx into out (NUL-terminated); return length (or <0 on error)
343func docx_read_text(path: *u8, out: *u8, cap: i64) -> i64 {
344 let docbuf: *u8=sys_mmap(K_MAGIC_1048592)
345 let doclen: i64=docx_read_part(path, "word/document.xml" as *u8, docbuf, K_MAGIC_1048576)
346 if doclen<0 { return doclen }
347 // extract each <w:t ...>TEXT</w:t> run; join runs with newline; unescape entities
348 var o: i64=0; var i: i64=0; var first: i64=1
349 while i<doclen {
350 var advanced: i64=0
351 if docbuf[i]==(60 as u8) { // '<'
352 if dx_match(docbuf, i, doclen, "<w:t" as *u8)==1 {
353 let c4: i64=docbuf[i+4] as i64
354 if c4==62 { advanced=1 } else { if c4==32 { advanced=1 } } // only "<w:t>" or "<w:t " (not <w:tbl/<w:tc)
355 if advanced==1 {
356 var j: i64=i+4
357 while j<doclen { if docbuf[j]==(62 as u8) { break } j=j+1 }
358 j=j+1 // past '>'
359 if first==0 { out[o]=10 as u8; o=o+1 }
360 first=0
361 while j<doclen {
362 if dx_match(docbuf, j, doclen, "</w:t>" as *u8)==1 { break }
363 if docbuf[j]==(38 as u8) { // '&' entity
364 if dx_match(docbuf,j,doclen,"&" as *u8)==1 { out[o]=38 as u8; o=o+1; j=j+5 } else {
365 if dx_match(docbuf,j,doclen,"<" as *u8)==1 { out[o]=60 as u8; o=o+1; j=j+4 } else {
366 if dx_match(docbuf,j,doclen,">" as *u8)==1 { out[o]=62 as u8; o=o+1; j=j+4 } else {
367 if dx_match(docbuf,j,doclen,""" as *u8)==1 { out[o]=34 as u8; o=o+1; j=j+6 } else {
368 if dx_match(docbuf,j,doclen,"'" as *u8)==1 { out[o]=39 as u8; o=o+1; j=j+6 } else {
369 out[o]=docbuf[j]; o=o+1; j=j+1 } } } } }
370 } else { out[o]=docbuf[j]; o=o+1; j=j+1 }
371 }
372 i=j+6 // past "</w:t>"
373 }
374 }
375 }
376 if advanced==0 { i=i+1 }
377 }
378 out[o]=0 as u8
379 return o
380}
381
382// wrap the current run's inline text with <b>/<i> per the run flags (into the paragraph buffer)
383func dx_wrapopen(pb: *u8, o: i64, rb: i64, ri: i64) -> i64 {
384 var oo: i64=o
385 if rb==1 { oo=dx_cat(pb,oo,"<b>" as *u8) }
386 if ri==1 { oo=dx_cat(pb,oo,"<i>" as *u8) }
387 return oo
388}
389func dx_wrapclose(pb: *u8, o: i64, rb: i64, ri: i64) -> i64 {
390 var oo: i64=o
391 if ri==1 { oo=dx_cat(pb,oo,"</i>" as *u8) }
392 if rb==1 { oo=dx_cat(pb,oo,"</b>" as *u8) }
393 return oo
394}
395
396// Render a .docx (word/document.xml) into a clean, sovereign, 0-JS HTML PREVIEW page the Nishi browser shows
397// INLINE (browsers can't render raw .docx -- they download it; so we render OOXML -> HTML, the way Docs/Office
398// preview do). Handles paragraphs, headings (b + sz=32), bold/italic runs, and tables (tbl/tr/tc). XML text
399// entities (& < >) are already valid HTML, so run text is copied verbatim. dlurl (optional) adds a
400// "Download .docx" interop link. Returns HTML length, or <0 if the part is missing.
401func dx_to_html(path: *u8, out: *u8, cap: i64, dlurl: *u8) -> i64 {
402 let doc: *u8=sys_mmap(K_MAGIC_1048592)
403 let dl: i64=docx_read_part(path, "word/document.xml" as *u8, doc, K_MAGIC_1048576)
404 if dl<0 { return dl }
405 var o: i64=0
406 o=dx_cat(out,o,"<!DOCTYPE html>\n<html lang=en><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Document Preview - Nishi Office</title><style>\nbody{font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;background:#eef1f5;color:#1a1d21}\nheader{background:#0f172a;color:#fff;padding:16px 24px}\nheader h1{margin:0;font-size:17px;font-weight:600}\n.doc{max-width:800px;margin:22px auto;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:34px 46px;box-shadow:0 1px 4px rgba(15,23,42,.08)}\n.doc h2{font-size:20px;color:#0f172a;margin:22px 0 8px;border-bottom:1px solid #eef2f7;padding-bottom:5px}\n.doc h2:first-child{margin-top:0}\n.doc p{margin:9px 0}\n.doc table{border-collapse:collapse;margin:16px 0;width:100%}\n.doc td{border:1px solid #cbd5e1;padding:7px 12px;font-size:15px;vertical-align:top}\n.bar{max-width:800px;margin:0 auto 6px;text-align:right}\n.dl{display:inline-block;padding:9px 18px;background:#2563eb;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500}\nfooter{max-width:800px;margin:14px auto 46px;color:#64748b;font-size:13px;text-align:center;line-height:1.5}\n</style></head>\n<body>\n<header><h1>📄 Document Preview — Nishi Office</h1></header>\n<div class=doc>\n" as *u8)
407 let pb: *u8=sys_mmap(K_MAGIC_262160)
408 var po: i64=0; var phead: i64=0; var incell: i64=0
409 var rb: i64=0; var ri: i64=0; var rhead: i64=0
410 var i: i64=0
411 while i<dl {
412 var m: i64=0
413 if m==0 { if dx_match(doc,i,dl,"<w:tbl>" as *u8)==1 { o=dx_cat(out,o,"<table>" as *u8); i=i+7; m=1 } }
414 if m==0 { if dx_match(doc,i,dl,"</w:tbl>" as *u8)==1 { o=dx_cat(out,o,"</table>" as *u8); i=i+8; m=1 } }
415 if m==0 { if dx_match(doc,i,dl,"<w:tr>" as *u8)==1 { o=dx_cat(out,o,"<tr>" as *u8); i=i+6; m=1 } }
416 if m==0 { if dx_match(doc,i,dl,"</w:tr>" as *u8)==1 { o=dx_cat(out,o,"</tr>" as *u8); i=i+7; m=1 } }
417 if m==0 { if dx_match(doc,i,dl,"<w:tc>" as *u8)==1 { o=dx_cat(out,o,"<td>" as *u8); incell=1; po=0; phead=0; i=i+6; m=1 } }
418 if m==0 { if dx_match(doc,i,dl,"</w:tc>" as *u8)==1 { pb[po]=0 as u8; o=dx_cat(out,o,pb); o=dx_cat(out,o,"</td>" as *u8); incell=0; po=0; i=i+7; m=1 } }
419 if m==0 { if dx_match(doc,i,dl,"<w:p>" as *u8)==1 { po=0; phead=0; i=i+5; m=1 } }
420 if m==0 { if dx_match(doc,i,dl,"</w:p>" as *u8)==1 {
421 if incell==0 {
422 pb[po]=0 as u8
423 if po>0 { if phead==1 { o=dx_cat(out,o,"<h2>" as *u8); o=dx_cat(out,o,pb); o=dx_cat(out,o,"</h2>" as *u8) } else { o=dx_cat(out,o,"<p>" as *u8); o=dx_cat(out,o,pb); o=dx_cat(out,o,"</p>" as *u8) } }
424 po=0
425 }
426 i=i+6; m=1 } }
427 if m==0 { if dx_match(doc,i,dl,"<w:r>" as *u8)==1 { rb=0; ri=0; rhead=0; i=i+5; m=1 } }
428 if m==0 { if dx_match(doc,i,dl,"<w:b/>" as *u8)==1 { rb=1; i=i+6; m=1 } }
429 if m==0 { if dx_match(doc,i,dl,"<w:i/>" as *u8)==1 { ri=1; i=i+6; m=1 } }
430 if m==0 { if dx_match(doc,i,dl,"<w:sz w:val=\"32\"" as *u8)==1 { rhead=1; phead=1; i=i+16; m=1 } }
431 if m==0 { if dx_match(doc,i,dl,"<w:t" as *u8)==1 {
432 let c4: i64=doc[i+4] as i64
433 var istext: i64=0
434 if c4==62 { istext=1 } else { if c4==32 { istext=1 } }
435 if istext==1 {
436 var j: i64=i+4
437 while j<dl { if doc[j]==(62 as u8) { break } j=j+1 }
438 j=j+1
439 if rhead==0 { po=dx_wrapopen(pb,po,rb,ri) }
440 while j<dl {
441 if dx_match(doc,j,dl,"</w:t>" as *u8)==1 { break }
442 pb[po]=doc[j]; po=po+1; j=j+1
443 }
444 if rhead==0 { po=dx_wrapclose(pb,po,rb,ri) }
445 i=j+6; m=1
446 }
447 } }
448 if m==0 { i=i+1 }
449 }
450 o=dx_cat(out,o,"</div>\n" as *u8)
451 if (dlurl as i64)!=0 {
452 o=dx_cat(out,o,"<div class=bar><a class=dl href=\"" as *u8); o=dx_cat(out,o,dlurl); o=dx_cat(out,o,"\">⬇ Download .docx (opens in Word / Pages / Google Docs)</a></div>\n" as *u8)
453 }
454 o=dx_cat(out,o,"<footer>Rendered natively from OOXML by <b>nx_docx</b> · viewable inline in the Nishi browser · your documents stay on your hardware</footer>\n</body></html>\n" as *u8)
455 return o
456}
457
458
459// Package a .docx that USES every emitter above: header/footer parts + comments + footnotes + an inline
460// PNG, with the [Content_Types].xml overrides and word/_rels/document.xml.rels that make Word accept
461// them. Emitters that are never packaged are decoration -- this is what makes the axes real rather than
462// symbol-present, and `nx_docx feat <out.docx> <in.png>` exercises the whole path.
463func docx_write_featured(path: *u8, pngdata: *u8, pnglen: i64) -> i64 {
464 let doc: *u8 = sys_mmap(K_MAGIC_1048592)
465 var o: i64 = 0
466 o = dx_cat(doc, o, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"><w:body>" as *u8)
467 o = dx_emit_track(doc, o, 100, "Nishi" as *u8, "2026-08-01T00:00:00Z" as *u8, "inserted text" as *u8, "deleted text" as *u8)
468 o = dx_cm_add(doc, o, 1, "commented sentence" as *u8)
469 o = dx_fn_add(doc, o, 2, "sentence with a footnote" as *u8)
470 let wh: *i64 = sys_mmap(16) as *i64
471 wh[0] = 0
472 wh[1] = 0
473 if pnglen > 0 { if dx_png_dim(pngdata, pnglen, wh) == 1 { o = dx_emit_drawing(doc, o, "rId5" as *u8, wh[0], wh[1], 1, "image1.png" as *u8) } }
474 o = dx_emit_sectpr(doc, o, "rId2" as *u8, "rId3" as *u8)
475 o = dx_cat(doc, o, "</w:body></w:document>" as *u8)
476
477 let cm: *u8 = sys_mmap(K_MAGIC_1048592)
478 let cmn: i64 = dx_cm_part(cm, 0, 1, "Nishi" as *u8, "NX" as *u8, "2026-08-01T00:00:00Z" as *u8, "a real anchored comment" as *u8)
479 let fn: *u8 = sys_mmap(K_MAGIC_1048592)
480 let fnn: i64 = dx_fn_part(fn, 0, 2, "a real footnote body" as *u8)
481
482 let hdr: *u8 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:hdr xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:p><w:r><w:t>Nishi header</w:t></w:r></w:p></w:hdr>" as *u8
483 let ftr: *u8 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<w:ftr xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"><w:p><w:r><w:t>Nishi footer</w:t></w:r></w:p></w:ftr>" as *u8
484 let ct: *u8 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"><Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/><Default Extension=\"xml\" ContentType=\"application/xml\"/><Default Extension=\"png\" ContentType=\"image/png\"/><Override PartName=\"/word/document.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/><Override PartName=\"/word/header1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml\"/><Override PartName=\"/word/footer1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml\"/><Override PartName=\"/word/comments.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml\"/><Override PartName=\"/word/footnotes.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml\"/></Types>" as *u8
485 let rels: *u8 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"word/document.xml\"/></Relationships>" as *u8
486 let drels: *u8 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header\" Target=\"header1.xml\"/><Relationship Id=\"rId3\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer\" Target=\"footer1.xml\"/><Relationship Id=\"rId4\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments\" Target=\"comments.xml\"/><Relationship Id=\"rId6\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes\" Target=\"footnotes.xml\"/><Relationship Id=\"rId5\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"media/image1.png\"/></Relationships>" as *u8
487
488 let names: *i64 = sys_mmap(128) as *i64
489 let datap: *i64 = sys_mmap(128) as *i64
490 let lens: *i64 = sys_mmap(128) as *i64
491 names[0]="[Content_Types].xml" as *u8 as i64; datap[0]=ct as i64; lens[0]=dx_strlen(ct)
492 names[1]="_rels/.rels" as *u8 as i64; datap[1]=rels as i64; lens[1]=dx_strlen(rels)
493 names[2]="word/document.xml" as *u8 as i64; datap[2]=doc as i64; lens[2]=o
494 names[3]="word/_rels/document.xml.rels" as *u8 as i64; datap[3]=drels as i64; lens[3]=dx_strlen(drels)
495 names[4]="word/header1.xml" as *u8 as i64; datap[4]=hdr as i64; lens[4]=dx_strlen(hdr)
496 names[5]="word/footer1.xml" as *u8 as i64; datap[5]=ftr as i64; lens[5]=dx_strlen(ftr)
497 names[6]="word/comments.xml" as *u8 as i64; datap[6]=cm as i64; lens[6]=cmn
498 names[7]="word/footnotes.xml" as *u8 as i64; datap[7]=fn as i64; lens[7]=fnn
499 var nparts: i64 = 8
500 if pnglen > 0 {
501 names[8]="word/media/image1.png" as *u8 as i64; datap[8]=pngdata as i64; lens[8]=pnglen
502 nparts = 9
503 }
504 return opc_write(path, names, datap, lens, nparts)
505}
506
507func main(argc: i64, argv: *i64) -> i64 {
508 if argc >= 3 {
509 let v0: *u8 = argv[1] as *u8
510 if dx_streq(v0, "feat\x00" as *u8) == 1 {
511 let png: *u8 = sys_mmap(K_MAGIC_1048592)
512 var pn: i64 = 0
513 if argc >= 4 { pn = dx_readfile(argv[3] as *u8, png, K_MAGIC_1048592) }
514 if pn < 0 { pn = 0 }
515 let tot: i64 = docx_write_featured(argv[2] as *u8, png, pn)
516 dx_puts("DOCX-FEAT bytes=" as *u8); dx_num(tot)
517 dx_puts(" png_bytes=" as *u8); dx_num(pn); dx_puts("\n" as *u8)
518 return 0
519 }
520 }
521 if argc<3 { dx_puts("usage: nx_docx write <path.docx> <paragraph>... | writerich <path.docx> <specfile> | read <path.docx> | html <path.docx> <out.html> [download_url]\n" as *u8); sys_exit(2); return 2 }
522 let cmd: *u8=argv[1] as *u8
523 let path: *u8=argv[2] as *u8
524 if dx_streq(cmd, "write" as *u8)==1 {
525 let paras: *i64=sys_mmap(8*256) as *i64
526 var np: i64=0; var i: i64=3
527 while i<argc { paras[np]=argv[i]; np=np+1; i=i+1 }
528 if np==0 { paras[0]="(empty document)" as *u8 as i64; np=1 }
529 let sz: i64=docx_write(path, paras, np)
530 if sz<0 { dx_puts("DOCX-WRITE-FAIL\n" as *u8); sys_exit(1); return 1 }
531 dx_puts("DOCX-WRITE-OK path=" as *u8); dx_puts(path); dx_puts(" bytes=" as *u8); dx_num(sz); dx_puts(" paragraphs=" as *u8); dx_num(np); dx_puts("\n" as *u8)
532 sys_exit(0); return 0
533 }
534 if dx_streq(cmd, "writerich" as *u8)==1 {
535 if argc<4 { dx_puts("usage: nx_docx writerich <path.docx> <specfile>\n" as *u8); sys_exit(2); return 2 }
536 let spec: *u8=sys_mmap(K_MAGIC_1048592)
537 let slen: i64=dx_readfile(argv[3] as *u8, spec, K_MAGIC_1048576)
538 if slen<=0 { dx_puts("DOCX-RICH-FAIL specfile-missing-or-empty\n" as *u8); sys_exit(1); return 1 }
539 let sz: i64=docx_write_rich(path, spec, slen)
540 if sz<0 { dx_puts("DOCX-RICH-FAIL write\n" as *u8); sys_exit(1); return 1 }
541 dx_puts("DOCX-WRITE-RICH-OK path=" as *u8); dx_puts(path); dx_puts(" bytes=" as *u8); dx_num(sz); dx_puts("\n" as *u8)
542 sys_exit(0); return 0
543 }
544 if dx_streq(cmd, "read" as *u8)==1 {
545 let out: *u8=sys_mmap(K_MAGIC_1048592)
546 let tl: i64=docx_read_text(path, out, K_MAGIC_1048576)
547 if tl<0 { dx_puts("DOCX-READ-FAIL code=" as *u8); dx_num(tl); dx_puts("\n" as *u8); sys_exit(1); return 1 }
548 dx_puts("DOCX-TEXT-BEGIN\n" as *u8)
549 sys_write(1, out, tl)
550 dx_puts("\nDOCX-TEXT-END len=" as *u8); dx_num(tl); dx_puts("\n" as *u8)
551 sys_exit(0); return 0
552 }
553 if dx_streq(cmd, "html" as *u8)==1 {
554 if argc<4 { dx_puts("usage: nx_docx html <path.docx> <out.html> [download_url]\n" as *u8); sys_exit(2); return 2 }
555 let outp: *u8=argv[3] as *u8
556 var dlu: *u8=0 as *u8
557 if argc>4 { dlu=argv[4] as *u8 }
558 let out: *u8=sys_mmap(K_MAGIC_2097168)
559 let n: i64=dx_to_html(path, out, K_MAGIC_2097152, dlu)
560 if n<0 { dx_puts("DOCX-HTML-FAIL code=" as *u8); dx_num(n); dx_puts("\n" as *u8); sys_exit(1); return 1 }
561 let fd: i64=sys_openat_wr(outp, 0x1a4)
562 if fd<0 { dx_puts("DOCX-HTML-FAIL cannot-write\n" as *u8); sys_exit(1); return 1 }
563 sys_write(fd, out, n); sys_close(fd)
564 dx_puts("DOCX-HTML-OK path=" as *u8); dx_puts(outp); dx_puts(" bytes=" as *u8); dx_num(n); dx_puts("\n" as *u8)
565 sys_exit(0); return 0
566 }
567 dx_puts("nx_docx: unknown command (write|writerich|read|html)\n" as *u8)
568 sys_exit(2); return 2
569}