code wiki / _hdl_build / nx_xlsx.nx
nx_xlsx.nx source
↩ module page · 1283 lines · 60962 B
1// nx_xlsx.nx -- SOVEREIGN MS-Excel (.xlsx / SpreadsheetML) interop for the Nishi Office suite. A .xlsx is an OPC
2// package (framed by the shared nx_opc, same ZIP as .docx) with 5 parts:
3// [Content_Types].xml, _rels/.rels, xl/workbook.xml, xl/_rels/workbook.xml.rels, xl/worksheets/sheet1.xml
4// fromtsv : a tab-separated grid -> a valid Excel-openable .xlsx (numeric cells as <v>, text as inlineStr;
5// cell text XML-escaped). Use case: an estate asset SCHEDULE / valuation workbook.
6// read : a .xlsx -> the sheet1 grid back as TSV (handles our inlineStr + numeric cells).
7// Sovereignty = our own SpreadsheetML; rigor = the harness proves a 3rd-party (python zipfile) opens our .xlsx
8// with valid CRC-32s and the expected cell values.
9// HONEST LIMITATION: read supports inlineStr + numeric <v>; a real Excel file that uses a sharedStrings table
10// (t="s") would yield string INDICES not text -- sharedStrings read is a follow-on. Sparse columns are emitted
11// in document order (dense round-trip is exact; gaps are not repositioned).
12// nx_xlsx fromtsv <in.tsv> <out.xlsx> | nx_xlsx read <in.xlsx>
13// license_tier: ORIGINAL
14import "nx_syscalls.nx"
15import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
16import "nx_opc.nx"
17const XF_MAGIC_10000: i64 = 10000
18const XF_MAGIC_8388624: i64 = 8388624
19const XF_MAGIC_4194320: i64 = 4194320
20const XF_MAGIC_4194304: i64 = 4194304
21const XF_MAGIC_8388608: i64 = 8388608
22const XF_MAGIC_1048592: i64 = 1048592
23const XF_MAGIC_16384: i64 = 16384
24const XF_MAGIC_33554432: i64 = 33554432
25const XF_MAGIC_1048576: i64 = 1048576
26
27func xl_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
28// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
29// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
30// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
31// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
32func xl_num(v: i64) -> i64 { nxi_out(v); return 0 }
33func xl_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 }
34func xl_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
35func xl_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 }
36func xl_uint(dst: *u8, off: i64, v: i64) -> i64 { var m: i64=v; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var o: i64=off; var i: i64=0; while i<k{dst[o]=t[k-1-i];o=o+1;i=i+1} return o }
37func xl_match(buf: *u8, pos: i64, n: i64, pat: *u8) -> i64 {
38 var k: i64=0; while pat[k]!=(0 as u8) { if pos+k>=n { return 0 } if buf[pos+k]!=pat[k] { return 0 } k=k+1 } return 1
39}
40// bijective base-26 column label (0->A, 25->Z, 26->AA) appended to dst
41func xl_col(dst: *u8, off: i64, idx: i64) -> i64 {
42 var n: i64=idx+1; let t: *u8=sys_mmap(8); var k: i64=0
43 while n>0 { let rem: i64=(n-1)%26; t[k]=(65+rem) as u8; k=k+1; n=(n-1)/26 }
44 var o: i64=off; var i: i64=k-1; while i>=0 { dst[o]=t[i]; o=o+1; i=i-1 } return o
45}
46// is src[soff..soff+slen) a plain number? (optional leading '-', digits, at most one '.')
47func xl_is_numeric(src: *u8, soff: i64, slen: i64) -> i64 {
48 if slen<=0 { return 0 }
49 var dots: i64=0; var digits: i64=0; var i: i64=0
50 while i<slen {
51 let c: i64=src[soff+i] as i64
52 var ok: i64=0
53 if c>=48 { if c<=57 { ok=1; digits=digits+1 } }
54 if c==45 { if i==0 { ok=1 } }
55 if c==46 { dots=dots+1; if dots<=1 { ok=1 } }
56 if ok==0 { return 0 }
57 i=i+1
58 }
59 if digits==0 { return 0 }
60 return 1
61}
62// append src[soff..soff+slen) escaping XML & < > (and " for safety)
63func xl_escape_region(out: *u8, o: i64, src: *u8, soff: i64, slen: i64) -> i64 {
64 var oo: i64=o; var k: i64=0
65 while k<slen {
66 let c: i64=src[soff+k] as i64
67 if c==38 { oo=xl_cat(out,oo,"&" as *u8) } else { if c==60 { oo=xl_cat(out,oo,"<" as *u8) } else { if c==62 { oo=xl_cat(out,oo,">" as *u8) } else { out[oo]=src[soff+k]; oo=oo+1 } } }
68 k=k+1
69 }
70 return oo
71}
72// emit one cell <c r="<col><row>" ...> for src[soff..soff+slen)
73func xl_emit_cell(out: *u8, o: i64, col: i64, row: i64, src: *u8, soff: i64, slen: i64) -> i64 {
74 var oo: i64=o
75 oo=xl_cat(out,oo,"<c r=\"" as *u8); oo=xl_col(out,oo,col); oo=xl_uint(out,oo,row); oo=xl_cat(out,oo,"\"" as *u8)
76 if slen>0 { if xl_is_numeric(src,soff,slen)==1 {
77 oo=xl_cat(out,oo,"><v>" as *u8)
78 var k: i64=0; while k<slen { out[oo]=src[soff+k]; oo=oo+1; k=k+1 }
79 oo=xl_cat(out,oo,"</v></c>" as *u8)
80 return oo
81 } }
82 oo=xl_cat(out,oo," t=\"inlineStr\"><is><t xml:space=\"preserve\">" as *u8)
83 oo=xl_escape_region(out,oo,src,soff,slen)
84 oo=xl_cat(out,oo,"</t></is></c>" as *u8)
85 return oo
86}
87// build xl/worksheets/sheet1.xml from a tab-separated grid; return length
88func xl_build_sheet(tsv: *u8, tn: i64, out: *u8) -> i64 {
89 var o: i64=xl_cat(out,0,"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><sheetData>" as *u8)
90 var rownum: i64=0; var pos: i64=0
91 while pos<tn {
92 var le: i64=tn; var i: i64=pos; while i<tn { if tsv[i]==(10 as u8) { le=i; break } i=i+1 }
93 if le>pos {
94 var lineend: i64=le; if tsv[lineend-1]==(13 as u8) { lineend=lineend-1 }
95 rownum=rownum+1
96 o=xl_cat(out,o,"<row r=\"" as *u8); o=xl_uint(out,o,rownum); o=xl_cat(out,o,"\">" as *u8)
97 var cs: i64=pos; var col: i64=0; var j: i64=pos
98 while j<=lineend {
99 var sep: i64=0
100 if j==lineend { sep=1 } else { if tsv[j]==(9 as u8) { sep=1 } }
101 if sep==1 { o=xl_emit_cell(out, o, col, rownum, tsv, cs, j-cs); col=col+1; cs=j+1 }
102 j=j+1
103 }
104 o=xl_cat(out,o,"</row>" as *u8)
105 }
106 pos=le+1
107 }
108 o=xl_cat(out,o,"</sheetData></worksheet>" as *u8)
109 return o
110}
111
112// ---- FORMULAS (OF-S3): '='-cells in the TSV become real SpreadsheetML <f> formula cells with a computed
113// cached <v> (exactly how Excel stores them). Values are Q4 FIXED-POINT (value x 10000, 4 decimal places;
114// integer arithmetic, division floors) -- honest scope: SUM/AVERAGE/MIN/MAX/COUNT + - * / ( ) unary-,
115// A1/AB12 refs and A1:B9 ranges; formula-referencing-formula chains memoized; cycles/div-0/bad-ref/syntax
116// LOUD-FAIL the write (never a silently wrong workbook). ----
117const XF_MAXC: i64 = 64
118const XF_MAXR: i64 = 1024
119
120struct XfGrid {
121 tsv: *u8,
122 gv: *i64, // Q4 value per cell [r*XF_MAXC+c]
123 gk: *u8, // 0 empty | 1 number | 2 text | 3 formula
124 gs: *u8, // formula state: 0 todo | 1 in-progress | 2 done
125 gfo: *i64, // formula span offset (past '=')
126 gfl: *i64, // formula span length
127 nrows: i64,
128 ncols: i64,
129 err: i64, // 0 ok | 1 syntax | 2 bad-ref/value | 3 cycle | 4 div-0
130 errow: i64,
131 ercol: i64
132}
133
134func xf_grid_new(tsv: *u8) -> *XfGrid {
135 let g: *XfGrid = sys_mmap(96) as *XfGrid
136 g.tsv = tsv
137 g.gv = sys_mmap(8*XF_MAXR*XF_MAXC) as *i64
138 g.gk = sys_mmap(XF_MAXR*XF_MAXC)
139 g.gs = sys_mmap(XF_MAXR*XF_MAXC)
140 g.gfo = sys_mmap(8*XF_MAXR*XF_MAXC) as *i64
141 g.gfl = sys_mmap(8*XF_MAXR*XF_MAXC) as *i64
142 g.nrows = 0
143 g.ncols = 0
144 g.err = 0
145 g.errow = 0
146 g.ercol = 0
147 return g
148}
149
150func xf_ws(s: *u8, ip: *i64, e: i64) -> i64 {
151 while ip[0] < e { if s[ip[0]]==(32 as u8) { ip[0]=ip[0]+1 } else { return 0 } }
152 return 0
153}
154
155// verified-numeric text -> Q4 (sign, int, up to 4 frac digits; extras truncated)
156func xf_q4_of(s: *u8, off: i64, len: i64) -> i64 {
157 var i: i64=0
158 var neg: i64=0
159 if len>0 { if s[off]==(45 as u8) { neg=1; i=1 } }
160 var v: i64=0
161 while i<len { let c: i64=s[off+i] as i64; if c==46 { break } v=v*10+(c-48); i=i+1 }
162 v=v*XF_MAGIC_10000
163 if i<len { if s[off+i]==(46 as u8) {
164 i=i+1
165 var sc: i64=1000
166 while i<len { if sc>0 { let c2: i64=s[off+i] as i64; v=v+(c2-48)*sc; sc=sc/10 } i=i+1 }
167 } }
168 if neg==1 { return 0-v }
169 return v
170}
171
172// Q4 -> decimal text (frac trimmed)
173func xf_fmtq4(dst: *u8, off: i64, v: i64) -> i64 {
174 var o: i64=off
175 var m: i64=v
176 if m<0 { dst[o]=45 as u8; o=o+1; m=0-m }
177 let ipart: i64=m/XF_MAGIC_10000
178 var fpart: i64=m%XF_MAGIC_10000
179 o=xl_uint(dst,o,ipart)
180 if fpart>0 {
181 dst[o]=46 as u8; o=o+1
182 let t: *u8=sys_mmap(8)
183 t[0]=(48+fpart/1000) as u8
184 t[1]=(48+(fpart/100)%10) as u8
185 t[2]=(48+(fpart/10)%10) as u8
186 t[3]=(48+fpart%10) as u8
187 var last: i64=3
188 while last>0 { if t[last]==(48 as u8) { last=last-1 } else { break } }
189 var q: i64=0
190 while q<=last { dst[o]=t[q]; o=o+1; q=q+1 }
191 }
192 return o
193}
194
195// parse an A1/AB12 ref at *ip; rc[0]=col rc[1]=row (0-based); 1 ok, 0 not-a-ref (ip restored by caller)
196func xf_refparse(s: *u8, ip: *i64, e: i64, rc: *i64) -> i64 {
197 var i: i64=ip[0]
198 var col: i64=0
199 var nl: i64=0
200 while i<e { let c: i64=s[i] as i64; if c>=65 { if c<=90 { col=col*26+(c-64); nl=nl+1; i=i+1 } else { break } } else { break } }
201 if nl<1 { return 0 }
202 if nl>2 { return 0 }
203 var row: i64=0
204 var nd: i64=0
205 while i<e { let c2: i64=s[i] as i64; if c2>=48 { if c2<=57 { row=row*10+(c2-48); nd=nd+1; i=i+1 } else { break } } else { break } }
206 if nd<1 { return 0 }
207 if row<1 { return 0 }
208 rc[0]=col-1
209 rc[1]=row-1
210 ip[0]=i
211 return 1
212}
213
214// numeric value of cell (r,c): numbers directly; formulas evaluated on demand (memo + cycle detect);
215// empty/text/out-of-range -> err 2 (LOUD beats silently-wrong money).
216func xf_cellnum(g: *XfGrid, r: i64, c: i64) -> i64 {
217 if g.err != 0 { return 0 }
218 if r<0 { g.err=2; return 0 }
219 if c<0 { g.err=2; return 0 }
220 if r>=XF_MAXR { g.err=2; return 0 }
221 if c>=XF_MAXC { g.err=2; return 0 }
222 let idx: i64=r*XF_MAXC+c
223 let k: i64=g.gk[idx] as i64
224 if k==1 { return g.gv[idx] }
225 if k==3 {
226 let st: i64=g.gs[idx] as i64
227 if st==2 { return g.gv[idx] }
228 if st==1 { g.err=3; return 0 }
229 xf_evalcell(g, r, c)
230 if g.err != 0 { return 0 }
231 return g.gv[idx]
232 }
233 g.err=2
234 return 0
235}
236
237func xf_factor(g: *XfGrid, ip: *i64, e: i64) -> i64 {
238 if g.err != 0 { return 0 }
239 let s: *u8=g.tsv
240 xf_ws(s, ip, e)
241 if ip[0]>=e { g.err=1; return 0 }
242 let c0: i64=s[ip[0]] as i64
243 if c0==40 {
244 ip[0]=ip[0]+1
245 let v: i64=xf_expr(g, ip, e)
246 xf_ws(s, ip, e)
247 if ip[0]<e { if s[ip[0]]==(41 as u8) { ip[0]=ip[0]+1; return v } }
248 g.err=1
249 return 0
250 }
251 if c0==45 { ip[0]=ip[0]+1; let v2: i64=xf_factor(g, ip, e); return 0-v2 }
252 var isnum: i64=0
253 if c0>=48 { if c0<=57 { isnum=1 } }
254 if c0==46 { isnum=1 }
255 if isnum==1 {
256 let st: i64=ip[0]
257 var i: i64=ip[0]
258 var dots: i64=0
259 while i<e { let cc: i64=s[i] as i64; var ok: i64=0; if cc>=48 { if cc<=57 { ok=1 } } if cc==46 { dots=dots+1; if dots<=1 { ok=1 } } if ok==1 { i=i+1 } else { break } }
260 ip[0]=i
261 return xf_q4_of(s, st, i-st)
262 }
263 if c0>=65 { if c0<=90 {
264 // FUNC( ... ) or a cell ref
265 var la: i64=ip[0]
266 var nlet: i64=0
267 while la<e { let cl: i64=s[la] as i64; if cl>=65 { if cl<=90 { nlet=nlet+1; la=la+1 } else { break } } else { break } }
268 var isfunc: i64=0
269 if la<e { if s[la]==(40 as u8) { isfunc=1 } }
270 if isfunc==1 {
271 // identify: SUM AVERAGE MIN MAX COUNT
272 var fid: i64=0
273 if nlet==3 { if s[ip[0]]==(83 as u8) { if s[ip[0]+1]==(85 as u8) { if s[ip[0]+2]==(77 as u8) { fid=1 } } } }
274 if nlet==7 { if s[ip[0]]==(65 as u8) { fid=2 } }
275 if nlet==3 { if s[ip[0]]==(77 as u8) { if s[ip[0]+1]==(73 as u8) { fid=3 } } }
276 if nlet==3 { if s[ip[0]]==(77 as u8) { if s[ip[0]+1]==(65 as u8) { fid=4 } } }
277 if nlet==5 { if s[ip[0]]==(67 as u8) { fid=5 } }
278 if fid==0 { g.err=1; return 0 }
279 ip[0]=la+1
280 var sum: i64=0
281 var cnt: i64=0
282 var mn: i64=0
283 var mx: i64=0
284 var go: i64=1
285 while go==1 {
286 if g.err != 0 { return 0 }
287 xf_ws(s, ip, e)
288 // arg = RANGE (ref:ref) | expr
289 let save: i64=ip[0]
290 let rc1: *i64=sys_mmap(32) as *i64
291 var isrange: i64=0
292 if xf_refparse(s, ip, e, rc1)==1 { if ip[0]<e { if s[ip[0]]==(58 as u8) {
293 ip[0]=ip[0]+1
294 let rc2: *i64=sys_mmap(32) as *i64
295 if xf_refparse(s, ip, e, rc2)==0 { g.err=1; return 0 }
296 isrange=1
297 var r1: i64=rc1[1]
298 var r2: i64=rc2[1]
299 var c1: i64=rc1[0]
300 var c2: i64=rc2[0]
301 if r2<r1 { let tt: i64=r1; r1=r2; r2=tt }
302 if c2<c1 { let t2: i64=c1; c1=c2; c2=t2 }
303 var rr: i64=r1
304 while rr<=r2 {
305 var cc2: i64=c1
306 while cc2<=c2 {
307 let idx2: i64=rr*XF_MAXC+cc2
308 var kk: i64=0
309 if rr<XF_MAXR { if cc2<XF_MAXC { kk=g.gk[idx2] as i64 } }
310 var vv: i64=0
311 var incl: i64=0
312 if kk==1 { vv=g.gv[idx2]; incl=1 }
313 if kk==3 { vv=xf_cellnum(g, rr, cc2); incl=1; if g.err != 0 { return 0 } }
314 if incl==1 {
315 sum=sum+vv
316 if cnt==0 { mn=vv; mx=vv } else { if vv<mn { mn=vv } if vv>mx { mx=vv } }
317 cnt=cnt+1
318 }
319 cc2=cc2+1
320 }
321 rr=rr+1
322 }
323 } } }
324 if isrange==0 {
325 ip[0]=save
326 let v3: i64=xf_expr(g, ip, e)
327 if g.err != 0 { return 0 }
328 sum=sum+v3
329 if cnt==0 { mn=v3; mx=v3 } else { if v3<mn { mn=v3 } if v3>mx { mx=v3 } }
330 cnt=cnt+1
331 }
332 xf_ws(s, ip, e)
333 if ip[0]>=e { g.err=1; return 0 }
334 if s[ip[0]]==(44 as u8) { ip[0]=ip[0]+1 } else {
335 if s[ip[0]]==(41 as u8) { ip[0]=ip[0]+1; go=0 } else { g.err=1; return 0 }
336 }
337 }
338 if fid==1 { return sum }
339 if fid==2 { if cnt==0 { g.err=4; return 0 } return sum/cnt }
340 if fid==3 { if cnt==0 { g.err=2; return 0 } return mn }
341 if fid==4 { if cnt==0 { g.err=2; return 0 } return mx }
342 return cnt*XF_MAGIC_10000
343 }
344 // plain cell ref
345 let rc: *i64=sys_mmap(32) as *i64
346 if xf_refparse(s, ip, e, rc)==1 { return xf_cellnum(g, rc[1], rc[0]) }
347 g.err=1
348 return 0
349 } }
350 g.err=1
351 return 0
352}
353
354func xf_term(g: *XfGrid, ip: *i64, e: i64) -> i64 {
355 var v: i64=xf_factor(g, ip, e)
356 var go: i64=1
357 while go==1 {
358 if g.err != 0 { return 0 }
359 xf_ws(g.tsv, ip, e)
360 var op: i64=0
361 if ip[0]<e { op=g.tsv[ip[0]] as i64 }
362 if op==42 {
363 ip[0]=ip[0]+1
364 let r: i64=xf_factor(g, ip, e)
365 v=(v*r)/XF_MAGIC_10000
366 } else { if op==47 {
367 ip[0]=ip[0]+1
368 let r2: i64=xf_factor(g, ip, e)
369 if r2==0 { g.err=4; return 0 }
370 v=(v*XF_MAGIC_10000)/r2
371 } else { go=0 } }
372 }
373 return v
374}
375
376func xf_expr(g: *XfGrid, ip: *i64, e: i64) -> i64 {
377 var v: i64=xf_term(g, ip, e)
378 var go: i64=1
379 while go==1 {
380 if g.err != 0 { return 0 }
381 xf_ws(g.tsv, ip, e)
382 var op: i64=0
383 if ip[0]<e { op=g.tsv[ip[0]] as i64 }
384 if op==43 {
385 ip[0]=ip[0]+1
386 let r: i64=xf_term(g, ip, e)
387 v=v+r
388 } else { if op==45 {
389 ip[0]=ip[0]+1
390 let r2: i64=xf_term(g, ip, e)
391 v=v-r2
392 } else { go=0 } }
393 }
394 return v
395}
396
397func xf_evalcell(g: *XfGrid, r: i64, c: i64) -> i64 {
398 let idx: i64=r*XF_MAXC+c
399 g.gs[idx]=1 as u8
400 let ipb: *i64=sys_mmap(16) as *i64
401 ipb[0]=g.gfo[idx]
402 let e: i64=g.gfo[idx]+g.gfl[idx]
403 let v: i64=xf_expr(g, ipb, e)
404 if g.err==0 { xf_ws(g.tsv, ipb, e); if ipb[0] != e { g.err=1 } }
405 if g.err != 0 { return 0-1 }
406 g.gv[idx]=v
407 g.gs[idx]=2 as u8
408 return 0
409}
410
411// classify the TSV into the grid, then evaluate EVERY formula cell. 0 ok; nonzero = g.err (LOUD).
412func xf_load(g: *XfGrid, tsv: *u8, tn: i64) -> i64 {
413 var row: i64=0
414 var pos: i64=0
415 while pos<tn {
416 var le: i64=tn
417 var i: i64=pos
418 while i<tn { if tsv[i]==(10 as u8) { le=i; break } i=i+1 }
419 if le>pos {
420 var lineend: i64=le
421 if tsv[lineend-1]==(13 as u8) { lineend=lineend-1 }
422 var cs: i64=pos
423 var col: i64=0
424 var j: i64=pos
425 while j<=lineend {
426 var sep: i64=0
427 if j==lineend { sep=1 } else { if tsv[j]==(9 as u8) { sep=1 } }
428 if sep==1 {
429 if row<XF_MAXR { if col<XF_MAXC {
430 let idx: i64=row*XF_MAXC+col
431 let clen: i64=j-cs
432 if clen<=0 { g.gk[idx]=0 as u8 } else {
433 if tsv[cs]==(61 as u8) {
434 g.gk[idx]=3 as u8
435 g.gs[idx]=0 as u8
436 g.gfo[idx]=cs+1
437 g.gfl[idx]=clen-1
438 } else {
439 if xl_is_numeric(tsv, cs, clen)==1 { g.gk[idx]=1 as u8; g.gv[idx]=xf_q4_of(tsv, cs, clen) } else { g.gk[idx]=2 as u8 }
440 }
441 }
442 } }
443 col=col+1
444 cs=j+1
445 }
446 j=j+1
447 }
448 if col>g.ncols { g.ncols=col }
449 row=row+1
450 }
451 pos=le+1
452 }
453 g.nrows=row
454 // evaluate all formulas (memoized; chains resolve on demand; first error wins, attributed to its cell)
455 var r2: i64=0
456 while r2<g.nrows {
457 if r2>=XF_MAXR { break }
458 var c2: i64=0
459 while c2<g.ncols {
460 if c2>=XF_MAXC { break }
461 let idx2: i64=r2*XF_MAXC+c2
462 if (g.gk[idx2] as i64)==3 { if (g.gs[idx2] as i64)==0 {
463 g.errow=r2
464 g.ercol=c2
465 xf_evalcell(g, r2, c2)
466 if g.err != 0 { return g.err }
467 } }
468 c2=c2+1
469 }
470 r2=r2+1
471 }
472 return 0
473}
474
475// formula-aware sheet builder: '='-cells emit <f>FORMULA</f><v>computed</v>; all other cells take the
476// EXACT existing xl_emit_cell path (byte-identical output for formula-free workbooks).
477func xl_build_sheet_g(g: *XfGrid, tsv: *u8, tn: i64, out: *u8) -> i64 {
478 var o: i64=xl_cat(out,0,"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><sheetData>" as *u8)
479 var rownum: i64=0
480 var pos: i64=0
481 while pos<tn {
482 var le: i64=tn
483 var i: i64=pos
484 while i<tn { if tsv[i]==(10 as u8) { le=i; break } i=i+1 }
485 if le>pos {
486 var lineend: i64=le
487 if tsv[lineend-1]==(13 as u8) { lineend=lineend-1 }
488 rownum=rownum+1
489 o=xl_cat(out,o,"<row r=\"" as *u8); o=xl_uint(out,o,rownum); o=xl_cat(out,o,"\">" as *u8)
490 var cs: i64=pos
491 var col: i64=0
492 var j: i64=pos
493 while j<=lineend {
494 var sep: i64=0
495 if j==lineend { sep=1 } else { if tsv[j]==(9 as u8) { sep=1 } }
496 if sep==1 {
497 var isform: i64=0
498 if rownum-1<XF_MAXR { if col<XF_MAXC {
499 let idx: i64=(rownum-1)*XF_MAXC+col
500 if (g.gk[idx] as i64)==3 {
501 isform=1
502 o=xl_cat(out,o,"<c r=\"" as *u8); o=xl_col(out,o,col); o=xl_uint(out,o,rownum); o=xl_cat(out,o,"\"><f>" as *u8)
503 o=xl_escape_region(out,o,tsv,g.gfo[idx],g.gfl[idx])
504 o=xl_cat(out,o,"</f><v>" as *u8)
505 o=xf_fmtq4(out,o,g.gv[idx])
506 o=xl_cat(out,o,"</v></c>" as *u8)
507 }
508 } }
509 if isform==0 { o=xl_emit_cell(out, o, col, rownum, tsv, cs, j-cs) }
510 col=col+1
511 cs=j+1
512 }
513 j=j+1
514 }
515 o=xl_cat(out,o,"</row>" as *u8)
516 }
517 pos=le+1
518 }
519 o=xl_cat(out,o,"</sheetData></worksheet>" as *u8)
520 return o
521}
522
523
524// ================= DIRECTIVES: CONDITIONAL FORMATTING / DATA VALIDATION / MACROS ==================
525// Both capabilities are DATA-DRIVEN: they are carried in the sheet source as '#' directive lines, so a
526// spreadsheet is still one plain TSV file and nothing needs a side-car format.
527// #CF <sqref> <op> <value> conditional formatting e.g. #CF B2:B4 greaterThan 100
528// #DV <sqref> <min> <max> data validation (whole, between)
529// #MACRO SET <row> <col> <value> write one cell
530// #MACRO FILL <r1> <c1> <r2> <c2> <value>
531// #MACRO CLEAR <row> <col>
532//
533// MACROS RUN BEFORE FORMULA EVALUATION, and that ordering is the whole point: a macro-written cell must
534// feed SUM, so the macro rewrites the SHEET SOURCE and the existing formula engine then loads the
535// already-macroed grid. Deliberately NOT Turing-complete -- no loops, no branches, no I/O, no exec. A
536// spreadsheet that can run arbitrary code is a malware format; this one can only move numbers it was
537// handed, which is the whole reason macro-enabled formats are a security disaster.
538const XL_MAXR: i64 = 512
539const XL_MAXC: i64 = 64
540const XL_CELLW: i64 = 40
541
542func xl_dir_is(buf: *u8, s: i64, e: i64, tag: *u8) -> i64 {
543 if s >= e { return 0 }
544 if buf[s] != (35 as u8) { return 0 }
545 var i: i64 = 0
546 while tag[i] != (0 as u8) { if s + i >= e { return 0 } if buf[s + i] != tag[i] { return 0 } i = i + 1 }
547 return 1
548}
549func xl_tok(buf: *u8, p: i64, e: i64, out: *u8, cap: i64, np: *i64) -> i64 {
550 var i: i64 = p
551 while i < e { if buf[i] != (32 as u8) { if buf[i] != (9 as u8) { break } } i = i + 1 }
552 var k: i64 = 0
553 while i < e { if buf[i] == (32 as u8) { break } if buf[i] == (9 as u8) { break } if k < cap - 1 { out[k] = buf[i]; k = k + 1 } i = i + 1 }
554 out[k] = 0 as u8
555 np[0] = i
556 return k
557}
558func xl_atoi_s(s: *u8) -> i64 {
559 var v: i64 = 0
560 var i: i64 = 0
561 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 }
562 return v
563}
564
565// Apply every #MACRO to the sheet source and emit a TSV with the directive lines REMOVED (a directive is
566// not a data row). Returns the new length. This is what runs before xf_grid_new/xf_load.
567func xl_macro_apply(tsv: *u8, n: i64, out: *u8, cap: i64) -> i64 {
568 let cells: *u8 = sys_mmap(XL_MAXR * XL_MAXC * XL_CELLW)
569 let used: *i64 = sys_mmap(XL_MAXR * 8) as *i64
570 var r: i64 = 0
571 while r < XL_MAXR { used[r] = 0; r = r + 1 }
572 var maxr: i64 = 0
573
574 // pass 1: load the data rows into the grid
575 var pos: i64 = 0
576 var row: i64 = 0
577 while pos < n {
578 var le: i64 = n
579 var i: i64 = pos
580 while i < n { if tsv[i] == (10 as u8) { le = i; break } i = i + 1 }
581 if le > pos { if tsv[pos] != (35 as u8) {
582 var col: i64 = 0
583 var cs: i64 = pos
584 var j: i64 = pos
585 while j <= le {
586 if j == le {
587 if col < XL_MAXC { if row < XL_MAXR {
588 var k: i64 = 0
589 while cs + k < j { if k < XL_CELLW - 1 { cells[(row * XL_MAXC + col) * XL_CELLW + k] = tsv[cs + k] } k = k + 1 }
590 cells[(row * XL_MAXC + col) * XL_CELLW + k] = 0 as u8
591 } }
592 col = col + 1
593 } else { if tsv[j] == (9 as u8) {
594 if col < XL_MAXC { if row < XL_MAXR {
595 var k2: i64 = 0
596 while cs + k2 < j { if k2 < XL_CELLW - 1 { cells[(row * XL_MAXC + col) * XL_CELLW + k2] = tsv[cs + k2] } k2 = k2 + 1 }
597 cells[(row * XL_MAXC + col) * XL_CELLW + k2] = 0 as u8
598 } }
599 col = col + 1
600 cs = j + 1
601 } }
602 j = j + 1
603 }
604 if row < XL_MAXR { used[row] = col }
605 if row + 1 > maxr { maxr = row + 1 }
606 row = row + 1
607 } }
608 pos = le + 1
609 }
610
611 // pass 2: apply the macros in source order
612 let t: *u8 = sys_mmap(64)
613 let np: *i64 = sys_mmap(16) as *i64
614 pos = 0
615 while pos < n {
616 var le2: i64 = n
617 var i2: i64 = pos
618 while i2 < n { if tsv[i2] == (10 as u8) { le2 = i2; break } i2 = i2 + 1 }
619 if xl_dir_is(tsv, pos, le2, "#MACRO" as *u8) == 1 {
620 var q: i64 = pos + 6
621 xl_tok(tsv, q, le2, t, 64, np)
622 q = np[0]
623 if xl_streq(t, "SET" as *u8) == 1 {
624 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let rr: i64 = xl_atoi_s(t) - 1
625 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let cc: i64 = xl_atoi_s(t) - 1
626 xl_tok(tsv, q, le2, t, 64, np); q = np[0]
627 if rr >= 0 { if cc >= 0 { if rr < XL_MAXR { if cc < XL_MAXC {
628 var k3: i64 = 0
629 while t[k3] != (0 as u8) { cells[(rr * XL_MAXC + cc) * XL_CELLW + k3] = t[k3]; k3 = k3 + 1 }
630 cells[(rr * XL_MAXC + cc) * XL_CELLW + k3] = 0 as u8
631 if used[rr] <= cc { used[rr] = cc + 1 }
632 if rr + 1 > maxr { maxr = rr + 1 }
633 } } } }
634 }
635 if xl_streq(t, "CLEAR" as *u8) == 1 {
636 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let rr2: i64 = xl_atoi_s(t) - 1
637 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let cc2: i64 = xl_atoi_s(t) - 1
638 if rr2 >= 0 { if cc2 >= 0 { if rr2 < XL_MAXR { if cc2 < XL_MAXC { cells[(rr2 * XL_MAXC + cc2) * XL_CELLW] = 0 as u8 } } } }
639 }
640 if xl_streq(t, "FILL" as *u8) == 1 {
641 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let r1: i64 = xl_atoi_s(t) - 1
642 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let c1: i64 = xl_atoi_s(t) - 1
643 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let r2: i64 = xl_atoi_s(t) - 1
644 xl_tok(tsv, q, le2, t, 64, np); q = np[0]; let c2: i64 = xl_atoi_s(t) - 1
645 xl_tok(tsv, q, le2, t, 64, np); q = np[0]
646 var rr3: i64 = r1
647 while rr3 <= r2 {
648 var cc3: i64 = c1
649 while cc3 <= c2 {
650 if rr3 >= 0 { if cc3 >= 0 { if rr3 < XL_MAXR { if cc3 < XL_MAXC {
651 var k4: i64 = 0
652 while t[k4] != (0 as u8) { cells[(rr3 * XL_MAXC + cc3) * XL_CELLW + k4] = t[k4]; k4 = k4 + 1 }
653 cells[(rr3 * XL_MAXC + cc3) * XL_CELLW + k4] = 0 as u8
654 if used[rr3] <= cc3 { used[rr3] = cc3 + 1 }
655 if rr3 + 1 > maxr { maxr = rr3 + 1 }
656 } } } }
657 cc3 = cc3 + 1
658 }
659 rr3 = rr3 + 1
660 }
661 }
662 }
663 pos = le2 + 1
664 }
665
666 // pass 3: re-emit the grid as TSV (directives dropped)
667 var o: i64 = 0
668 var rr4: i64 = 0
669 while rr4 < maxr {
670 var cc4: i64 = 0
671 let nc: i64 = used[rr4]
672 while cc4 < nc {
673 let base: i64 = (rr4 * XL_MAXC + cc4) * XL_CELLW
674 var k5: i64 = 0
675 while cells[base + k5] != (0 as u8) { if o < cap - 2 { out[o] = cells[base + k5]; o = o + 1 } k5 = k5 + 1 }
676 if cc4 + 1 < nc { if o < cap - 2 { out[o] = 9 as u8; o = o + 1 } }
677 cc4 = cc4 + 1
678 }
679 if o < cap - 2 { out[o] = 10 as u8; o = o + 1 }
680 rr4 = rr4 + 1
681 }
682 out[o] = 0 as u8
683 return o
684}
685
686// styles.xml carrying a real <dxfs> entry. THE cfRule BELOW POINTS AT dxfId 0, and a cfRule pointing at a
687// missing dxf is a HARD CORRUPT FILE in Excel -- so the style part is not optional decoration, it is what
688// makes the conditional-formatting claim true rather than merely emitted.
689func xl_styles_dxf() -> *u8 {
690 return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><fonts count=\"1\"><font><sz val=\"11\"/><name val=\"Calibri\"/></font></fonts><fills count=\"2\"><fill><patternFill patternType=\"none\"/></fill><fill><patternFill patternType=\"gray125\"/></fill></fills><borders count=\"1\"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs><cellXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/></cellXfs><cellStyles count=\"1\"><cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\"/></cellStyles><dxfs count=\"1\"><dxf><font><color rgb=\"FF9C0006\"/></font><fill><patternFill><bgColor rgb=\"FFFFC7CE\"/></patternFill></fill></dxf></dxfs><tableStyles count=\"0\"/></styleSheet>" as *u8
691}
692
693// Emit <conditionalFormatting> and <dataValidations> collected from the #CF / #DV directives.
694func xl_emit_cf_dv(out: *u8, off: i64, tsv: *u8, n: i64) -> i64 {
695 var o: i64 = off
696 let t: *u8 = sys_mmap(64)
697 let sq: *u8 = sys_mmap(64)
698 let np: *i64 = sys_mmap(16) as *i64
699 var pri: i64 = 1
700 var pos: i64 = 0
701 while pos < n {
702 var le: i64 = n
703 var i: i64 = pos
704 while i < n { if tsv[i] == (10 as u8) { le = i; break } i = i + 1 }
705 if xl_dir_is(tsv, pos, le, "#CF" as *u8) == 1 {
706 var q: i64 = pos + 3
707 xl_tok(tsv, q, le, sq, 64, np); q = np[0]
708 xl_tok(tsv, q, le, t, 64, np); q = np[0]
709 o = xl_cat(out, o, "<conditionalFormatting sqref=\"" as *u8)
710 o = xl_cat(out, o, sq)
711 o = xl_cat(out, o, "\"><cfRule type=\"cellIs\" dxfId=\"0\" priority=\"" as *u8)
712 o = xl_uint(out, o, pri)
713 o = xl_cat(out, o, "\" operator=\"" as *u8)
714 o = xl_cat(out, o, t)
715 o = xl_cat(out, o, "\"><formula>" as *u8)
716 xl_tok(tsv, q, le, t, 64, np); q = np[0]
717 o = xl_cat(out, o, t)
718 o = xl_cat(out, o, "</formula></cfRule></conditionalFormatting>" as *u8)
719 pri = pri + 1
720 }
721 pos = le + 1
722 }
723 // data validations must be wrapped in ONE <dataValidations> with a count
724 var ndv: i64 = 0
725 pos = 0
726 while pos < n {
727 var le3: i64 = n
728 var i3: i64 = pos
729 while i3 < n { if tsv[i3] == (10 as u8) { le3 = i3; break } i3 = i3 + 1 }
730 if xl_dir_is(tsv, pos, le3, "#DV" as *u8) == 1 { ndv = ndv + 1 }
731 pos = le3 + 1
732 }
733 if ndv > 0 {
734 o = xl_cat(out, o, "<dataValidations count=\"" as *u8)
735 o = xl_uint(out, o, ndv)
736 o = xl_cat(out, o, "\">" as *u8)
737 pos = 0
738 while pos < n {
739 var le4: i64 = n
740 var i4: i64 = pos
741 while i4 < n { if tsv[i4] == (10 as u8) { le4 = i4; break } i4 = i4 + 1 }
742 if xl_dir_is(tsv, pos, le4, "#DV" as *u8) == 1 {
743 var q2: i64 = pos + 3
744 xl_tok(tsv, q2, le4, sq, 64, np); q2 = np[0]
745 o = xl_cat(out, o, "<dataValidation type=\"whole\" operator=\"between\" allowBlank=\"1\" showInputMessage=\"1\" showErrorMessage=\"1\" sqref=\"" as *u8)
746 o = xl_cat(out, o, sq)
747 o = xl_cat(out, o, "\"><formula1>" as *u8)
748 xl_tok(tsv, q2, le4, t, 64, np); q2 = np[0]
749 o = xl_cat(out, o, t)
750 o = xl_cat(out, o, "</formula1><formula2>" as *u8)
751 xl_tok(tsv, q2, le4, t, 64, np); q2 = np[0]
752 o = xl_cat(out, o, t)
753 o = xl_cat(out, o, "</formula2></dataValidation>" as *u8)
754 }
755 pos = le4 + 1
756 }
757 o = xl_cat(out, o, "</dataValidations>" as *u8)
758 }
759 return o
760}
761
762func xlsx_write_tsv(tsvpath: *u8, outpath: *u8) -> i64 {
763 let szp: *i64=sys_mmap(16) as *i64
764 let tsv: *u8=sys_read_file(tsvpath, szp)
765 if (tsv as i64)==0 { return 0-1 }
766 // FORMULAS: classify + evaluate '='-cells before emitting (LOUD on syntax/bad-ref/cycle/div-0)
767 // MACROS FIRST: rewrite the sheet source so a macro-written cell is loaded as data and therefore
768 // feeds SUM. Running them after xf_load would make the macro invisible to every formula.
769 let mtsv: *u8=sys_mmap(XF_MAGIC_8388624)
770 var mlen: i64=xl_macro_apply(tsv, szp[0], mtsv, XF_MAGIC_8388624)
771 if mlen <= 0 { mlen = szp[0] }
772 let g: *XfGrid=xf_grid_new(mtsv)
773 let frc: i64=xf_load(g, mtsv, mlen)
774 if frc != 0 {
775 xl_puts("XLSX-FORMULA-FAIL kind=" as *u8); xl_num(frc)
776 xl_puts(" cell_row=" as *u8); xl_num(g.errow+1)
777 xl_puts(" cell_col=" as *u8); xl_num(g.ercol+1)
778 xl_puts(" (1=syntax 2=bad-ref/value 3=CYCLE 4=div-0)\n" as *u8)
779 return 0-9
780 }
781 let sheet: *u8=sys_mmap(XF_MAGIC_8388624)
782 var slen: i64=xl_build_sheet_g(g, mtsv, mlen, sheet)
783 // splice CF/DV between </sheetData> and </worksheet> (they are invalid anywhere else)
784 slen = slen - 12
785 slen = xl_emit_cf_dv(sheet, slen, tsv, szp[0])
786 slen = xl_cat(sheet, slen, "</worksheet>" as *u8)
787 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=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/><Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/></Types>" as *u8
788 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=\"xl/workbook.xml\"/></Relationships>" as *u8
789 let wb: *u8="<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"><sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets></workbook>" as *u8
790 let wbr: *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/worksheet\" Target=\"worksheets/sheet1.xml\"/></Relationships>" as *u8
791 let names: *i64=sys_mmap(64) as *i64; let datap: *i64=sys_mmap(64) as *i64; let lens: *i64=sys_mmap(64) as *i64
792 names[0]="[Content_Types].xml" as *u8 as i64; datap[0]=ct as i64; lens[0]=xl_strlen(ct)
793 names[1]="_rels/.rels" as *u8 as i64; datap[1]=rels as i64; lens[1]=xl_strlen(rels)
794 names[2]="xl/workbook.xml" as *u8 as i64; datap[2]=wb as i64; lens[2]=xl_strlen(wb)
795 names[3]="xl/_rels/workbook.xml.rels" as *u8 as i64; datap[3]=wbr as i64; lens[3]=xl_strlen(wbr)
796 names[4]="xl/worksheets/sheet1.xml" as *u8 as i64; datap[4]=sheet as i64; lens[4]=slen
797 let sty: *u8=xl_styles_dxf()
798 names[5]="xl/styles.xml" as *u8 as i64; datap[5]=sty as i64; lens[5]=xl_strlen(sty)
799 return opc_write(outpath, names, datap, lens, 6)
800}
801
802// load xl/sharedStrings.xml (foreign Excel/Sheets files use t="s" cells indexing this table): each <si>'s
803// FIRST <t> text (entity-unescaped, NUL-terminated) into sbuf, offsets into soffs; returns the count.
804// Our own fromtsv files have no such part -> count 0, zero behavior change. (Rich <r>-run <si>s: first <t> only.)
805func xl_shared_load(path: *u8, sbuf: *u8, soffs: *i64, maxn: i64) -> i64 {
806 let sx: *u8=sys_mmap(XF_MAGIC_4194320)
807 let n: i64=opc_read_part(path, "xl/sharedStrings.xml" as *u8, sx, XF_MAGIC_4194304)
808 if n<0 { return 0 }
809 var cnt: i64=0; var i: i64=0; var o: i64=0
810 while i<n {
811 if xl_match(sx,i,n,"<si>" as *u8)==1 {
812 if cnt>=maxn { i=n } else {
813 soffs[cnt]=o
814 var k: i64=i+4
815 var havet: i64=0
816 while k<n { if xl_match(sx,k,n,"</si>" as *u8)==1 { break } if xl_match(sx,k,n,"<t" as *u8)==1 { havet=1; break } k=k+1 }
817 if havet==1 {
818 var selfc: i64=0
819 while k<n { if sx[k]==(47 as u8) { if sx[k+1]==(62 as u8) { selfc=1 } } if sx[k]==(62 as u8) { k=k+1; break } k=k+1 }
820 if selfc==0 {
821 while k<n {
822 if xl_match(sx,k,n,"</t>" as *u8)==1 { break }
823 if sx[k]==(38 as u8) {
824 if xl_match(sx,k,n,"&" as *u8)==1 { sbuf[o]=38 as u8; o=o+1; k=k+5 } else {
825 if xl_match(sx,k,n,"<" as *u8)==1 { sbuf[o]=60 as u8; o=o+1; k=k+4 } else {
826 if xl_match(sx,k,n,">" as *u8)==1 { sbuf[o]=62 as u8; o=o+1; k=k+4 } else {
827 if xl_match(sx,k,n,""" as *u8)==1 { sbuf[o]=34 as u8; o=o+1; k=k+6 } else {
828 if xl_match(sx,k,n,"'" as *u8)==1 { sbuf[o]=39 as u8; o=o+1; k=k+6 } else {
829 sbuf[o]=sx[k]; o=o+1; k=k+1 } } } } }
830 } else { sbuf[o]=sx[k]; o=o+1; k=k+1 }
831 }
832 }
833 }
834 sbuf[o]=0 as u8; o=o+1
835 cnt=cnt+1
836 i=i+4
837 }
838 } else { i=i+1 }
839 }
840 return cnt
841}
842
843// read sheet1 grid back to TSV into out; return length (or <0). inlineStr + numeric <v> + SHARED (t="s",
844// resolved through xl_shared_load) all yield TEXT.
845func xlsx_read_tsv(path: *u8, out: *u8, cap: i64) -> i64 {
846 let sh: *u8=sys_mmap(XF_MAGIC_8388624)
847 let n: i64=opc_read_part(path, "xl/worksheets/sheet1.xml" as *u8, sh, XF_MAGIC_8388608)
848 if n<0 { return n }
849 let sstr: *u8=sys_mmap(XF_MAGIC_1048592)
850 let soffs: *i64=sys_mmap(8*XF_MAGIC_16384) as *i64
851 let scount: i64=xl_shared_load(path, sstr, soffs, XF_MAGIC_16384)
852 var o: i64=0; var i: i64=0; var firstrow: i64=1; var cellcol: i64=0
853 while i<n {
854 if xl_match(sh,i,n,"</row>" as *u8)==1 { i=i+6 } else {
855 if xl_match(sh,i,n,"<row" as *u8)==1 {
856 if firstrow==0 { out[o]=10 as u8; o=o+1 }
857 firstrow=0; cellcol=0
858 while i<n { if sh[i]==(62 as u8) { i=i+1; break } i=i+1 }
859 } else {
860 if xl_match(sh,i,n,"<c" as *u8)==1 {
861 let c2: i64=sh[i+2] as i64
862 var iscell: i64=0
863 if c2==32 { iscell=1 } else { if c2==62 { iscell=1 } else { if c2==47 { iscell=1 } } }
864 if iscell==1 {
865 if cellcol>0 { out[o]=9 as u8; o=o+1 }
866 cellcol=cellcol+1
867 var isStr: i64=0; var isShared: i64=0; var selfclose: i64=0; var j: i64=i+2
868 while j<n { if sh[j]==(62 as u8) { break } if xl_match(sh,j,n,"t=\"inlineStr\"" as *u8)==1 { isStr=1 } if xl_match(sh,j,n,"t=\"s\"" as *u8)==1 { isShared=1 } if sh[j]==(47 as u8) { if sh[j+1]==(62 as u8) { selfclose=1 } } j=j+1 }
869 if selfclose==1 { i=j+1 } else {
870 if isStr==1 {
871 var k: i64=j
872 while k<n { if xl_match(sh,k,n,"<t" as *u8)==1 { break } k=k+1 }
873 while k<n { if sh[k]==(62 as u8) { k=k+1; break } k=k+1 } // past <t...>
874 while k<n {
875 if xl_match(sh,k,n,"</t>" as *u8)==1 { break }
876 if sh[k]==(38 as u8) {
877 if xl_match(sh,k,n,"&" as *u8)==1 { out[o]=38 as u8; o=o+1; k=k+5 } else {
878 if xl_match(sh,k,n,"<" as *u8)==1 { out[o]=60 as u8; o=o+1; k=k+4 } else {
879 if xl_match(sh,k,n,">" as *u8)==1 { out[o]=62 as u8; o=o+1; k=k+4 } else {
880 if xl_match(sh,k,n,""" as *u8)==1 { out[o]=34 as u8; o=o+1; k=k+6 } else {
881 if xl_match(sh,k,n,"'" as *u8)==1 { out[o]=39 as u8; o=o+1; k=k+6 } else {
882 out[o]=sh[k]; o=o+1; k=k+1 } } } } }
883 } else { out[o]=sh[k]; o=o+1; k=k+1 }
884 }
885 while k<n { if xl_match(sh,k,n,"</c>" as *u8)==1 { break } k=k+1 }
886 i=k+4
887 } else {
888 if isShared==1 {
889 // t="s": <v> holds an INDEX into the sharedStrings table -> emit the resolved text
890 var k: i64=j
891 while k<n { if xl_match(sh,k,n,"<v>" as *u8)==1 { break } k=k+1 }
892 k=k+3
893 var idxv: i64=0
894 while k<n { if xl_match(sh,k,n,"</v>" as *u8)==1 { break } let dc: i64=sh[k] as i64; if dc>=48 { if dc<=57 { idxv=idxv*10+(dc-48) } } k=k+1 }
895 if idxv<scount { o=xl_cat(out, o, ((sstr as i64) + soffs[idxv]) as *u8) }
896 while k<n { if xl_match(sh,k,n,"</c>" as *u8)==1 { break } k=k+1 }
897 i=k+4
898 } else {
899 var k: i64=j
900 while k<n { if xl_match(sh,k,n,"<v>" as *u8)==1 { break } k=k+1 }
901 k=k+3
902 while k<n { if xl_match(sh,k,n,"</v>" as *u8)==1 { break } out[o]=sh[k]; o=o+1; k=k+1 }
903 while k<n { if xl_match(sh,k,n,"</c>" as *u8)==1 { break } k=k+1 }
904 i=k+4
905 }
906 }
907 }
908 } else { i=i+1 }
909 } else { i=i+1 } } }
910 }
911 out[o]=0 as u8
912 return o
913}
914
915// emit one char HTML-escaped into dst
916func xl_esc1(dst: *u8, o: i64, c: i64) -> i64 {
917 if c==38 { return xl_cat(dst,o,"&" as *u8) }
918 if c==60 { return xl_cat(dst,o,"<" as *u8) }
919 if c==62 { return xl_cat(dst,o,">" as *u8) }
920 dst[o]=c as u8; return o+1
921}
922
923// one TSV row tsv[s,e) -> <tr> with TAB-split th/td cells
924func xl_rowhtml(out: *u8, off: i64, tsv: *u8, s: i64, e: i64, ishdr: i64) -> i64 {
925 var o: i64=off
926 o=xl_cat(out,o,"<tr>" as *u8)
927 var p: i64=s
928 var cs: i64=s
929 while p<=e {
930 var attab: i64=0
931 if p==e { attab=1 } else { if tsv[p]==(9 as u8) { attab=1 } }
932 if attab==1 {
933 if ishdr==1 { o=xl_cat(out,o,"<th>" as *u8) } else { o=xl_cat(out,o,"<td>" as *u8) }
934 var q: i64=cs
935 while q<p { o=xl_esc1(out,o,tsv[q] as i64); q=q+1 }
936 if ishdr==1 { o=xl_cat(out,o,"</th>" as *u8) } else { o=xl_cat(out,o,"</td>" as *u8) }
937 cs=p+1
938 }
939 p=p+1
940 }
941 o=xl_cat(out,o,"</tr>\n" as *u8)
942 return o
943}
944
945// CHARTS v1 (OF-S4, native-first): render a 0-JS BAR CHART from the sheet's data -- labels = column A,
946// values = the first column whose data cells are >=60% numeric (formula cells count: read resolves them to
947// computed values first). Bar widths are integer percents of the max (style attr, no JS). Sheets with no
948// numeric column get no chart section (graceful). In-FILE DrawingML c:chart parts (Excel-native charts) are
949// the interop second half -- a NAMED follow-up, not claimed.
950func xl_chart_html(out: *u8, off: i64, tsv: *u8, tn: i64) -> i64 {
951 var o: i64=off
952 // collect row spans (up to 25: header + 24 bars)
953 let rs: *i64=sys_mmap(8*64) as *i64
954 let re: *i64=sys_mmap(8*64) as *i64
955 var nr: i64=0
956 var i: i64=0
957 while i<tn {
958 var e: i64=i
959 while e<tn { if tsv[e]==(10 as u8) { break } e=e+1 }
960 if e>i { if nr<40 { rs[nr]=i; re[nr]=e; nr=nr+1 } }
961 i=e+1
962 }
963 if nr<2 { return o }
964 // find the value column: first col c>=1 whose data cells are >=60% numeric (and >0 numeric)
965 var vc: i64=0-1
966 var c: i64=1
967 while c<16 {
968 if vc>=0 { c=16 } else {
969 var num: i64=0
970 var tot: i64=0
971 var r: i64=1
972 while r<nr {
973 // cell (r, c)
974 var p: i64=rs[r]
975 var cc: i64=0
976 var cs2: i64=p
977 var fend: i64=0-1
978 var fstart: i64=0-1
979 while p<=re[r] {
980 var sep: i64=0
981 if p==re[r] { sep=1 } else { if tsv[p]==(9 as u8) { sep=1 } }
982 if sep==1 { if cc==c { fstart=cs2; fend=p; p=re[r]+1 } else { cc=cc+1; cs2=p+1 } }
983 p=p+1
984 }
985 if fstart>=0 { if fend>fstart {
986 tot=tot+1
987 if xl_is_numeric(tsv, fstart, fend-fstart)==1 { num=num+1 }
988 } }
989 r=r+1
990 }
991 if num>0 { if num*10 >= tot*6 { vc=c } }
992 if vc<0 { c=c+1 }
993 }
994 }
995 if vc<0 { return o }
996 // max value (Q4)
997 var mx: i64=0
998 var r2: i64=1
999 while r2<nr {
1000 var p2: i64=rs[r2]
1001 var cc2: i64=0
1002 var cs3: i64=p2
1003 while p2<=re[r2] {
1004 var sep2: i64=0
1005 if p2==re[r2] { sep2=1 } else { if tsv[p2]==(9 as u8) { sep2=1 } }
1006 if sep2==1 {
1007 if cc2==vc { if p2>cs3 { if xl_is_numeric(tsv,cs3,p2-cs3)==1 { let v: i64=xf_q4_of(tsv,cs3,p2-cs3); if v>mx { mx=v } } } }
1008 cc2=cc2+1
1009 cs3=p2+1
1010 }
1011 p2=p2+1
1012 }
1013 r2=r2+1
1014 }
1015 if mx<=0 { return o }
1016 o=xl_cat(out,o,"<div class=chart><p class=ct>Chart — first numeric column, bars sized to the max value</p>\n" as *u8)
1017 var r3: i64=1
1018 var bars: i64=0
1019 while r3<nr {
1020 if bars<24 {
1021 // label = col 0, value = col vc
1022 var p3: i64=rs[r3]
1023 var cc3: i64=0
1024 var cs4: i64=p3
1025 var lab_s: i64=0-1
1026 var lab_e: i64=0-1
1027 var val_s: i64=0-1
1028 var val_e: i64=0-1
1029 while p3<=re[r3] {
1030 var sep3: i64=0
1031 if p3==re[r3] { sep3=1 } else { if tsv[p3]==(9 as u8) { sep3=1 } }
1032 if sep3==1 {
1033 if cc3==0 { lab_s=cs4; lab_e=p3 }
1034 if cc3==vc { val_s=cs4; val_e=p3 }
1035 cc3=cc3+1
1036 cs4=p3+1
1037 }
1038 p3=p3+1
1039 }
1040 if val_s>=0 { if val_e>val_s { if xl_is_numeric(tsv,val_s,val_e-val_s)==1 {
1041 let v2: i64=xf_q4_of(tsv,val_s,val_e-val_s)
1042 if v2>=0 {
1043 var pct: i64=(v2*100)/mx
1044 if pct<1 { pct=1 }
1045 o=xl_cat(out,o,"<div class=brow><span class=bl>" as *u8)
1046 var q2: i64=lab_s
1047 while q2<lab_e { o=xl_esc1(out,o,tsv[q2] as i64); q2=q2+1 }
1048 o=xl_cat(out,o,"</span><div class=bar style=\"width:" as *u8)
1049 o=xl_uint(out,o,pct)
1050 o=xl_cat(out,o,"%\">" as *u8)
1051 var q3: i64=val_s
1052 while q3<val_e { o=xl_esc1(out,o,tsv[q3] as i64); q3=q3+1 }
1053 o=xl_cat(out,o,"</div></div>\n" as *u8)
1054 bars=bars+1
1055 }
1056 } } }
1057 }
1058 r3=r3+1
1059 }
1060 o=xl_cat(out,o,"</div>\n" as *u8)
1061 return o
1062}
1063
1064// shared TSV -> standard 0-JS preview page (auto-chart + table). label = the small caption over the data.
1065// Used by BOTH the sheet preview (xlsx_to_html) and the PIVOT view (rule 15 DRY).
1066func xl_tsv_to_html(tsv: *u8, tn: i64, out: *u8, dlurl: *u8, label: *u8) -> i64 {
1067 var o: i64=0
1068 o=xl_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}header h1{margin:0;font-size:17px;font-weight:600}\n.doc{max-width:960px;margin:22px auto;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:24px;box-shadow:0 1px 4px rgba(15,23,42,.08);overflow-x:auto}\n.sheet{font-size:13px;color:#64748b;margin:0 0 10px}\ntable{border-collapse:collapse;width:100%;font-size:14px}\nth,td{border:1px solid #cbd5e1;padding:7px 12px;text-align:left}\nth{background:#0f172a;color:#fff;position:sticky;top:0}\ntr:nth-child(even) td{background:#f8fafc}\n.chart{margin:0 0 18px}.ct{font-size:13px;color:#64748b;margin:0 0 8px}\n.brow{display:flex;align-items:center;margin:4px 0}.bl{width:220px;font-size:13px;color:#334155;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex-shrink:0}\n.bar{background:#2563eb;color:#fff;font-size:12px;border-radius:4px;padding:3px 8px;min-width:14px}\n.dlbar{max-width:960px;margin:0 auto 6px;text-align:right}.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:960px;margin:14px auto 46px;color:#64748b;font-size:13px;text-align:center}\n</style></head>\n<body>\n<header><h1>📊 Document Preview — Nishi Office</h1></header>\n<div class=doc>\n<p class=sheet>" as *u8)
1069 o=xl_cat(out,o,label)
1070 o=xl_cat(out,o,"</p>\n" as *u8)
1071 o=xl_chart_html(out, o, tsv, tn)
1072 o=xl_cat(out,o,"<table>\n" as *u8)
1073 var i: i64=0; var first: i64=1
1074 while i<tn {
1075 var e: i64=i
1076 while e<tn { if tsv[e]==(10 as u8) { break } e=e+1 }
1077 if e>i {
1078 if first==1 { o=xl_rowhtml(out,o,tsv,i,e,1); first=0 } else { o=xl_rowhtml(out,o,tsv,i,e,0) }
1079 }
1080 i=e+1
1081 }
1082 o=xl_cat(out,o,"</table>\n</div>\n" as *u8)
1083 if (dlurl as i64)!=0 { o=xl_cat(out,o,"<div class=dlbar><a class=dl href=\"" as *u8); o=xl_cat(out,o,dlurl); o=xl_cat(out,o,"\">⬇ Download .xlsx (opens in Excel / Sheets / Numbers)</a></div>\n" as *u8) }
1084 o=xl_cat(out,o,"<footer>Rendered natively from SpreadsheetML by <b>nx_xlsx</b> · viewable inline in the Nishi browser · your workbooks stay on your hardware</footer>\n</body></html>\n" as *u8)
1085 return o
1086}
1087
1088// .xlsx -> the standard 0-JS inline preview (sheet1 as a table; first row = header). Composes
1089// xlsx_read_tsv, so inlineStr + numeric + SHARED strings all render as text. Same iface as nx_docx/nx_md/
1090// nx_csv/nx_pdf so the nx_doc_view registry (and thus the Nishi browser) routes .xlsx with zero new code.
1091func xlsx_to_html(path: *u8, out: *u8, dlurl: *u8) -> i64 {
1092 let tsv: *u8=sys_mmap(XF_MAGIC_8388624)
1093 let tn: i64=xlsx_read_tsv(path, tsv, XF_MAGIC_8388608)
1094 if tn<0 { return tn }
1095 return xl_tsv_to_html(tsv, tn, out, dlurl, "Sheet1" as *u8)
1096}
1097
1098// ---- PIVOT v1 (OF-S5, native-first): GROUP-BY aggregation over the resolved sheet (formula cells count at
1099// their computed values). Groups by column G (letter), aggregates column V: SUM / COUNT / AVG (Q4; AVG floors)
1100// + a TOTAL row. Data rows = 2..N (row 1 = header); a row participates when its group cell is non-empty AND
1101// its value cell is numeric. HONEST scope: single group column, three aggregates; in-FILE pivotTable parts +
1102// drag-drop pivot UI = named follow-ups. Intended for TRANSACTIONAL sheets (summary rows form literal groups).
1103func xl_colletter(s: *u8) -> i64 {
1104 var col: i64=0
1105 var i: i64=0
1106 while s[i]!=(0 as u8) {
1107 let c: i64=s[i] as i64
1108 if c>=65 { if c<=90 { col=col*26+(c-64); i=i+1 } else { return 0-1 } } else { return 0-1 }
1109 if i>2 { return 0-1 }
1110 }
1111 if col<1 { return 0-1 }
1112 return col-1
1113}
1114
1115// pivot the TSV: group col gc, value col vc (0-based) -> pivot TSV in pout. Returns length (or <0: -1 = no
1116// participating rows).
1117func xl_pivot(tsv: *u8, tn: i64, gc: i64, vc: i64, pout: *u8) -> i64 {
1118 // group registry (cap 128): span into tsv + sum/count
1119 let gso: *i64=sys_mmap(8*128) as *i64
1120 let gsl: *i64=sys_mmap(8*128) as *i64
1121 let gsum: *i64=sys_mmap(8*128) as *i64
1122 let gcnt: *i64=sys_mmap(8*128) as *i64
1123 var ng: i64=0
1124 var row: i64=0
1125 var i: i64=0
1126 var total: i64=0
1127 var totcnt: i64=0
1128 while i<tn {
1129 var e: i64=i
1130 while e<tn { if tsv[e]==(10 as u8) { break } e=e+1 }
1131 if e>i {
1132 row=row+1
1133 if row>=2 {
1134 // locate group + value cells
1135 var p: i64=i
1136 var cc: i64=0
1137 var cs: i64=i
1138 var g_s: i64=0-1
1139 var g_e: i64=0-1
1140 var v_s: i64=0-1
1141 var v_e: i64=0-1
1142 while p<=e {
1143 var sep: i64=0
1144 if p==e { sep=1 } else { if tsv[p]==(9 as u8) { sep=1 } }
1145 if sep==1 {
1146 if cc==gc { g_s=cs; g_e=p }
1147 if cc==vc { v_s=cs; v_e=p }
1148 cc=cc+1
1149 cs=p+1
1150 }
1151 p=p+1
1152 }
1153 if g_s>=0 { if g_e>g_s { if v_s>=0 { if v_e>v_s { if xl_is_numeric(tsv,v_s,v_e-v_s)==1 {
1154 let v: i64=xf_q4_of(tsv,v_s,v_e-v_s)
1155 // find/insert group
1156 var gi: i64=0-1
1157 var k: i64=0
1158 while k<ng {
1159 if gsl[k]==g_e-g_s {
1160 var same: i64=1
1161 var q: i64=0
1162 while q<gsl[k] { if tsv[gso[k]+q]!=tsv[g_s+q] { same=0; q=gsl[k] } else { q=q+1 } }
1163 if same==1 { gi=k; k=ng }
1164 }
1165 if gi<0 { k=k+1 }
1166 }
1167 if gi<0 { if ng<128 { gso[ng]=g_s; gsl[ng]=g_e-g_s; gsum[ng]=0; gcnt[ng]=0; gi=ng; ng=ng+1 } }
1168 if gi>=0 {
1169 gsum[gi]=gsum[gi]+v
1170 gcnt[gi]=gcnt[gi]+1
1171 total=total+v
1172 totcnt=totcnt+1
1173 }
1174 } } } } }
1175 }
1176 }
1177 i=e+1
1178 }
1179 if totcnt==0 { return 0-1 }
1180 var o: i64=0
1181 o=xl_cat(pout,o,"Group\x09Sum\x09Count\x09Avg\n" as *u8)
1182 var g2: i64=0
1183 while g2<ng {
1184 var q2: i64=0
1185 while q2<gsl[g2] { pout[o]=tsv[gso[g2]+q2]; o=o+1; q2=q2+1 }
1186 pout[o]=9 as u8
1187 o=o+1
1188 o=xf_fmtq4(pout,o,gsum[g2])
1189 pout[o]=9 as u8
1190 o=o+1
1191 o=xl_uint(pout,o,gcnt[g2])
1192 pout[o]=9 as u8
1193 o=o+1
1194 o=xf_fmtq4(pout,o,gsum[g2]/gcnt[g2])
1195 pout[o]=10 as u8
1196 o=o+1
1197 g2=g2+1
1198 }
1199 o=xl_cat(pout,o,"TOTAL\x09" as *u8)
1200 o=xf_fmtq4(pout,o,total)
1201 pout[o]=9 as u8
1202 o=o+1
1203 o=xl_uint(pout,o,totcnt)
1204 pout[o]=9 as u8
1205 o=o+1
1206 o=xf_fmtq4(pout,o,total/totcnt)
1207 pout[o]=10 as u8
1208 o=o+1
1209 return o
1210}
1211
1212func main(argc: i64, argv: *i64) -> i64 {
1213 if argc<3 { xl_puts("usage: nx_xlsx fromtsv <in.tsv> <out.xlsx> | read <in.xlsx> | html <in.xlsx> <out.html> [download_url]\n" as *u8); sys_exit(2); return 2 }
1214 let cmd: *u8=argv[1] as *u8
1215 if xl_streq(cmd,"fromtsv" as *u8)==1 {
1216 if argc<4 { xl_puts("usage: nx_xlsx fromtsv <in.tsv> <out.xlsx>\n" as *u8); sys_exit(2); return 2 }
1217 let sz: i64=xlsx_write_tsv(argv[2] as *u8, argv[3] as *u8)
1218 if sz<0 { xl_puts("XLSX-WRITE-FAIL code=" as *u8); xl_num(sz); xl_puts("\n" as *u8); sys_exit(1); return 1 }
1219 xl_puts("XLSX-WRITE-OK path=" as *u8); xl_puts(argv[3] as *u8); xl_puts(" bytes=" as *u8); xl_num(sz); xl_puts("\n" as *u8)
1220 sys_exit(0); return 0
1221 }
1222 if xl_streq(cmd,"read" as *u8)==1 {
1223 let out: *u8=sys_mmap(XF_MAGIC_8388624)
1224 let tl: i64=xlsx_read_tsv(argv[2] as *u8, out, XF_MAGIC_8388608)
1225 if tl<0 { xl_puts("XLSX-READ-FAIL code=" as *u8); xl_num(tl); xl_puts("\n" as *u8); sys_exit(1); return 1 }
1226 xl_puts("XLSX-GRID-BEGIN\n" as *u8); sys_write(1, out, tl); xl_puts("\nXLSX-GRID-END\n" as *u8)
1227 sys_exit(0); return 0
1228 }
1229 if xl_streq(cmd,"html" as *u8)==1 {
1230 if argc<4 { xl_puts("usage: nx_xlsx html <in.xlsx> <out.html> [download_url]\n" as *u8); sys_exit(2); return 2 }
1231 var dlu: *u8=0 as *u8
1232 if argc>4 { dlu=argv[4] as *u8 }
1233 let out: *u8=sys_mmap(XF_MAGIC_33554432)
1234 let hn: i64=xlsx_to_html(argv[2] as *u8, out, dlu)
1235 if hn<0 { xl_puts("XLSX-HTML-FAIL code=" as *u8); xl_num(hn); xl_puts("\n" as *u8); sys_exit(1); return 1 }
1236 let fd: i64=sys_openat_wr(argv[3] as *u8, 0x1a4)
1237 if fd<0 { xl_puts("XLSX-HTML-FAIL cannot-write\n" as *u8); sys_exit(1); return 1 }
1238 sys_write(fd, out, hn); sys_close(fd)
1239 xl_puts("XLSX-HTML-OK path=" as *u8); xl_puts(argv[3] as *u8); xl_puts(" bytes=" as *u8); xl_num(hn); xl_puts("\n" as *u8)
1240 sys_exit(0); return 0
1241 }
1242 if xl_streq(cmd,"pivot" as *u8)==1 {
1243 if argc<5 { xl_puts("usage: nx_xlsx pivot <in.xlsx> <groupcol> <valuecol> (columns as letters, e.g. B C)\n" as *u8); sys_exit(2); return 2 }
1244 let gc: i64=xl_colletter(argv[3] as *u8)
1245 let vc: i64=xl_colletter(argv[4] as *u8)
1246 if gc<0 { xl_puts("XLSX-PIVOT-FAIL bad-group-column\n" as *u8); sys_exit(1); return 1 }
1247 if vc<0 { xl_puts("XLSX-PIVOT-FAIL bad-value-column\n" as *u8); sys_exit(1); return 1 }
1248 let tsv: *u8=sys_mmap(XF_MAGIC_8388624)
1249 let tn: i64=xlsx_read_tsv(argv[2] as *u8, tsv, XF_MAGIC_8388608)
1250 if tn<0 { xl_puts("XLSX-PIVOT-FAIL cannot-read code=" as *u8); xl_num(tn); xl_puts("\n" as *u8); sys_exit(1); return 1 }
1251 let pv: *u8=sys_mmap(XF_MAGIC_1048576)
1252 let pn: i64=xl_pivot(tsv, tn, gc, vc, pv)
1253 if pn<0 { xl_puts("XLSX-PIVOT-FAIL no-numeric-rows\n" as *u8); sys_exit(1); return 1 }
1254 xl_puts("XLSX-PIVOT-BEGIN\n" as *u8)
1255 sys_write(1, pv, pn)
1256 xl_puts("XLSX-PIVOT-END\n" as *u8)
1257 sys_exit(0); return 0
1258 }
1259 if xl_streq(cmd,"pivothtml" as *u8)==1 {
1260 if argc<6 { xl_puts("usage: nx_xlsx pivothtml <in.xlsx> <groupcol> <valuecol> <out.html> [download_url]\n" as *u8); sys_exit(2); return 2 }
1261 let gc: i64=xl_colletter(argv[3] as *u8)
1262 let vc: i64=xl_colletter(argv[4] as *u8)
1263 if gc<0 { xl_puts("XLSX-PIVOT-FAIL bad-group-column\n" as *u8); sys_exit(1); return 1 }
1264 if vc<0 { xl_puts("XLSX-PIVOT-FAIL bad-value-column\n" as *u8); sys_exit(1); return 1 }
1265 let tsv: *u8=sys_mmap(XF_MAGIC_8388624)
1266 let tn: i64=xlsx_read_tsv(argv[2] as *u8, tsv, XF_MAGIC_8388608)
1267 if tn<0 { xl_puts("XLSX-PIVOT-FAIL cannot-read code=" as *u8); xl_num(tn); xl_puts("\n" as *u8); sys_exit(1); return 1 }
1268 let pv: *u8=sys_mmap(XF_MAGIC_1048576)
1269 let pn: i64=xl_pivot(tsv, tn, gc, vc, pv)
1270 if pn<0 { xl_puts("XLSX-PIVOT-FAIL no-numeric-rows\n" as *u8); sys_exit(1); return 1 }
1271 var dlu: *u8=0 as *u8
1272 if argc>6 { dlu=argv[6] as *u8 }
1273 let out: *u8=sys_mmap(XF_MAGIC_33554432)
1274 let hn: i64=xl_tsv_to_html(pv, pn, out, dlu, "Pivot (group-by with SUM / COUNT / AVG; chart = Sum per group)" as *u8)
1275 let fd: i64=sys_openat_wr(argv[5] as *u8, 0x1a4)
1276 if fd<0 { xl_puts("XLSX-PIVOT-FAIL cannot-write\n" as *u8); sys_exit(1); return 1 }
1277 sys_write(fd, out, hn); sys_close(fd)
1278 xl_puts("XLSX-PIVOT-HTML-OK path=" as *u8); xl_puts(argv[5] as *u8); xl_puts(" bytes=" as *u8); xl_num(hn); xl_puts("\n" as *u8)
1279 sys_exit(0); return 0
1280 }
1281 xl_puts("nx_xlsx: unknown command (fromtsv|read|html|pivot|pivothtml)\n" as *u8)
1282 sys_exit(2); return 2
1283}