From 5ef49e2b96f6e1a9b43b5aafeb1e5a09d7e4e7b4 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Fri, 31 May 2024 03:16:53 -0400 Subject: [PATCH] XLML Streaming Write - CSV Export only quote leading ID (fixes #2959) --- bits/10_ssf.js | 1 + bits/38_xlstypes.js | 4 +- bits/75_xlml.js | 9 +- bits/78_writebiff.js | 14 +-- bits/90_utils.js | 6 +- bits/97_node.js | 158 +++++++++++++++++++++++++++++++- packages/ssf/bits/40_general.js | 1 + packages/ssf/ssf.flow.js | 1 + packages/ssf/ssf.js | 1 + packages/ssf/test/oddities.js | 8 +- 10 files changed, 184 insertions(+), 19 deletions(-) diff --git a/bits/10_ssf.js b/bits/10_ssf.js index f016a65..f644151 100644 --- a/bits/10_ssf.js +++ b/bits/10_ssf.js @@ -243,6 +243,7 @@ function SSF_large_exp(v/*:number*/)/*:string*/ { } function SSF_general_num(v/*:number*/)/*:string*/ { + if(!isFinite(v)) return isNaN(v) ? "#VALUE!" : "#DIV/0!"; var V = Math.floor(Math.log(Math.abs(v))*Math.LOG10E), o; if(V >= -4 && V <= -1) o = v.toPrecision(10+V); diff --git a/bits/38_xlstypes.js b/bits/38_xlstypes.js index 4b9a685..28a3e11 100644 --- a/bits/38_xlstypes.js +++ b/bits/38_xlstypes.js @@ -123,8 +123,8 @@ function parse_TypedPropertyValue(blob, type/*:number*/, _opts)/*:any*/ { case 0x03 /*VT_I4*/: ret = blob.read_shift(4, 'i'); return ret; case 0x0B /*VT_BOOL*/: return blob.read_shift(4) !== 0x0; case 0x13 /*VT_UI4*/: ret = blob.read_shift(4); return ret; - case 0x1E /*VT_LPSTR*/: return parse_lpstr(blob, t, 4).replace(chr0,''); - case 0x1F /*VT_LPWSTR*/: return parse_lpwstr(blob); + case 0x1E /*VT_LPSTR*/: blob.l += 4; val = parse_VtString(blob, blob[blob.l-4]).replace(/(^|[^\u0000])\u0000+$/,"$1"); break; + case 0x1F /*VT_LPWSTR*/: blob.l += 4; val = parse_VtString(blob, blob[blob.l-4]).replace(/(^|[^\u0000])\u0000+$/,"$1"); break; case 0x40 /*VT_FILETIME*/: return parse_FILETIME(blob); case 0x41 /*VT_BLOB*/: return parse_BLOB(blob); case 0x47 /*VT_CF*/: return parse_ClipboardData(blob); diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 32d2e08..eae5c1a 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -1186,9 +1186,12 @@ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbo o.push(writextag("Column",null,k)); }); var dense = ws["!data"] != null; + var addr = {r:0,c:0}; for(var R = range.s.r; R <= range.e.r; ++R) { var row = [write_ws_xlml_row(R, (ws['!rows']||[])[R])]; + addr.r = R; for(var C = range.s.c; C <= range.e.c; ++C) { + addr.c = C; var skip = false; for(mi = 0; mi != marr.length; ++mi) { if(marr[mi].s.c > C) continue; @@ -1199,7 +1202,6 @@ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbo break; } if(skip) continue; - var addr = {r:R,c:C}; var ref = encode_col(C) + encode_row(R), cell = dense ? (ws["!data"][R]||[])[C] : ws[ref]; row.push(write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)); } @@ -1223,7 +1225,7 @@ function write_ws_xlml(idx/*:number*/, opts, wb/*:Workbook*/)/*:string*/ { /* WorksheetOptions */ o.push(write_ws_xlml_wsopts(ws, opts, idx, wb)); - if(ws["!autofilter"]) o.push(''); + if(ws && ws["!autofilter"]) o.push(''); return o.join(""); } @@ -1242,11 +1244,10 @@ function write_xlml(wb, opts)/*:string*/ { d.push(write_props_xlml(wb, opts)); d.push(write_wb_xlml(wb, opts)); d.push(""); - d.push(""); + d.push(write_names_xlml(wb, opts)); for(var i = 0; i < wb.SheetNames.length; ++i) d.push(writextag("Worksheet", write_ws_xlml(i, opts, wb), {"ss:Name":escapexml(wb.SheetNames[i])})); d[2] = write_sty_xlml(wb, opts); - d[3] = write_names_xlml(wb, opts); return XML_HEADER + writextag("Workbook", d.join(""), { 'xmlns': XLMLNS.ss, 'xmlns:o': XLMLNS.o, diff --git a/bits/78_writebiff.js b/bits/78_writebiff.js index 6fc87a2..785a8f5 100644 --- a/bits/78_writebiff.js +++ b/bits/78_writebiff.js @@ -105,7 +105,7 @@ function write_ws_biff2(ba/*:BufArray*/, ws/*:Worksheet*/, idx/*:number*/, opts, if(range.e.c > 0xFF || range.e.r > 0x3FFF) { if(opts.WTF) throw new Error("Range " + (ws['!ref'] || "A1") + " exceeds format limit A1:IV16384"); range.e.c = Math.min(range.e.c, 0xFF); - range.e.r = Math.min(range.e.c, 0x3FFF); + range.e.r = Math.min(range.e.r, 0x3FFF); } var date1904 = (((wb||{}).Workbook||{}).WBProps||{}).date1904; var row = [], comments = []; @@ -515,9 +515,9 @@ function write_ws_biff8(idx/*:number*/, opts, wb/*:Workbook*/) { var range = safe_decode_range(ws['!ref'] || "A1"); var MAX_ROWS = b8 ? 65536 : 16384; if(range.e.c > 0xFF || range.e.r >= MAX_ROWS) { - if(opts.WTF) throw new Error("Range " + (ws['!ref'] || "A1") + " exceeds format limit A1:IV16384"); + if(opts.WTF) throw new Error("Range " + (ws['!ref'] || "A1") + " exceeds format limit A1:IV" + MAX_ROWS); range.e.c = Math.min(range.e.c, 0xFF); - range.e.r = Math.min(range.e.c, MAX_ROWS-1); + range.e.r = Math.min(range.e.r, MAX_ROWS-1); } write_biff_rec(ba, 0x0809, write_BOF(wb, 0x10, opts)); @@ -552,12 +552,11 @@ function write_ws_biff8(idx/*:number*/, opts, wb/*:Workbook*/) { if(dense) row = ws["!data"][R] || []; rr = encode_row(R); for(C = range.s.c; C <= range.e.c; ++C) { - ref = cols[C] + rr; - var cell = dense ? row[C] : ws[ref]; + var cell = dense ? row[C] : ws[cols[C] + rr]; if(!cell) continue; /* write cell */ write_ws_biff8_cell(ba, cell, R, C, opts, date1904); - if(b8 && cell.l) ws['!links'].push([ref, cell.l]); + if(b8 && cell.l) ws['!links'].push([cols[C] + rr, cell.l]); if(cell.c) comments.push([cell.c, R, C]); } } @@ -701,6 +700,9 @@ function write_biff_buf(wb/*:Workbook*/, opts/*:WriteOpts*/) { if(range.e.c > 255) { // note: 255 is IV if(typeof console != "undefined" && console.error) console.error("Worksheet '" + wb.SheetNames[i] + "' extends beyond column IV (255). Data may be lost."); } + if(range.e.r > 65535) { + if(typeof console != "undefined" && console.error) console.error("Worksheet '" + wb.SheetNames[i] + "' extends beyond row 65536. Data may be lost."); + } } var o = opts || {}; diff --git a/bits/90_utils.js b/bits/90_utils.js index 3429131..faa6c35 100644 --- a/bits/90_utils.js +++ b/bits/90_utils.js @@ -104,7 +104,7 @@ function sheet_to_json(sheet/*:Worksheet*/, opts/*:?Sheet2JSONOpts*/) { } var qreg = /"/g; -function make_csv_row(sheet/*:Worksheet*/, r/*:Range*/, R/*:number*/, cols/*:Array*/, fs/*:number*/, rs/*:number*/, FS/*:string*/, o/*:Sheet2CSVOpts*/)/*:?string*/ { +function make_csv_row(sheet/*:Worksheet*/, r/*:Range*/, R/*:number*/, cols/*:Array*/, fs/*:number*/, rs/*:number*/, FS/*:string*/, w/*:number*/, o/*:Sheet2CSVOpts*/)/*:?string*/ { var isempty = true; var row/*:Array*/ = [], txt = "", rr = encode_row(R); var dense = sheet["!data"] != null; @@ -117,7 +117,7 @@ function make_csv_row(sheet/*:Worksheet*/, r/*:Range*/, R/*:number*/, cols/*:Arr isempty = false; txt = ''+(o.rawNumbers && val.t == "n" ? val.v : format_cell(val, null, o)); for(var i = 0, cc = 0; i !== txt.length; ++i) if((cc = txt.charCodeAt(i)) === fs || cc === rs || cc === 34 || o.forceQuotes) {txt = "\"" + txt.replace(qreg, '""') + "\""; break; } - if(txt == "ID") txt = '"ID"'; + if(txt == "ID" && w == 0 && row.length == 0) txt = '"ID"'; } else if(val.f != null && !val.F) { isempty = false; txt = '=' + val.f; if(txt.indexOf(",") >= 0) txt = '"' + txt.replace(qreg, '""') + '"'; @@ -144,7 +144,7 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/)/*:string*/ { var w = 0; for(var R = r.s.r; R <= r.e.r; ++R) { if ((rowinfo[R]||{}).hidden) continue; - row = make_csv_row(sheet, r, R, cols, fs, rs, FS, o); + row = make_csv_row(sheet, r, R, cols, fs, rs, FS, w, o); if(row == null) { continue; } if(row || (o.blankrows !== false)) out.push((w++ ? RS : "") + row); } diff --git a/bits/97_node.js b/bits/97_node.js index d2eb614..e912d27 100644 --- a/bits/97_node.js +++ b/bits/97_node.js @@ -19,7 +19,7 @@ function write_csv_stream(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { while(R <= r.e.r) { ++R; if ((rowinfo[R-1]||{}).hidden) continue; - row = make_csv_row(sheet, r, R-1, cols, fs, rs, FS, o); + row = make_csv_row(sheet, r, R-1, cols, fs, rs, FS, w, o); if(row != null) { if(row || (o.blankrows !== false)) return stream.push((w++ ? RS : "") + row); } @@ -116,9 +116,165 @@ function write_json_stream(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { return stream; } +function write_xlml_stream(wb/*:Workbook*/, o/*:?Sheet2XLMLOpts*/) { + var stream = _Readable(); + var opts = o == null ? {} : o; + if(!wb.SSF) wb.SSF = dup(table_fmt); + if(wb.SSF) { + make_ssf(); SSF_load_table(wb.SSF); + // $FlowIgnore + opts.revssf = evert_num(wb.SSF); opts.revssf[wb.SSF[65535]] = 0; + opts.ssf = wb.SSF; + opts.cellXfs = []; + get_cell_style(opts.cellXfs, {}, {revssf:{"General":0}}); + } + + /* do one pass to determine styles since they must be added before tables */ + wb.SheetNames.forEach(function(n) { + var ws = wb.Sheets[n]; + if(!ws || !ws["!ref"]) return; + var range = decode_range(ws["!ref"]); + var dense = ws["!data"] != null; + var ddata = dense ? ws["!data"] : []; + var addr = {r:0,c:0}; + for(var R = range.s.r; R <= range.e.r; ++R) { + addr.r = R; + if(dense && !ddata[R]) continue; + for(var C = range.s.c; C <= range.e.c; ++C) { + addr.c = C; + var cell = dense ? ddata[R][C] : ws[encode_col(C) + encode_row(R)]; + if(!cell) continue; + if(cell.t == "d" && cell.z == null) { cell = dup(cell); cell.z = table_fmt[14]; } + void get_cell_style(opts.cellXfs, cell, opts); + } + } + }); + var sty = write_sty_xlml(wb, opts); + + var stage = 0, wsidx = 0, ws = wb.Sheets[wb.SheetNames[wsidx]], range = safe_decode_range(ws), R = -1, T = false; + + var marr = [], mi = 0, dense = false, darr = [], addr = {r:0,c:0}; + + stream._read = function() { switch(stage) { + /* header */ + case 0: { + stage = 1; + stream.push(XML_HEADER); + stream.push(""); + } break; + + /* preamble */ + case 1: { + stage = 2; + stream.push(write_props_xlml(wb, opts)); + stream.push(write_wb_xlml(wb, opts)); + } break; + + /* style and name tables */ + case 2: { + stage = 3; + stream.push(sty); + stream.push(write_names_xlml(wb, opts)); + } break; + + /* worksheet preamble */ + case 3: { + T = false; + if(wsidx >= wb.SheetNames.length) { stage = -1; stream.push(""); break; } + + stream.push(""); + + ws = wb.Sheets[wb.SheetNames[wsidx]]; + if(!ws) { stream.push(""); return void ++wsidx; } + + var names = write_ws_xlml_names(ws, opts, wsidx, wb); + if(names.length) stream.push("" + names + ""); + + if(!ws["!ref"]) return (stage = 5); + range = safe_decode_range(ws["!ref"]); + R = range.s.r; + stage = 4; + } break; + + /* worksheet intramble */ + case 4: { + if(R < 0 || R > range.e.r) { stream.push(T ? "" : ""); return void (stage = 5); } + + if(R <= range.s.r) { + if(ws['!cols']) ws['!cols'].forEach(function(n, i) { + process_col(n); + var w = !!n.width; + var p = col_obj_w(i, n); + var k/*:any*/ = {"ss:Index":i+1}; + if(w) k['ss:Width'] = width2px(p.width); + if(n.hidden) k['ss:Hidden']="1"; + if(!T) { T = true; stream.push(""); } + stream.push(writextag("Column",null,k)); + }); + dense = ws["!data"] != null; + if(dense) darr = ws["!data"]; + addr.r = addr.c = 0; + } + + /* process 10 rows per invocation */ + for(var cnt = 0; R <= range.e.r && cnt < 10; ++R, ++cnt) { + var row = [write_ws_xlml_row(R, (ws['!rows']||[])[R])]; + addr.r = R; + if(!(dense && !darr[R])) for(var C = range.s.c; C <= range.e.c; ++C) { + addr.c = C; + var skip = false; + for(mi = 0; mi != marr.length; ++mi) { + if(marr[mi].s.c > C) continue; + if(marr[mi].s.r > R) continue; + if(marr[mi].e.c < C) continue; + if(marr[mi].e.r < R) continue; + if(marr[mi].s.c != C || marr[mi].s.r != R) skip = true; + break; + } + if(skip) continue; + var ref = encode_col(C) + encode_row(R), cell = dense ? darr[R][C] : ws[ref]; + row.push(write_ws_xlml_cell(cell, ref, ws, opts, wsidx, wb, addr)); + } + row.push(""); + if(row.length > 2) { + if(!T) { T = true; stream.push("
"); } + stream.push(row.join("")); + } + } + } break; + + /* worksheet postamble */ + case 5: { + stream.push(write_ws_xlml_wsopts(ws, opts, wsidx, wb)); + if(ws && ws["!autofilter"]) stream.push(''); + stream.push(""); + wsidx++; R = -1; + return void (stage = 3); + } + + /* footer */ + case -1: { + stage = -2; + stream.push(""); + } break; + + /* exeunt */ + case -2: stream.push(null); break; + }}; + return stream; +} + var __stream = { to_json: write_json_stream, to_html: write_html_stream, to_csv: write_csv_stream, + to_xlml: write_xlml_stream, set_readable: set_readable }; diff --git a/packages/ssf/bits/40_general.js b/packages/ssf/bits/40_general.js index bcdc562..342909a 100644 --- a/packages/ssf/bits/40_general.js +++ b/packages/ssf/bits/40_general.js @@ -29,6 +29,7 @@ var general_fmt_num = (function make_general_fmt_num() { } function general_fmt_num_base(v/*:number*/)/*:string*/ { + if(!isFinite(v)) return isNaN(v) ? "#VALUE!" : "#DIV/0!"; var V = Math.floor(Math.log(Math.abs(v))*Math.LOG10E), o; if(V >= -4 && V <= -1) o = v.toPrecision(10+V); diff --git a/packages/ssf/ssf.flow.js b/packages/ssf/ssf.flow.js index 50f8cc6..c3aab5c 100644 --- a/packages/ssf/ssf.flow.js +++ b/packages/ssf/ssf.flow.js @@ -254,6 +254,7 @@ var general_fmt_num = (function make_general_fmt_num() { } function general_fmt_num_base(v/*:number*/)/*:string*/ { + if(!isFinite(v)) return isNaN(v) ? "#VALUE!" : "#DIV/0!"; var V = Math.floor(Math.log(Math.abs(v))*Math.LOG10E), o; if(V >= -4 && V <= -1) o = v.toPrecision(10+V); diff --git a/packages/ssf/ssf.js b/packages/ssf/ssf.js index 7cd7109..a29395f 100644 --- a/packages/ssf/ssf.js +++ b/packages/ssf/ssf.js @@ -250,6 +250,7 @@ var general_fmt_num = (function make_general_fmt_num() { } function general_fmt_num_base(v) { + if(!isFinite(v)) return isNaN(v) ? "#VALUE!" : "#DIV/0!"; var V = Math.floor(Math.log(Math.abs(v))*Math.LOG10E), o; if(V >= -4 && V <= -1) o = v.toPrecision(10+V); diff --git a/packages/ssf/test/oddities.js b/packages/ssf/test/oddities.js index cbe8a07..f607b56 100644 --- a/packages/ssf/test/oddities.js +++ b/packages/ssf/test/oddities.js @@ -23,8 +23,10 @@ describe('oddities', function() { assert.equal(SSF.format('#,##0.0; (#,##0.0); "-"', NaN), " -"); assert.equal(SSF.format('#,##0.0; (#,##0.0); "-"', Infinity), " -"); assert.equal(SSF.format('#,##0.0; (#,##0.0); "-"', -Infinity), " -"); - assert.equal(SSF.format('0.00', NaN), "#VALUE!"); - assert.equal(SSF.format('0.00', Infinity), "#DIV/0!"); - assert.equal(SSF.format('0.00', -Infinity), "#DIV/0!"); + ["0.00", "General"].forEach(function(fmt) { + assert.equal(SSF.format(fmt, NaN), "#VALUE!"); + assert.equal(SSF.format(fmt, Infinity), "#DIV/0!"); + assert.equal(SSF.format(fmt, -Infinity), "#DIV/0!"); + }); }); });