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,"&amp;" as *u8) } else { if c==60 { o=dx_cat(dst,o,"&lt;" as *u8) } else { if c==62 { o=dx_cat(dst,o,"&gt;" 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,"&amp;" as *u8) } else { if c==60 { o=dx_cat(dst,o,"&lt;" as *u8) } else { if c==62 { o=dx_cat(dst,o,"&gt;" 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,"&amp;" as *u8)==1 { out[o]=38 as u8; o=o+1; j=j+5 } else { 365 if dx_match(docbuf,j,doclen,"&lt;" as *u8)==1 { out[o]=60 as u8; o=o+1; j=j+4 } else { 366 if dx_match(docbuf,j,doclen,"&gt;" as *u8)==1 { out[o]=62 as u8; o=o+1; j=j+4 } else { 367 if dx_match(docbuf,j,doclen,"&quot;" as *u8)==1 { out[o]=34 as u8; o=o+1; j=j+6 } else { 368 if dx_match(docbuf,j,doclen,"&apos;" 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 (&amp; &lt; &gt;) 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>&#128196; Document Preview &mdash; 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,"\">&#11015; 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> &middot; viewable inline in the Nishi browser &middot; 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}