code wiki / _hdl_build / nx_mailmerge.nx
nx_mailmerge.nx source
↩ module page · 124 lines · 6644 B
1// nx_mailmerge.nx -- SOVEREIGN mail merge for the Nishi Office suite (the lawyer workflow): one TEMPLATE .docx
2// carrying {{field}} placeholders + a tab-separated DATA source (header row of field names, then one record per
3// row) -> one personalized .docx per record. Composes directly on nx_docx (docx_read_part reads the template's
4// raw word/document.xml; docx_write_document_xml frames each merged result as a valid OPC package). Placeholder
5// values are XML-escaped, so '&'/'<' in data are safe. Unknown placeholders are left literally (never silently
6// dropped). Use case: generate per-heir bequest letters / probate notices from the estate inventory.
7// nx_mailmerge <template.docx> <data.tsv> <out_prefix>
8// -> writes <out_prefix>1.docx .. <out_prefix>N.docx (one per data row)
9// HONEST LIMITATION: substitutes at the document.xml byte level, so a placeholder must live within a single
10// <w:t> run (true for templates we author; a real Word template that splits {{x}} across runs is a follow-on).
11// license_tier: ORIGINAL
12import "nx_docx.nx"
13const K_MAGIC_1048592: i64 = 1048592
14const K_MAGIC_1048576: i64 = 1048576
15
16// two byte-slices equal?
17func mm_slice_eq(a: *u8, aoff: i64, alen: i64, b: *u8, boff: i64, blen: i64) -> i64 {
18 if alen!=blen { return 0 }
19 var k: i64=0; while k<alen { if a[aoff+k]!=b[boff+k] { return 0 } k=k+1 } return 1
20}
21// append decimal v to buf at off; return new off
22func mm_int_to(buf: *u8, off: i64, v: i64) -> i64 {
23 var m: i64=v; let t: *u8=sys_mmap(28); var k: i64=0
24 if m==0 { t[0]=48 as u8; k=1 }
25 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
26 var o: i64=off; var i: i64=0; while i<k { buf[o]=t[k-1-i]; o=o+1; i=i+1 } return o
27}
28// append src[soff..soff+slen) to out at o, XML-escaping & < > ; return new offset
29func mm_escape_region(out: *u8, o: i64, src: *u8, soff: i64, slen: i64) -> i64 {
30 var oo: i64=o; var k: i64=0
31 while k<slen {
32 let c: i64=src[soff+k] as i64
33 if c==38 { oo=dx_cat(out,oo,"&" as *u8) } else { if c==60 { oo=dx_cat(out,oo,"<" as *u8) } else { if c==62 { oo=dx_cat(out,oo,">" as *u8) } else { out[oo]=src[soff+k]; oo=oo+1 } } }
34 k=k+1
35 }
36 return oo
37}
38// parse one TSV line starting at `start`; fill off[]/len[] per field (split on TAB); strip trailing CR;
39// nextp[0] = start of next line; returns field count.
40func mm_parse_line(buf: *u8, start: i64, n: i64, off: *i64, len: *i64, nextp: *i64) -> i64 {
41 var lineend: i64=n; var i: i64=start
42 while i<n { if buf[i]==(10 as u8) { lineend=i; break } i=i+1 }
43 var nf: i64=0; var fs: i64=start; var j: i64=start
44 while j<=lineend {
45 var sep: i64=0
46 if j==lineend { sep=1 } else { if buf[j]==(9 as u8) { sep=1 } }
47 if sep==1 {
48 var fe: i64=j
49 if j==lineend { if fe>fs { if buf[fe-1]==(13 as u8) { fe=fe-1 } } } // strip trailing CR (CRLF)
50 off[nf]=fs; len[nf]=fe-fs; nf=nf+1
51 fs=j+1
52 }
53 j=j+1
54 }
55 nextp[0]=lineend+1
56 return nf
57}
58// substitute {{field}} placeholders in tmpl[0..tlen) using header slices (tsv,hoff,hlen,nfields) and row value
59// slices (tsv,voff,vlen,nvals). Writes to out; returns out length.
60func mm_merge_one(tmpl: *u8, tlen: i64, tsv: *u8, hoff: *i64, hlen: *i64, nfields: i64, voff: *i64, vlen: *i64, nvals: i64, out: *u8) -> i64 {
61 var o: i64=0; var i: i64=0
62 while i<tlen {
63 var handled: i64=0
64 if tmpl[i]==(123 as u8) { // '{'
65 if i+1<tlen { if tmpl[i+1]==(123 as u8) { // "{{"
66 var j: i64=i+2; var foundc: i64=0
67 while j+1<tlen { if tmpl[j]==(125 as u8) { if tmpl[j+1]==(125 as u8) { foundc=1; break } } j=j+1 }
68 if foundc==1 {
69 let fns: i64=i+2; let fnl: i64=j-(i+2)
70 var fi: i64=0-1; var k: i64=0
71 while k<nfields { if mm_slice_eq(tmpl, fns, fnl, tsv, hoff[k], hlen[k])==1 { fi=k; k=nfields } else { k=k+1 } }
72 if fi>=0 { if fi<nvals { o=mm_escape_region(out, o, tsv, voff[fi], vlen[fi]); handled=1 } }
73 if handled==0 { var c: i64=i; while c<j+2 { out[o]=tmpl[c]; o=o+1; c=c+1 } handled=1 } // unknown: keep literal
74 i=j+2
75 }
76 } } }
77 if handled==0 { out[o]=tmpl[i]; o=o+1; i=i+1 }
78 }
79 return o
80}
81
82func main(argc: i64, argv: *i64) -> i64 {
83 if argc<4 { dx_puts("usage: nx_mailmerge <template.docx> <data.tsv> <out_prefix>\n" as *u8); sys_exit(2); return 2 }
84 let template: *u8=argv[1] as *u8
85 let tsvpath: *u8=argv[2] as *u8
86 let outprefix: *u8=argv[3] as *u8
87
88 // template document.xml (raw)
89 let tmpl: *u8=sys_mmap(K_MAGIC_1048592)
90 let tlen: i64=docx_read_part(template, "word/document.xml" as *u8, tmpl, K_MAGIC_1048576)
91 if tlen<0 { dx_puts("MAILMERGE-FAIL: cannot read template document.xml code=" as *u8); dx_num(tlen); dx_puts("\n" as *u8); sys_exit(1); return 1 }
92
93 // data source
94 let szp: *i64=sys_mmap(16) as *i64
95 let tsv: *u8=sys_read_file(tsvpath, szp)
96 if (tsv as i64)==0 { dx_puts("MAILMERGE-FAIL: cannot read data tsv\n" as *u8); sys_exit(1); return 1 }
97 let tn: i64=szp[0]
98
99 let hoff: *i64=sys_mmap(8*64) as *i64; let hlen: *i64=sys_mmap(8*64) as *i64
100 let nextp: *i64=sys_mmap(16) as *i64
101 let nfields: i64=mm_parse_line(tsv, 0, tn, hoff, hlen, nextp)
102 if nfields<=0 { dx_puts("MAILMERGE-FAIL: empty header\n" as *u8); sys_exit(1); return 1 }
103
104 let voff: *i64=sys_mmap(8*64) as *i64; let vlen: *i64=sys_mmap(8*64) as *i64
105 let merged: *u8=sys_mmap(K_MAGIC_1048592)
106 var pos: i64=nextp[0]
107 var rownum: i64=0
108 while pos<tn {
109 let lstart: i64=pos
110 let nv: i64=mm_parse_line(tsv, pos, tn, voff, vlen, nextp)
111 pos=nextp[0]
112 // skip blank lines (a single zero-length field)
113 if nv<=1 { if vlen[0]==0 { continue } }
114 rownum=rownum+1
115 let mlen: i64=mm_merge_one(tmpl, tlen, tsv, hoff, hlen, nfields, voff, vlen, nv, merged)
116 let outpath: *u8=sys_mmap(512); var po: i64=0
117 po=dx_cat(outpath, po, outprefix); po=mm_int_to(outpath, po, rownum); po=dx_cat(outpath, po, ".docx" as *u8); outpath[po]=0 as u8
118 let wr: i64=docx_write_document_xml(outpath, merged, mlen)
119 dx_puts(" MERGE row=" as *u8); dx_num(rownum); dx_puts(" -> " as *u8); dx_puts(outpath)
120 if wr<0 { dx_puts(" WRITE-FAIL\n" as *u8) } else { dx_puts(" (" as *u8); dx_num(wr); dx_puts(" bytes)\n" as *u8) }
121 }
122 dx_puts("MAILMERGE-OK fields=" as *u8); dx_num(nfields); dx_puts(" documents=" as *u8); dx_num(rownum); dx_puts("\n" as *u8)
123 sys_exit(0); return 0
124}