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,"&amp;" as *u8) } else { if c==60 { oo=dx_cat(out,oo,"&lt;" as *u8) } else { if c==62 { oo=dx_cat(out,oo,"&gt;" 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}