From 5187bc0b63916e07cc6e8e2afb6408310dc0fd29 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Mon, 10 Apr 2017 01:10:54 -0400 Subject: [PATCH] Basic AutoFilter Read/Write - XLSX/XLSB read/write autofilter - XLS/XLML/*ODS read autofilter - Workbook Properties override via Props option - XLSB write sheet protection Issues: - fixes #472 h/t @nishthasb - fixes #478 h/t @yonatannn - see #623 h/t @jcarvin --- README.md | 18 +++ bits/33_coreprops.js | 21 ++- bits/34_extprops.js | 2 +- bits/36_xlsprops.js | 71 +++++---- bits/39_xlsbiff.js | 4 +- bits/65_fods.js | 6 + bits/67_wsxml.js | 95 +++++++++--- bits/68_wsbin.js | 52 ++++++- bits/72_wbxml.js | 23 +++ bits/75_xlml.js | 15 +- bits/76_xls.js | 11 +- bits/77_parsetab.js | 2 +- bits/80_parseods.js | 10 +- docbits/54_shobject.md | 8 ++ docbits/56_wbprops.md | 6 + docbits/81_writeopts.md | 3 + docbits/95_contrib.md | 1 + test.js | 63 ++++++++ tests/write.js | 2 +- xlsx.flow.js | 312 +++++++++++++++++++++++++++++++--------- xlsx.js | 312 +++++++++++++++++++++++++++++++--------- 21 files changed, 844 insertions(+), 193 deletions(-) diff --git a/README.md b/README.md index 5a61e9a..b5c39a5 100644 --- a/README.md +++ b/README.md @@ -598,6 +598,14 @@ In addition to the base sheet keys, worksheets also add: | `objects` | Edit objects | | `scenarios` | Edit scenarios | +- `ws['!autofilter']`: AutoFilter object following the schema: + +```typescript +type AutoFilter = { + ref:string; // A-1 based range representing the AutoFilter table range +} +``` + #### Chartsheet Object Chartsheets are represented as standard sheets. They are distinguished with the @@ -655,6 +663,12 @@ if(!wb.Custprops) wb.Custprops = {}; wb.Custprops["Custom Property"] = "Custom Value"; ``` +Writers will process the `Props` key of the options object: + +```js +/* force the Author to be "SheetJS" */ +XLSX.write(wb, {Props:{Author:"SheetJS"}}); +``` ### Document Features Even for basic features like date storage, the official Excel formats store the @@ -935,6 +949,7 @@ The exported `write` and `writeFile` functions accept an options argument: | bookType | `"xlsx"` | Type of Workbook (see below for supported formats) | | sheet | `""` | Name of Worksheet for single-sheet formats ** | | compression | `false` | Use ZIP compression for ZIP-based formats ** | +| Props | | Override workbook properties when writing ** | - `bookSST` is slower and more memory intensive, but has better compatibility with older versions of iOS Numbers @@ -943,6 +958,8 @@ The exported `write` and `writeFile` functions accept an options argument: - `cellDates` only applies to XLSX output and is not guaranteed to work with third-party readers. Excel itself does not usually write cells with type `d` so non-Excel tools may ignore the data or blow up in the presence of dates. +- `Props` is an object mirroring the workbook `Props` field. See the table from + the [Workbook File Properties](#workbook-file-properties) section. ### Supported Output Formats @@ -1450,6 +1467,7 @@ Latest test files snapshot: Download and unzip to the `test_files` subdirectory. + ## License Please consult the attached LICENSE file for details. All rights not explicitly diff --git a/bits/33_coreprops.js b/bits/33_coreprops.js index c2a0e71..69438fe 100644 --- a/bits/33_coreprops.js +++ b/bits/33_coreprops.js @@ -58,15 +58,24 @@ function cp_doit(f, g, h, o, p) { o[o.length] = (h ? writextag(f,g,h) : writetag(f,g)); } -function write_core_props(cp, opts) { +function write_core_props(cp, _opts) { + var opts = _opts || {}; var o = [XML_HEADER, CORE_PROPS_XML_ROOT], p = {}; - if(!cp) return o.join(""); + if(!cp && !opts.Props) return o.join(""); + if(cp) { + if(cp.CreatedDate != null) cp_doit("dcterms:created", typeof cp.CreatedDate === "string" ? cp.CreatedDate : write_w3cdtf(cp.CreatedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); + if(cp.ModifiedDate != null) cp_doit("dcterms:modified", typeof cp.ModifiedDate === "string" ? cp.ModifiedDate : write_w3cdtf(cp.ModifiedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); + } - if(cp.CreatedDate != null) cp_doit("dcterms:created", typeof cp.CreatedDate === "string" ? cp.CreatedDate : write_w3cdtf(cp.CreatedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); - if(cp.ModifiedDate != null) cp_doit("dcterms:modified", typeof cp.ModifiedDate === "string" ? cp.ModifiedDate : write_w3cdtf(cp.ModifiedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); - - for(var i = 0; i != CORE_PROPS.length; ++i) { var f = CORE_PROPS[i]; cp_doit(f[0], cp[f[1]], null, o, p); } + for(var i = 0; i != CORE_PROPS.length; ++i) { + var f = CORE_PROPS[i]; + var v = opts.Props && opts.Props[f[1]] != null ? opts.Props[f[1]] : cp ? cp[f[1]] : null; + if(v === true) v = "1"; + else if(v === false) v = "0"; + else if(typeof v == "number") v = String(v); + if(v != null) cp_doit(f[0], v, null, o, p); + } if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } diff --git a/bits/34_extprops.js b/bits/34_extprops.js index 5aea48c..a31fa79 100644 --- a/bits/34_extprops.js +++ b/bits/34_extprops.js @@ -89,7 +89,7 @@ function write_ext_props(cp, opts)/*:string*/ { if(cp[f[1]] === undefined) return; var v; switch(f[2]) { - case 'string': v = cp[f[1]]; break; + case 'string': v = String(cp[f[1]]); break; case 'bool': v = cp[f[1]] ? 'true' : 'false'; break; } if(v !== undefined) o[o.length] = (W(f[0], v)); diff --git a/bits/36_xlsprops.js b/bits/36_xlsprops.js index dfb8e5f..451715b 100644 --- a/bits/36_xlsprops.js +++ b/bits/36_xlsprops.js @@ -1,50 +1,61 @@ +/* Common Name -> XLML Name */ +var XLMLDocPropsMap = { + Category: 'Category', + ContentStatus: 'ContentStatus', /* NOTE: missing from schema */ + Keywords: 'Keywords', + LastAuthor: 'LastAuthor', + LastPrinted: 'LastPrinted', + RevNumber: 'Revision', + Author: 'Author', + Comments: 'Description', + Identifier: 'Identifier', /* NOTE: missing from schema */ + Language: 'Language', /* NOTE: missing from schema */ + Subject: 'Subject', + Title: 'Title', + CreatedDate: 'Created', + ModifiedDate: 'LastSaved', + + Application: 'AppName', + AppVersion: 'Version', + TotalTime: 'TotalTime', + Manager: 'Manager', + Company: 'Company' +}; +var evert_XLMLDPM = evert(XLMLDocPropsMap); + function xlml_set_prop(Props, tag/*:string*/, val) { - /* TODO: Normalize the properties */ - switch(tag) { - case 'Description': tag = 'Comments'; break; - case 'Created': tag = 'CreatedDate'; break; - case 'LastSaved': tag = 'ModifiedDate'; break; - } + tag = evert_XLMLDPM[tag] || tag; Props[tag] = val; } -var XLMLDocumentProperties = [ - ['Title', 'Title'], - ['Subject', 'Subject'], - ['Author', 'Author'], - ['Keywords', 'Keywords'], - ['Comments', 'Description'], - ['LastAuthor', 'LastAuthor'], - ['CreatedDate', 'Created', 'date'], - ['ModifiedDate', 'LastSaved', 'date'], - ['Category', 'Category'], - ['Manager', 'Manager'], - ['Company', 'Company'], - ['AppVersion', 'Version'] -]; /* TODO: verify */ -function xlml_write_docprops(Props) { - var T = 'DocumentProperties'; +function xlml_write_docprops(Props, opts) { var o = []; - XLMLDocumentProperties.forEach(function(p) { - if(!Props[p[0]]) return; - var m = Props[p[0]]; + CORE_PROPS.concat(EXT_PROPS).forEach(function(p) { + if(Props[p[1]] == null) return; + var m = opts && opts.Props && opts.Props[p[1]] != null ? opts.Props[p[1]] : Props[p[1]]; switch(p[2]) { case 'date': m = new Date(m).toISOString(); break; } - o.push(writetag(p[1], m)); + if(typeof m == 'number') m = String(m); + else if(m === true || m === false) { t = "boolean"; m = m ? "1" : "0"; } + else if(m instanceof Date) m = new Date(m).toISOString(); + o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); }); - return '<' + T + ' xmlns="' + XLMLNS.o + '">' + o.join("") + ''; + return writextag('DocumentProperties', o.join(""), {xmlns:XLMLNS.o }); } -function xlml_write_custprops(Props, Custprops) { +function xlml_write_custprops(Props, Custprops, opts) { + var BLACKLIST = ["Worksheets","SheetNames"]; var T = 'CustomDocumentProperties'; var o = []; if(Props) keys(Props).forEach(function(k) { /*:: if(!Props) return; */ if(!Props.hasOwnProperty(k)) return; - for(var i = 0; i < XLMLDocumentProperties.length; ++i) - if(k == XLMLDocumentProperties[i][0]) return; + for(var i = 0; i < CORE_PROPS.length; ++i) if(k == CORE_PROPS[i][1]) return; + for(i = 0; i < EXT_PROPS.length; ++i) if(k == EXT_PROPS[i][1]) return; + for(i = 0; i < BLACKLIST.length; ++i) if(k == BLACKLIST[i]) return; + var m = Props[k]; var t = "string"; if(typeof m == 'number') { t = "float"; m = String(m); } diff --git a/bits/39_xlsbiff.js b/bits/39_xlsbiff.js index fa81378..7c5fe8c 100644 --- a/bits/39_xlsbiff.js +++ b/bits/39_xlsbiff.js @@ -452,9 +452,10 @@ function parse_Lbl(blob, length, opts) { var chKey = blob.read_shift(1); var cch = blob.read_shift(1); var cce = blob.read_shift(opts && opts.biff == 2 ? 1 : 2); + var itab = 0; if(!opts || opts.biff >= 5) { blob.l += 2; - var itab = blob.read_shift(2); + itab = blob.read_shift(2); blob.l += 4; } var name = parse_XLUnicodeStringNoCch(blob, cch, opts); @@ -463,6 +464,7 @@ function parse_Lbl(blob, length, opts) { return { chKey: chKey, Name: name, + itab: itab, rgce: rgce }; } diff --git a/bits/65_fods.js b/bits/65_fods.js index ec9bdf1..f01889a 100644 --- a/bits/65_fods.js +++ b/bits/65_fods.js @@ -19,3 +19,9 @@ function csf_to_ods_formula(f/*:string*/)/*:string*/ { /* TODO: something other than this */ return o.replace(/;/g, "|").replace(/,/g,";"); } + +function ods_to_csf_range_3D(r/*:string*/) { + var a = r.split(":"); + var s = a[0].split(".")[0]; + return [s, a[0].split(".")[1] + ":" + a[1].split(".")[1]]; +} diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 24fd5f5..37fba84 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -7,43 +7,54 @@ var sheetdataregex = /<(?:\w+:)?sheetData>([^\u2603]*)<\/(?:\w+:)?sheetData>/; var hlinkregex = /<(?:\w:)?hyperlink [^>]*>/mg; var dimregex = /"(\w*:\w*)"/; var colregex = /<(?:\w:)?col[^>]*[\/]?>/g; +var afregex = /<(?:\w:)?autoFilter[^>]*([\/]|>([^\u2603]*)<\/(?:\w:)?autoFilter)>/g; /* 18.3 Worksheets */ function parse_ws_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { if(!data) return data; if(DENSE != null && opts.dense == null) opts.dense = DENSE; + /* 18.3.1.99 worksheet CT_Worksheet */ var s = opts.dense ? ([]/*:any*/) : ({}/*:any*/); + var refguess/*:Range*/ = ({s: {r:2000000, c:2000000}, e: {r:0, c:0} }/*:any*/); + + var data1 = "", data2 = ""; + var mtch=data.match(sheetdataregex); + if(mtch) { + data1 = data.substr(0, mtch.index); + data2 = data.substr(mtch.index + mtch[0].length); + } else data1 = data2 = data; /* 18.3.1.35 dimension CT_SheetDimension ? */ // $FlowIgnore - var ridx = (data.match(/<(?:\w*:)?dimension/)||{index:-1}).index; + var ridx = (data1.match(/<(?:\w*:)?dimension/)||{index:-1}).index; if(ridx > 0) { - var ref = data.substr(ridx,50).match(dimregex); - if(ref != null) parse_ws_xml_dim(s, ref[1]); + var ref = data1.substr(ridx,50).match(dimregex); + if(ref) parse_ws_xml_dim(s, ref[1]); } - /* 18.3.1.55 mergeCells CT_MergeCells */ - var mergecells = []; - var merges = data.match(mergecregex); - if(merges) for(ridx = 0; ridx != merges.length; ++ridx) - mergecells[ridx] = safe_decode_range(merges[ridx].substr(merges[ridx].indexOf("\"")+1)); - /* 18.3.1.17 cols CT_Cols */ var columns = []; if(opts.cellStyles) { /* 18.3.1.13 col CT_Col */ - var cols = data.match(colregex); + var cols = data1.match(colregex); if(cols) parse_ws_xml_cols(columns, cols); } - var refguess/*:Range*/ = ({s: {r:2000000, c:2000000}, e: {r:0, c:0} }/*:any*/); - /* 18.3.1.80 sheetData CT_SheetData ? */ - var mtch=data.match(sheetdataregex); if(mtch) parse_ws_xml_data(mtch[1], s, opts, refguess, themes, styles); + /* 18.3.1.2 autoFilter CT_AutoFilter */ + var afilter = data2.match(afregex); + if(afilter) s['!autofilter'] = parse_ws_xml_autofilter(afilter[0]); + + /* 18.3.1.55 mergeCells CT_MergeCells */ + var mergecells = []; + var merges = data2.match(mergecregex); + if(merges) for(ridx = 0; ridx != merges.length; ++ridx) + mergecells[ridx] = safe_decode_range(merges[ridx].substr(merges[ridx].indexOf("\"")+1)); + /* 18.3.1.48 hyperlinks CT_Hyperlinks */ - var hlink = data.match(hlinkregex); + var hlink = data2.match(hlinkregex); if(hlink) parse_ws_xml_hlinks(s, hlink, rels); if(!s["!ref"] && refguess.e.c >= refguess.s.c && refguess.e.r >= refguess.s.r) s["!ref"] = encode_range(refguess); @@ -142,6 +153,14 @@ function write_ws_xml_cols(ws, cols)/*:string*/ { return o.join(""); } +function parse_ws_xml_autofilter(data) { + var o = { ref: (data.match(/ref="([^"]*)"/)||[])[1]}; + return o; +} +function write_ws_xml_autofilter(data)/*:string*/ { + return writextag("autoFilter", null, {ref:data.ref}); +} + function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; @@ -261,7 +280,6 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { p.F = arrayf[i][1]; } - /* SCHEMA IS ACTUALLY INCORRECT HERE. IF A CELL HAS NO T, EMIT "" */ if(tag.t === undefined && p.v === undefined) { if(!opts.sheetStubs) continue; p.t = "z"; @@ -293,7 +311,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { cref = d.match(isregex); p.t = 's'; if(cref != null && (sstr = parse_si(cref[1]))) p.v = sstr.t; else p.v = ""; - break; // inline string + break; case 'b': p.v = parsexmlbool(p.v); break; case 'd': if(!opts.cellDates) { p.v = datenum(parseDate(p.v)); p.t = 'n'; } @@ -361,19 +379,23 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { var o = [XML_HEADER, WS_XML_ROOT]; var s = wb.SheetNames[idx], sidx = 0, rdata = ""; var ws = wb.Sheets[s]; - if(ws === undefined) ws = {}; - var ref = ws['!ref']; if(ref === undefined) ref = 'A1'; + if(ws == null) ws = {}; + var ref = ws['!ref']; if(ref == null) ref = 'A1'; if(!rels) rels = {}; ws['!comments'] = []; ws['!drawing'] = []; o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])})); + o[o.length] = (writextag('dimension', null, {'ref': ref})); + /* sheetViews */ + /* TODO: store in WB, process styles */ if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' })); - if(ws['!cols'] !== undefined && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); + if(ws['!cols'] != null && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); + o[sidx = o.length] = ''; ws['!links'] = []; if(ws['!ref'] != null) { @@ -382,10 +404,25 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { } if(o.length>sidx+1) { o[o.length] = (''); o[sidx]=o[sidx].replace("/>",">"); } + /* sheetCalcPr */ + if(ws['!protect'] != null) o[o.length] = write_ws_xml_protection(ws['!protect']); + /* protectedRanges */ + /* scenarios */ + + if(ws['!autofilter'] != null) o[o.length] = write_ws_xml_autofilter(ws['!autofilter']); + + /* sortState */ + /* dataConsolidate */ + /* customSheetViews */ + if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); + /* phoneticPr */ + /* conditionalFormatting */ + /* dataValidations */ + var relc = -1, rel, rId = -1; if(ws['!links'].length > 0) { o[o.length] = ""; @@ -401,21 +438,41 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { } delete ws['!links']; + /* printOptions */ + /* pageMargins */ + /* pageSetup */ + var hfidx = o.length; o[o.length] = ""; + /* rowBreaks */ + /* colBreaks */ + /* customProperties */ + /* cellWatches */ + /* ignoredErrors */ + /* smartTags */ + if(ws['!drawing'].length > 0) { rId = add_rels(rels, -1, "../drawings/drawing" + (idx+1) + ".xml", RELS.DRAW); ws['!drawing'].rid = rId; o[o.length] = writextag("drawing", null, {"r:id":"rId" + rId}); } else delete ws['!drawing']; + if(ws['!comments'].length > 0) { rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); o[o.length] = writextag("legacyDrawing", null, {"r:id":"rId" + rId}); ws['!legacy'] = rId; } + /* drawingHF */ + /* picture */ + /* oleObjects */ + /* controls */ + /* webPublishItems */ + /* tableParts */ + /* extList */ + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index 64c9d13..108c18a 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -289,6 +289,35 @@ function write_BrtColInfo(C/*:number*/, col, o) { return o; } +/* [MS-XLSB] 2.4.740 BrtSheetProtection */ +function write_BrtSheetProtection(sp, o) { + if(o == null) o = new_buf(16*4+2); + o.write_shift(2, sp.password ? crypto_CreatePasswordVerifier_Method1(sp.password) : 0); + o.write_shift(4, 1); // this record should not be written if no protection + [ + ["objects", false], // fObjects + ["scenarios", false], // fScenarios + ["formatCells", true], // fFormatCells + ["formatColumns", true], // fFormatColumns + ["formatRows", true], // fFormatRows + ["insertColumns", true], // fInsertColumns + ["insertRows", true], // fInsertRows + ["insertHyperlinks", true], // fInsertHyperlinks + ["deleteColumns", true], // fDeleteColumns + ["deleteRows", true], // fDeleteRows + ["selectLockedCells", false], // fSelLockedCells + ["sort", true], // fSort + ["autoFilter", true], // fAutoFilter + ["pivotTables", true], // fPivotTables + ["selectUnlockedCells", false] // fSelUnlockedCells + ].forEach(function(n) { + o.write_shift(4, 1); + if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0); + else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1); + }); + return o; +} + /* [MS-XLSB] 2.1.7.61 Worksheet */ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { if(!data) return data; @@ -434,6 +463,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { } break; + case 0x00A1: /* 'BrtBeginAFilter' */ + s['!autofilter'] = { ref:encode_range(val) }; + break; + case 0x00AF: /* 'BrtAFilterDateGroupItem' */ case 0x0284: /* 'BrtActiveX' */ case 0x0271: /* 'BrtBigName' */ @@ -628,6 +661,21 @@ function write_LEGACYDRAWING(ba, ws/*:Worksheet*/, idx/*:number*/, rels) { } } +function write_AUTOFILTER(ba, ws) { + if(!ws['!autofilter']) return; + write_record(ba, "BrtBeginAFilter", write_UncheckedRfX(decode_range(ws['!autofilter'].ref))); + /* *FILTERCOLUMN */ + /* [SORTSTATE] */ + /* BrtEndAFilter */ + write_record(ba, "BrtEndAFilter"); +} + +function write_SHEETPROTECT(ba, ws) { + if(!ws['!protect']) return; + /* [BrtSheetProtectionIso] */ + write_record(ba, "BrtSheetProtection", write_BrtSheetProtection(ws['!protect'])); +} + function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { var ba = buf_array(); var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {}; @@ -643,10 +691,10 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { write_COLINFOS(ba, ws, idx, opts, wb); write_CELLTABLE(ba, ws, idx, opts, wb); /* [BrtSheetCalcProp] */ - /* [[BrtSheetProtectionIso] BrtSheetProtection] */ + write_SHEETPROTECT(ba, ws); /* *([BrtRangeProtectionIso] BrtRangeProtection) */ /* [SCENMAN] */ - /* [AUTOFILTER] */ + write_AUTOFILTER(ba, ws); /* [SORTSTATE] */ /* [DCON] */ /* [USERSHVIEWS] */ diff --git a/bits/72_wbxml.js b/bits/72_wbxml.js index f2ab333..a1469bd 100644 --- a/bits/72_wbxml.js +++ b/bits/72_wbxml.js @@ -159,7 +159,15 @@ function safe1904(wb/*:Workbook*/)/*:string*/ { function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { var o = [XML_HEADER]; o[o.length] = WB_XML_ROOT; + + /* fileVersion */ + /* fileSharing */ + o[o.length] = (writextag('workbookPr', null, {date1904:safe1904(wb), codeName:"ThisWorkbook"})); + + /* workbookProtection */ + /* bookViews */ + o[o.length] = ""; var sheets = wb.Workbook && wb.Workbook.Sheets || []; for(var i = 0; i != wb.SheetNames.length; ++i) { @@ -173,6 +181,21 @@ function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { o[o.length] = (writextag('sheet',null,sht)); } o[o.length] = ""; + + /* functionGroups */ + /* externalReferences */ + /* definedNames */ + /* calcPr */ + /* oleSize */ + /* customWorkbookViews */ + /* pivotCaches */ + /* smartTagPr */ + /* smartTagTypes */ + /* webPublishing */ + /* fileRecoveryPr */ + /* webPublishObjects */ + /* extLst */ + if(o.length>2){ o[o.length] = ''; o[1]=o[1].replace("/>",">"); } return o.join(""); } diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 2c1bdf4..0b6943c 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -351,6 +351,9 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'TotalTime': case 'HyperlinkBase': case 'Manager': + case 'ContentStatus': + case 'Identifier': + case 'Language': if(Rn[0].slice(-2) === "/>") break; else if(Rn[1]==="/") xlml_set_prop(Props, Rn[3], str.slice(pidx, Rn.index)); else pidx = Rn.index + Rn[0].length; @@ -375,6 +378,15 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { } break; + case 'AutoFilter': + if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw new Error("Bad state: "+tmp.join("|"));} + else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + var AutoFilter = xlml_parsexmltag(Rn[0]); + cursheet['!autofilter'] = { ref:rc_to_a1(AutoFilter.Range).replace(/\$/g,"") }; + state.push([Rn[3], true]); + } + break; + case 'Name': break; case 'ComponentOptions': @@ -388,7 +400,6 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'PageBreaks': case 'QueryTable': case 'DataValidation': - case 'AutoFilter': case 'Sorting': case 'Schema': case 'data': @@ -783,7 +794,7 @@ function parse_xlml(data, opts)/*:Workbook*/ { function write_props_xlml(wb, opts) { var o = []; /* DocumentProperties */ - if(wb.Props) o.push(xlml_write_docprops(wb.Props)); + if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); /* CustomDocumentProperties */ if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops)); return o.join(""); diff --git a/bits/76_xls.js b/bits/76_xls.js index 1f2b56b..ac45635 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -177,6 +177,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { var last_Rn = ''; var file_depth = 0; /* TODO: make a real stack */ var BIFF2Fmt = 0; + var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ /* explicit override for some broken writers */ opts.codepage = 1200; @@ -252,7 +253,14 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; case 'ExternName': supbooks[sbc][++sbci] = val; break; case 'Index': break; // TODO - case 'Lbl': supbooks[0][++sbcli] = val; break; + case 'Lbl': + supbooks[0][++sbcli] = val; // TODO: local formula storage in stringify_formula + if(!supbooks[val.itab]) supbooks[val.itab] = []; + supbooks[val.itab].push(val); + if(val.Name == "\r" && val.itab > 0) + if(val.rgce && val.rgce[0] && val.rgce[0][0] && val.rgce[0][0][0] == 'PtgArea3d') + FilterDatabases[val.itab - 1] = { ref: encode_range(val.rgce[0][0][1][2]) }; + break; case 'ExternSheet': supbooks[sbc] = supbooks[sbc].concat(val); sbci += val.length; break; case 'Protect': out["!protect"] = val; break; /* for sheet or book */ @@ -712,6 +720,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { wb.Directory=sheetnamesraw; wb.SheetNames=sheetnamesraw; if(!options.bookSheets) wb.Sheets=Sheets; + if(wb.Sheets) FilterDatabases.forEach(function(r,i) { wb.Sheets[wb.SheetNames[i]]['!autofilter'] = r; }); wb.Preamble=Preamble; wb.Strings = sst; wb.SSF = SSF.get_table(); diff --git a/bits/77_parsetab.js b/bits/77_parsetab.js index c410a58..ee01f39 100644 --- a/bits/77_parsetab.js +++ b/bits/77_parsetab.js @@ -94,7 +94,7 @@ var XLSBRecordEnum = { /*::[*/0x009E/*::]*/: { n:"BrtBookView", f:parsenoop }, /*::[*/0x009F/*::]*/: { n:"BrtBeginSst", f:parse_BrtBeginSst }, /*::[*/0x00A0/*::]*/: { n:"BrtEndSst", f:parsenoop }, - /*::[*/0x00A1/*::]*/: { n:"BrtBeginAFilter", f:parsenoop }, + /*::[*/0x00A1/*::]*/: { n:"BrtBeginAFilter", f:parse_UncheckedRfX }, /*::[*/0x00A2/*::]*/: { n:"BrtEndAFilter", f:parsenoop }, /*::[*/0x00A3/*::]*/: { n:"BrtBeginFilterColumn", f:parsenoop }, /*::[*/0x00A4/*::]*/: { n:"BrtEndFilterColumn", f:parsenoop }, diff --git a/bits/80_parseods.js b/bits/80_parseods.js index 50c29a6..3f52438 100644 --- a/bits/80_parseods.js +++ b/bits/80_parseods.js @@ -306,6 +306,15 @@ var parse_content_xml = (function() { if(Rn[1]==='/') textp = (textp.length > 0 ? textp + "\n" : "") + parse_text_p(str.slice(textpidx,Rn.index), textptag); else { textptag = parsexmltag(Rn[0], false); textpidx = Rn.index + Rn[0].length; } break; // + + case 'database-range': // 9.4.15 + if(Rn[1]==='/') break; + try { + var AutoFilter = ods_to_csf_range_3D(parsexmltag(Rn[0])['target-range-address']); + Sheets[AutoFilter[0]]['!autofilter'] = { ref: AutoFilter[1] }; + } catch(e) { } + break; + case 's': break; // case 'date': break; // <*:date> @@ -320,7 +329,6 @@ var parse_content_xml = (function() { case 'content-validation': break; // 9.4.5 - case 'database-range': break; // 9.4.15 case 'filter': break; // 9.5.2 case 'filter-and': break; // 9.5.3 case 'filter-or': break; // 9.5.4 diff --git a/docbits/54_shobject.md b/docbits/54_shobject.md index 9a8f467..a860e10 100644 --- a/docbits/54_shobject.md +++ b/docbits/54_shobject.md @@ -35,6 +35,14 @@ In addition to the base sheet keys, worksheets also add: | `objects` | Edit objects | | `scenarios` | Edit scenarios | +- `ws['!autofilter']`: AutoFilter object following the schema: + +```typescript +type AutoFilter = { + ref:string; // A-1 based range representing the AutoFilter table range +} +``` + #### Chartsheet Object Chartsheets are represented as standard sheets. They are distinguished with the diff --git a/docbits/56_wbprops.md b/docbits/56_wbprops.md index 50c31f9..9ffffb3 100644 --- a/docbits/56_wbprops.md +++ b/docbits/56_wbprops.md @@ -30,3 +30,9 @@ if(!wb.Custprops) wb.Custprops = {}; wb.Custprops["Custom Property"] = "Custom Value"; ``` +Writers will process the `Props` key of the options object: + +```js +/* force the Author to be "SheetJS" */ +XLSX.write(wb, {Props:{Author:"SheetJS"}}); +``` diff --git a/docbits/81_writeopts.md b/docbits/81_writeopts.md index 70cecb8..ffe65b5 100644 --- a/docbits/81_writeopts.md +++ b/docbits/81_writeopts.md @@ -10,6 +10,7 @@ The exported `write` and `writeFile` functions accept an options argument: | bookType | `"xlsx"` | Type of Workbook (see below for supported formats) | | sheet | `""` | Name of Worksheet for single-sheet formats ** | | compression | `false` | Use ZIP compression for ZIP-based formats ** | +| Props | | Override workbook properties when writing ** | - `bookSST` is slower and more memory intensive, but has better compatibility with older versions of iOS Numbers @@ -18,6 +19,8 @@ The exported `write` and `writeFile` functions accept an options argument: - `cellDates` only applies to XLSX output and is not guaranteed to work with third-party readers. Excel itself does not usually write cells with type `d` so non-Excel tools may ignore the data or blow up in the presence of dates. +- `Props` is an object mirroring the workbook `Props` field. See the table from + the [Workbook File Properties](#workbook-file-properties) section. ### Supported Output Formats diff --git a/docbits/95_contrib.md b/docbits/95_contrib.md index d253e6e..9e029c8 100644 --- a/docbits/95_contrib.md +++ b/docbits/95_contrib.md @@ -45,3 +45,4 @@ Latest test files snapshot: Download and unzip to the `test_files` subdirectory. + diff --git a/test.js b/test.js index f9cffb4..e749cf7 100644 --- a/test.js +++ b/test.js @@ -32,6 +32,11 @@ function fixjson(x) { return x.replace(/[\r\n]+$/,""); } var dir = "./test_files/"; var paths = { + afxls: dir + 'AutoFilter.xls', + afxml: dir + 'AutoFilter.xml', + afods: dir + 'AutoFilter.ods', + afxlsx: dir + 'AutoFilter.xlsx', + afxlsb: dir + 'AutoFilter.xlsb', cpxls: dir + 'custom_properties.xls', cpxml: dir + 'custom_properties.xls.xml', cpxlsx: dir + 'custom_properties.xlsx', @@ -888,6 +893,23 @@ describe('parse features', function() { }); }); }); + describe('auto filter', function() { + [ + ['xlsx', paths.afxlsx], + ['xlsb', paths.afxlsb], + ['xls', paths.afxls], + ['xlml', paths.afxml], + ['ods', paths.afods] + ].forEach(function(m) { it(m[0], function() { + var wb = X.readFile(m[1]); + assert(wb.Sheets[wb.SheetNames[0]]['!autofilter'] == null); + for(var i = 1; i < wb.SheetNames.length; ++i) { + assert(wb.Sheets[wb.SheetNames[i]]['!autofilter'] != null); + assert.equal(wb.Sheets[wb.SheetNames[i]]['!autofilter'].ref,"A1:E22"); + } + }); }); + }); + describe('should correctly handle styles', function() { var wsxls, wsxlsx, rn, rn2; var bef = (function() { @@ -966,6 +988,47 @@ describe('parse features', function() { }); }); +describe('write features', function() { + describe('props', function() { + describe('core', function() { + var ws, baseprops; + var bef = (function() { + X = require(modp); + ws = X.utils.aoa_to_sheet([["a","b","c"],[1,2,3]]); + baseprops = { + Category: "C4tegory", + ContentStatus: "C0ntentStatus", + Keywords: "K3ywords", + LastAuthor: "L4stAuthor", + LastPrinted: "L4stPrinted", + RevNumber: 6969, + AppVersion: 69, + Author: "4uth0r", + Comments: "C0mments", + Identifier: "1d", + Language: "L4nguage", + Subject: "Subj3ct", + Title: "T1tle" + } + }); + if(typeof before != 'undefined') before(bef); + else it('before', bef); + ['xlml', 'xlsx', 'xlsb'].forEach(function(w) { it(w, function() { + wb = { + Props: {}, + SheetNames: ["Sheet1"], + Sheets: {Sheet1: ws} + }; + Object.keys(baseprops).forEach(function(k) { wb.Props[k] = baseprops[k]; }); + var wb2 = X.read(X.write(wb, {bookType:w, type:"buffer"}), {type:"buffer"}); + Object.keys(baseprops).forEach(function(k) { assert.equal(baseprops[k], wb2.Props[k]); }); + var wb3 = X.read(X.write(wb2, {bookType:w, type:"buffer", Props: {Author:"SheetJS"}}), {type:"buffer"}); + assert.equal("SheetJS", wb3.Props.Author); + }); }); + }); + }); +}); + function seq(end, start) { var s = start || 0; var o = new Array(end - s); diff --git a/tests/write.js b/tests/write.js index 865b9df..ef5f9bf 100644 --- a/tests/write.js +++ b/tests/write.js @@ -41,7 +41,7 @@ if(typeof XLSX === "undefined") { try { XLSX = require('./'); } catch(e) { XLSX var wb = { SheetNames: [], Sheets: {} }; /* convert an array of arrays in JS to a CSF spreadsheet */ -var ws = XLSX.utils.aoa_to_sheet(data, {cellDates:true}); +var ws = XLSX.utils.aoa_to_sheet(data, {cellDates:true, dense:false}); /* TEST: add worksheet to workbook */ wb.SheetNames.push(ws_name); diff --git a/xlsx.flow.js b/xlsx.flow.js index 97f61ac..166ec95 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -3040,15 +3040,24 @@ function cp_doit(f, g, h, o, p) { o[o.length] = (h ? writextag(f,g,h) : writetag(f,g)); } -function write_core_props(cp, opts) { +function write_core_props(cp, _opts) { + var opts = _opts || {}; var o = [XML_HEADER, CORE_PROPS_XML_ROOT], p = {}; - if(!cp) return o.join(""); + if(!cp && !opts.Props) return o.join(""); + if(cp) { + if(cp.CreatedDate != null) cp_doit("dcterms:created", typeof cp.CreatedDate === "string" ? cp.CreatedDate : write_w3cdtf(cp.CreatedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); + if(cp.ModifiedDate != null) cp_doit("dcterms:modified", typeof cp.ModifiedDate === "string" ? cp.ModifiedDate : write_w3cdtf(cp.ModifiedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); + } - if(cp.CreatedDate != null) cp_doit("dcterms:created", typeof cp.CreatedDate === "string" ? cp.CreatedDate : write_w3cdtf(cp.CreatedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); - if(cp.ModifiedDate != null) cp_doit("dcterms:modified", typeof cp.ModifiedDate === "string" ? cp.ModifiedDate : write_w3cdtf(cp.ModifiedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); - - for(var i = 0; i != CORE_PROPS.length; ++i) { var f = CORE_PROPS[i]; cp_doit(f[0], cp[f[1]], null, o, p); } + for(var i = 0; i != CORE_PROPS.length; ++i) { + var f = CORE_PROPS[i]; + var v = opts.Props && opts.Props[f[1]] != null ? opts.Props[f[1]] : cp ? cp[f[1]] : null; + if(v === true) v = "1"; + else if(v === false) v = "0"; + else if(typeof v == "number") v = String(v); + if(v != null) cp_doit(f[0], v, null, o, p); + } if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -3143,7 +3152,7 @@ function write_ext_props(cp, opts)/*:string*/ { if(cp[f[1]] === undefined) return; var v; switch(f[2]) { - case 'string': v = cp[f[1]]; break; + case 'string': v = String(cp[f[1]]); break; case 'bool': v = cp[f[1]] ? 'true' : 'false'; break; } if(v !== undefined) o[o.length] = (W(f[0], v)); @@ -3223,53 +3232,64 @@ function write_cust_props(cp, opts)/*:string*/ { if(o.length>2){ o[o.length] = ''; o[1]=o[1].replace("/>",">"); } return o.join(""); } +/* Common Name -> XLML Name */ +var XLMLDocPropsMap = { + Category: 'Category', + ContentStatus: 'ContentStatus', /* NOTE: missing from schema */ + Keywords: 'Keywords', + LastAuthor: 'LastAuthor', + LastPrinted: 'LastPrinted', + RevNumber: 'Revision', + Author: 'Author', + Comments: 'Description', + Identifier: 'Identifier', /* NOTE: missing from schema */ + Language: 'Language', /* NOTE: missing from schema */ + Subject: 'Subject', + Title: 'Title', + CreatedDate: 'Created', + ModifiedDate: 'LastSaved', + + Application: 'AppName', + AppVersion: 'Version', + TotalTime: 'TotalTime', + Manager: 'Manager', + Company: 'Company' +}; +var evert_XLMLDPM = evert(XLMLDocPropsMap); + function xlml_set_prop(Props, tag/*:string*/, val) { - /* TODO: Normalize the properties */ - switch(tag) { - case 'Description': tag = 'Comments'; break; - case 'Created': tag = 'CreatedDate'; break; - case 'LastSaved': tag = 'ModifiedDate'; break; - } + tag = evert_XLMLDPM[tag] || tag; Props[tag] = val; } -var XLMLDocumentProperties = [ - ['Title', 'Title'], - ['Subject', 'Subject'], - ['Author', 'Author'], - ['Keywords', 'Keywords'], - ['Comments', 'Description'], - ['LastAuthor', 'LastAuthor'], - ['CreatedDate', 'Created', 'date'], - ['ModifiedDate', 'LastSaved', 'date'], - ['Category', 'Category'], - ['Manager', 'Manager'], - ['Company', 'Company'], - ['AppVersion', 'Version'] -]; /* TODO: verify */ -function xlml_write_docprops(Props) { - var T = 'DocumentProperties'; +function xlml_write_docprops(Props, opts) { var o = []; - XLMLDocumentProperties.forEach(function(p) { - if(!Props[p[0]]) return; - var m = Props[p[0]]; + CORE_PROPS.concat(EXT_PROPS).forEach(function(p) { + if(Props[p[1]] == null) return; + var m = opts && opts.Props && opts.Props[p[1]] != null ? opts.Props[p[1]] : Props[p[1]]; switch(p[2]) { case 'date': m = new Date(m).toISOString(); break; } - o.push(writetag(p[1], m)); + if(typeof m == 'number') m = String(m); + else if(m === true || m === false) { t = "boolean"; m = m ? "1" : "0"; } + else if(m instanceof Date) m = new Date(m).toISOString(); + o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); }); - return '<' + T + ' xmlns="' + XLMLNS.o + '">' + o.join("") + ''; + return writextag('DocumentProperties', o.join(""), {xmlns:XLMLNS.o }); } -function xlml_write_custprops(Props, Custprops) { +function xlml_write_custprops(Props, Custprops, opts) { + var BLACKLIST = ["Worksheets","SheetNames"]; var T = 'CustomDocumentProperties'; var o = []; if(Props) keys(Props).forEach(function(k) { /*:: if(!Props) return; */ if(!Props.hasOwnProperty(k)) return; - for(var i = 0; i < XLMLDocumentProperties.length; ++i) - if(k == XLMLDocumentProperties[i][0]) return; + for(var i = 0; i < CORE_PROPS.length; ++i) if(k == CORE_PROPS[i][1]) return; + for(i = 0; i < EXT_PROPS.length; ++i) if(k == EXT_PROPS[i][1]) return; + for(i = 0; i < BLACKLIST.length; ++i) if(k == BLACKLIST[i]) return; + var m = Props[k]; var t = "string"; if(typeof m == 'number') { t = "float"; m = String(m); } @@ -4190,9 +4210,10 @@ function parse_Lbl(blob, length, opts) { var chKey = blob.read_shift(1); var cch = blob.read_shift(1); var cce = blob.read_shift(opts && opts.biff == 2 ? 1 : 2); + var itab = 0; if(!opts || opts.biff >= 5) { blob.l += 2; - var itab = blob.read_shift(2); + itab = blob.read_shift(2); blob.l += 4; } var name = parse_XLUnicodeStringNoCch(blob, cch, opts); @@ -4201,6 +4222,7 @@ function parse_Lbl(blob, length, opts) { return { chKey: chKey, Name: name, + itab: itab, rgce: rgce }; } @@ -9604,6 +9626,12 @@ function csf_to_ods_formula(f/*:string*/)/*:string*/ { /* TODO: something other than this */ return o.replace(/;/g, "|").replace(/,/g,";"); } + +function ods_to_csf_range_3D(r/*:string*/) { + var a = r.split(":"); + var s = a[0].split(".")[0]; + return [s, a[0].split(".")[1] + ":" + a[1].split(".")[1]]; +} var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options @@ -9687,43 +9715,54 @@ var sheetdataregex = /<(?:\w+:)?sheetData>([^\u2603]*)<\/(?:\w+:)?sheetData>/; var hlinkregex = /<(?:\w:)?hyperlink [^>]*>/mg; var dimregex = /"(\w*:\w*)"/; var colregex = /<(?:\w:)?col[^>]*[\/]?>/g; +var afregex = /<(?:\w:)?autoFilter[^>]*([\/]|>([^\u2603]*)<\/(?:\w:)?autoFilter)>/g; /* 18.3 Worksheets */ function parse_ws_xml(data/*:?string*/, opts, rels, wb, themes, styles)/*:Worksheet*/ { if(!data) return data; if(DENSE != null && opts.dense == null) opts.dense = DENSE; + /* 18.3.1.99 worksheet CT_Worksheet */ var s = opts.dense ? ([]/*:any*/) : ({}/*:any*/); + var refguess/*:Range*/ = ({s: {r:2000000, c:2000000}, e: {r:0, c:0} }/*:any*/); + + var data1 = "", data2 = ""; + var mtch=data.match(sheetdataregex); + if(mtch) { + data1 = data.substr(0, mtch.index); + data2 = data.substr(mtch.index + mtch[0].length); + } else data1 = data2 = data; /* 18.3.1.35 dimension CT_SheetDimension ? */ // $FlowIgnore - var ridx = (data.match(/<(?:\w*:)?dimension/)||{index:-1}).index; + var ridx = (data1.match(/<(?:\w*:)?dimension/)||{index:-1}).index; if(ridx > 0) { - var ref = data.substr(ridx,50).match(dimregex); - if(ref != null) parse_ws_xml_dim(s, ref[1]); + var ref = data1.substr(ridx,50).match(dimregex); + if(ref) parse_ws_xml_dim(s, ref[1]); } - /* 18.3.1.55 mergeCells CT_MergeCells */ - var mergecells = []; - var merges = data.match(mergecregex); - if(merges) for(ridx = 0; ridx != merges.length; ++ridx) - mergecells[ridx] = safe_decode_range(merges[ridx].substr(merges[ridx].indexOf("\"")+1)); - /* 18.3.1.17 cols CT_Cols */ var columns = []; if(opts.cellStyles) { /* 18.3.1.13 col CT_Col */ - var cols = data.match(colregex); + var cols = data1.match(colregex); if(cols) parse_ws_xml_cols(columns, cols); } - var refguess/*:Range*/ = ({s: {r:2000000, c:2000000}, e: {r:0, c:0} }/*:any*/); - /* 18.3.1.80 sheetData CT_SheetData ? */ - var mtch=data.match(sheetdataregex); if(mtch) parse_ws_xml_data(mtch[1], s, opts, refguess, themes, styles); + /* 18.3.1.2 autoFilter CT_AutoFilter */ + var afilter = data2.match(afregex); + if(afilter) s['!autofilter'] = parse_ws_xml_autofilter(afilter[0]); + + /* 18.3.1.55 mergeCells CT_MergeCells */ + var mergecells = []; + var merges = data2.match(mergecregex); + if(merges) for(ridx = 0; ridx != merges.length; ++ridx) + mergecells[ridx] = safe_decode_range(merges[ridx].substr(merges[ridx].indexOf("\"")+1)); + /* 18.3.1.48 hyperlinks CT_Hyperlinks */ - var hlink = data.match(hlinkregex); + var hlink = data2.match(hlinkregex); if(hlink) parse_ws_xml_hlinks(s, hlink, rels); if(!s["!ref"] && refguess.e.c >= refguess.s.c && refguess.e.r >= refguess.s.r) s["!ref"] = encode_range(refguess); @@ -9822,6 +9861,14 @@ function write_ws_xml_cols(ws, cols)/*:string*/ { return o.join(""); } +function parse_ws_xml_autofilter(data) { + var o = { ref: (data.match(/ref="([^"]*)"/)||[])[1]}; + return o; +} +function write_ws_xml_autofilter(data)/*:string*/ { + return writextag("autoFilter", null, {ref:data.ref}); +} + function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; @@ -9941,7 +9988,6 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { p.F = arrayf[i][1]; } - /* SCHEMA IS ACTUALLY INCORRECT HERE. IF A CELL HAS NO T, EMIT "" */ if(tag.t === undefined && p.v === undefined) { if(!opts.sheetStubs) continue; p.t = "z"; @@ -9973,7 +10019,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { cref = d.match(isregex); p.t = 's'; if(cref != null && (sstr = parse_si(cref[1]))) p.v = sstr.t; else p.v = ""; - break; // inline string + break; case 'b': p.v = parsexmlbool(p.v); break; case 'd': if(!opts.cellDates) { p.v = datenum(parseDate(p.v)); p.t = 'n'; } @@ -10041,19 +10087,23 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { var o = [XML_HEADER, WS_XML_ROOT]; var s = wb.SheetNames[idx], sidx = 0, rdata = ""; var ws = wb.Sheets[s]; - if(ws === undefined) ws = {}; - var ref = ws['!ref']; if(ref === undefined) ref = 'A1'; + if(ws == null) ws = {}; + var ref = ws['!ref']; if(ref == null) ref = 'A1'; if(!rels) rels = {}; ws['!comments'] = []; ws['!drawing'] = []; o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])})); + o[o.length] = (writextag('dimension', null, {'ref': ref})); + /* sheetViews */ + /* TODO: store in WB, process styles */ if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' })); - if(ws['!cols'] !== undefined && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); + if(ws['!cols'] != null && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); + o[sidx = o.length] = ''; ws['!links'] = []; if(ws['!ref'] != null) { @@ -10062,10 +10112,25 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { } if(o.length>sidx+1) { o[o.length] = (''); o[sidx]=o[sidx].replace("/>",">"); } + /* sheetCalcPr */ + if(ws['!protect'] != null) o[o.length] = write_ws_xml_protection(ws['!protect']); + /* protectedRanges */ + /* scenarios */ + + if(ws['!autofilter'] != null) o[o.length] = write_ws_xml_autofilter(ws['!autofilter']); + + /* sortState */ + /* dataConsolidate */ + /* customSheetViews */ + if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); + /* phoneticPr */ + /* conditionalFormatting */ + /* dataValidations */ + var relc = -1, rel, rId = -1; if(ws['!links'].length > 0) { o[o.length] = ""; @@ -10081,21 +10146,41 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { } delete ws['!links']; + /* printOptions */ + /* pageMargins */ + /* pageSetup */ + var hfidx = o.length; o[o.length] = ""; + /* rowBreaks */ + /* colBreaks */ + /* customProperties */ + /* cellWatches */ + /* ignoredErrors */ + /* smartTags */ + if(ws['!drawing'].length > 0) { rId = add_rels(rels, -1, "../drawings/drawing" + (idx+1) + ".xml", RELS.DRAW); ws['!drawing'].rid = rId; o[o.length] = writextag("drawing", null, {"r:id":"rId" + rId}); } else delete ws['!drawing']; + if(ws['!comments'].length > 0) { rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); o[o.length] = writextag("legacyDrawing", null, {"r:id":"rId" + rId}); ws['!legacy'] = rId; } + /* drawingHF */ + /* picture */ + /* oleObjects */ + /* controls */ + /* webPublishItems */ + /* tableParts */ + /* extList */ + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -10390,6 +10475,35 @@ function write_BrtColInfo(C/*:number*/, col, o) { return o; } +/* [MS-XLSB] 2.4.740 BrtSheetProtection */ +function write_BrtSheetProtection(sp, o) { + if(o == null) o = new_buf(16*4+2); + o.write_shift(2, sp.password ? crypto_CreatePasswordVerifier_Method1(sp.password) : 0); + o.write_shift(4, 1); // this record should not be written if no protection + [ + ["objects", false], // fObjects + ["scenarios", false], // fScenarios + ["formatCells", true], // fFormatCells + ["formatColumns", true], // fFormatColumns + ["formatRows", true], // fFormatRows + ["insertColumns", true], // fInsertColumns + ["insertRows", true], // fInsertRows + ["insertHyperlinks", true], // fInsertHyperlinks + ["deleteColumns", true], // fDeleteColumns + ["deleteRows", true], // fDeleteRows + ["selectLockedCells", false], // fSelLockedCells + ["sort", true], // fSort + ["autoFilter", true], // fAutoFilter + ["pivotTables", true], // fPivotTables + ["selectUnlockedCells", false] // fSelUnlockedCells + ].forEach(function(n) { + o.write_shift(4, 1); + if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0); + else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1); + }); + return o; +} + /* [MS-XLSB] 2.1.7.61 Worksheet */ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { if(!data) return data; @@ -10535,6 +10649,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { } break; + case 0x00A1: /* 'BrtBeginAFilter' */ + s['!autofilter'] = { ref:encode_range(val) }; + break; + case 0x00AF: /* 'BrtAFilterDateGroupItem' */ case 0x0284: /* 'BrtActiveX' */ case 0x0271: /* 'BrtBigName' */ @@ -10729,6 +10847,21 @@ function write_LEGACYDRAWING(ba, ws/*:Worksheet*/, idx/*:number*/, rels) { } } +function write_AUTOFILTER(ba, ws) { + if(!ws['!autofilter']) return; + write_record(ba, "BrtBeginAFilter", write_UncheckedRfX(decode_range(ws['!autofilter'].ref))); + /* *FILTERCOLUMN */ + /* [SORTSTATE] */ + /* BrtEndAFilter */ + write_record(ba, "BrtEndAFilter"); +} + +function write_SHEETPROTECT(ba, ws) { + if(!ws['!protect']) return; + /* [BrtSheetProtectionIso] */ + write_record(ba, "BrtSheetProtection", write_BrtSheetProtection(ws['!protect'])); +} + function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { var ba = buf_array(); var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {}; @@ -10744,10 +10877,10 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { write_COLINFOS(ba, ws, idx, opts, wb); write_CELLTABLE(ba, ws, idx, opts, wb); /* [BrtSheetCalcProp] */ - /* [[BrtSheetProtectionIso] BrtSheetProtection] */ + write_SHEETPROTECT(ba, ws); /* *([BrtRangeProtectionIso] BrtRangeProtection) */ /* [SCENMAN] */ - /* [AUTOFILTER] */ + write_AUTOFILTER(ba, ws); /* [SORTSTATE] */ /* [DCON] */ /* [USERSHVIEWS] */ @@ -11185,7 +11318,15 @@ function safe1904(wb/*:Workbook*/)/*:string*/ { function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { var o = [XML_HEADER]; o[o.length] = WB_XML_ROOT; + + /* fileVersion */ + /* fileSharing */ + o[o.length] = (writextag('workbookPr', null, {date1904:safe1904(wb), codeName:"ThisWorkbook"})); + + /* workbookProtection */ + /* bookViews */ + o[o.length] = ""; var sheets = wb.Workbook && wb.Workbook.Sheets || []; for(var i = 0; i != wb.SheetNames.length; ++i) { @@ -11199,6 +11340,21 @@ function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { o[o.length] = (writextag('sheet',null,sht)); } o[o.length] = ""; + + /* functionGroups */ + /* externalReferences */ + /* definedNames */ + /* calcPr */ + /* oleSize */ + /* customWorkbookViews */ + /* pivotCaches */ + /* smartTagPr */ + /* smartTagTypes */ + /* webPublishing */ + /* fileRecoveryPr */ + /* webPublishObjects */ + /* extLst */ + if(o.length>2){ o[o.length] = ''; o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -11878,6 +12034,9 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'TotalTime': case 'HyperlinkBase': case 'Manager': + case 'ContentStatus': + case 'Identifier': + case 'Language': if(Rn[0].slice(-2) === "/>") break; else if(Rn[1]==="/") xlml_set_prop(Props, Rn[3], str.slice(pidx, Rn.index)); else pidx = Rn.index + Rn[0].length; @@ -11902,6 +12061,15 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { } break; + case 'AutoFilter': + if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw new Error("Bad state: "+tmp.join("|"));} + else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + var AutoFilter = xlml_parsexmltag(Rn[0]); + cursheet['!autofilter'] = { ref:rc_to_a1(AutoFilter.Range).replace(/\$/g,"") }; + state.push([Rn[3], true]); + } + break; + case 'Name': break; case 'ComponentOptions': @@ -11915,7 +12083,6 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { case 'PageBreaks': case 'QueryTable': case 'DataValidation': - case 'AutoFilter': case 'Sorting': case 'Schema': case 'data': @@ -12310,7 +12477,7 @@ function parse_xlml(data, opts)/*:Workbook*/ { function write_props_xlml(wb, opts) { var o = []; /* DocumentProperties */ - if(wb.Props) o.push(xlml_write_docprops(wb.Props)); + if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); /* CustomDocumentProperties */ if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops)); return o.join(""); @@ -12638,6 +12805,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { var last_Rn = ''; var file_depth = 0; /* TODO: make a real stack */ var BIFF2Fmt = 0; + var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ /* explicit override for some broken writers */ opts.codepage = 1200; @@ -12713,7 +12881,14 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; case 'ExternName': supbooks[sbc][++sbci] = val; break; case 'Index': break; // TODO - case 'Lbl': supbooks[0][++sbcli] = val; break; + case 'Lbl': + supbooks[0][++sbcli] = val; // TODO: local formula storage in stringify_formula + if(!supbooks[val.itab]) supbooks[val.itab] = []; + supbooks[val.itab].push(val); + if(val.Name == "\r" && val.itab > 0) + if(val.rgce && val.rgce[0] && val.rgce[0][0] && val.rgce[0][0][0] == 'PtgArea3d') + FilterDatabases[val.itab - 1] = { ref: encode_range(val.rgce[0][0][1][2]) }; + break; case 'ExternSheet': supbooks[sbc] = supbooks[sbc].concat(val); sbci += val.length; break; case 'Protect': out["!protect"] = val; break; /* for sheet or book */ @@ -13173,6 +13348,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { wb.Directory=sheetnamesraw; wb.SheetNames=sheetnamesraw; if(!options.bookSheets) wb.Sheets=Sheets; + if(wb.Sheets) FilterDatabases.forEach(function(r,i) { wb.Sheets[wb.SheetNames[i]]['!autofilter'] = r; }); wb.Preamble=Preamble; wb.Strings = sst; wb.SSF = SSF.get_table(); @@ -13330,7 +13506,7 @@ var XLSBRecordEnum = { /*::[*/0x009E/*::]*/: { n:"BrtBookView", f:parsenoop }, /*::[*/0x009F/*::]*/: { n:"BrtBeginSst", f:parse_BrtBeginSst }, /*::[*/0x00A0/*::]*/: { n:"BrtEndSst", f:parsenoop }, - /*::[*/0x00A1/*::]*/: { n:"BrtBeginAFilter", f:parsenoop }, + /*::[*/0x00A1/*::]*/: { n:"BrtBeginAFilter", f:parse_UncheckedRfX }, /*::[*/0x00A2/*::]*/: { n:"BrtEndAFilter", f:parsenoop }, /*::[*/0x00A3/*::]*/: { n:"BrtBeginFilterColumn", f:parsenoop }, /*::[*/0x00A4/*::]*/: { n:"BrtEndFilterColumn", f:parsenoop }, @@ -14981,6 +15157,15 @@ var parse_content_xml = (function() { if(Rn[1]==='/') textp = (textp.length > 0 ? textp + "\n" : "") + parse_text_p(str.slice(textpidx,Rn.index), textptag); else { textptag = parsexmltag(Rn[0], false); textpidx = Rn.index + Rn[0].length; } break; // + + case 'database-range': // 9.4.15 + if(Rn[1]==='/') break; + try { + var AutoFilter = ods_to_csf_range_3D(parsexmltag(Rn[0])['target-range-address']); + Sheets[AutoFilter[0]]['!autofilter'] = { ref: AutoFilter[1] }; + } catch(e) { } + break; + case 's': break; // case 'date': break; // <*:date> @@ -14995,7 +15180,6 @@ var parse_content_xml = (function() { case 'content-validation': break; // 9.4.5 - case 'database-range': break; // 9.4.15 case 'filter': break; // 9.5.2 case 'filter-and': break; // 9.5.3 case 'filter-or': break; // 9.5.4 diff --git a/xlsx.js b/xlsx.js index 4dfb687..f4ca499 100644 --- a/xlsx.js +++ b/xlsx.js @@ -2986,15 +2986,24 @@ function cp_doit(f, g, h, o, p) { o[o.length] = (h ? writextag(f,g,h) : writetag(f,g)); } -function write_core_props(cp, opts) { +function write_core_props(cp, _opts) { + var opts = _opts || {}; var o = [XML_HEADER, CORE_PROPS_XML_ROOT], p = {}; - if(!cp) return o.join(""); + if(!cp && !opts.Props) return o.join(""); + if(cp) { + if(cp.CreatedDate != null) cp_doit("dcterms:created", typeof cp.CreatedDate === "string" ? cp.CreatedDate : write_w3cdtf(cp.CreatedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); + if(cp.ModifiedDate != null) cp_doit("dcterms:modified", typeof cp.ModifiedDate === "string" ? cp.ModifiedDate : write_w3cdtf(cp.ModifiedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); + } - if(cp.CreatedDate != null) cp_doit("dcterms:created", typeof cp.CreatedDate === "string" ? cp.CreatedDate : write_w3cdtf(cp.CreatedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); - if(cp.ModifiedDate != null) cp_doit("dcterms:modified", typeof cp.ModifiedDate === "string" ? cp.ModifiedDate : write_w3cdtf(cp.ModifiedDate, opts.WTF), {"xsi:type":"dcterms:W3CDTF"}, o, p); - - for(var i = 0; i != CORE_PROPS.length; ++i) { var f = CORE_PROPS[i]; cp_doit(f[0], cp[f[1]], null, o, p); } + for(var i = 0; i != CORE_PROPS.length; ++i) { + var f = CORE_PROPS[i]; + var v = opts.Props && opts.Props[f[1]] != null ? opts.Props[f[1]] : cp ? cp[f[1]] : null; + if(v === true) v = "1"; + else if(v === false) v = "0"; + else if(typeof v == "number") v = String(v); + if(v != null) cp_doit(f[0], v, null, o, p); + } if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -3089,7 +3098,7 @@ function write_ext_props(cp, opts) { if(cp[f[1]] === undefined) return; var v; switch(f[2]) { - case 'string': v = cp[f[1]]; break; + case 'string': v = String(cp[f[1]]); break; case 'bool': v = cp[f[1]] ? 'true' : 'false'; break; } if(v !== undefined) o[o.length] = (W(f[0], v)); @@ -3169,52 +3178,63 @@ function write_cust_props(cp, opts) { if(o.length>2){ o[o.length] = ''; o[1]=o[1].replace("/>",">"); } return o.join(""); } +/* Common Name -> XLML Name */ +var XLMLDocPropsMap = { + Category: 'Category', + ContentStatus: 'ContentStatus', /* NOTE: missing from schema */ + Keywords: 'Keywords', + LastAuthor: 'LastAuthor', + LastPrinted: 'LastPrinted', + RevNumber: 'Revision', + Author: 'Author', + Comments: 'Description', + Identifier: 'Identifier', /* NOTE: missing from schema */ + Language: 'Language', /* NOTE: missing from schema */ + Subject: 'Subject', + Title: 'Title', + CreatedDate: 'Created', + ModifiedDate: 'LastSaved', + + Application: 'AppName', + AppVersion: 'Version', + TotalTime: 'TotalTime', + Manager: 'Manager', + Company: 'Company' +}; +var evert_XLMLDPM = evert(XLMLDocPropsMap); + function xlml_set_prop(Props, tag, val) { - /* TODO: Normalize the properties */ - switch(tag) { - case 'Description': tag = 'Comments'; break; - case 'Created': tag = 'CreatedDate'; break; - case 'LastSaved': tag = 'ModifiedDate'; break; - } + tag = evert_XLMLDPM[tag] || tag; Props[tag] = val; } -var XLMLDocumentProperties = [ - ['Title', 'Title'], - ['Subject', 'Subject'], - ['Author', 'Author'], - ['Keywords', 'Keywords'], - ['Comments', 'Description'], - ['LastAuthor', 'LastAuthor'], - ['CreatedDate', 'Created', 'date'], - ['ModifiedDate', 'LastSaved', 'date'], - ['Category', 'Category'], - ['Manager', 'Manager'], - ['Company', 'Company'], - ['AppVersion', 'Version'] -]; /* TODO: verify */ -function xlml_write_docprops(Props) { - var T = 'DocumentProperties'; +function xlml_write_docprops(Props, opts) { var o = []; - XLMLDocumentProperties.forEach(function(p) { - if(!Props[p[0]]) return; - var m = Props[p[0]]; + CORE_PROPS.concat(EXT_PROPS).forEach(function(p) { + if(Props[p[1]] == null) return; + var m = opts && opts.Props && opts.Props[p[1]] != null ? opts.Props[p[1]] : Props[p[1]]; switch(p[2]) { case 'date': m = new Date(m).toISOString(); break; } - o.push(writetag(p[1], m)); + if(typeof m == 'number') m = String(m); + else if(m === true || m === false) { t = "boolean"; m = m ? "1" : "0"; } + else if(m instanceof Date) m = new Date(m).toISOString(); + o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); }); - return '<' + T + ' xmlns="' + XLMLNS.o + '">' + o.join("") + ''; + return writextag('DocumentProperties', o.join(""), {xmlns:XLMLNS.o }); } -function xlml_write_custprops(Props, Custprops) { +function xlml_write_custprops(Props, Custprops, opts) { + var BLACKLIST = ["Worksheets","SheetNames"]; var T = 'CustomDocumentProperties'; var o = []; if(Props) keys(Props).forEach(function(k) { if(!Props.hasOwnProperty(k)) return; - for(var i = 0; i < XLMLDocumentProperties.length; ++i) - if(k == XLMLDocumentProperties[i][0]) return; + for(var i = 0; i < CORE_PROPS.length; ++i) if(k == CORE_PROPS[i][1]) return; + for(i = 0; i < EXT_PROPS.length; ++i) if(k == EXT_PROPS[i][1]) return; + for(i = 0; i < BLACKLIST.length; ++i) if(k == BLACKLIST[i]) return; + var m = Props[k]; var t = "string"; if(typeof m == 'number') { t = "float"; m = String(m); } @@ -4134,9 +4154,10 @@ function parse_Lbl(blob, length, opts) { var chKey = blob.read_shift(1); var cch = blob.read_shift(1); var cce = blob.read_shift(opts && opts.biff == 2 ? 1 : 2); + var itab = 0; if(!opts || opts.biff >= 5) { blob.l += 2; - var itab = blob.read_shift(2); + itab = blob.read_shift(2); blob.l += 4; } var name = parse_XLUnicodeStringNoCch(blob, cch, opts); @@ -4145,6 +4166,7 @@ function parse_Lbl(blob, length, opts) { return { chKey: chKey, Name: name, + itab: itab, rgce: rgce }; } @@ -9547,6 +9569,12 @@ function csf_to_ods_formula(f) { /* TODO: something other than this */ return o.replace(/;/g, "|").replace(/,/g,";"); } + +function ods_to_csf_range_3D(r) { + var a = r.split(":"); + var s = a[0].split(".")[0]; + return [s, a[0].split(".")[1] + ":" + a[1].split(".")[1]]; +} var strs = {}; // shared strings var _ssfopts = {}; // spreadsheet formatting options @@ -9630,43 +9658,54 @@ var sheetdataregex = /<(?:\w+:)?sheetData>([^\u2603]*)<\/(?:\w+:)?sheetData>/; var hlinkregex = /<(?:\w:)?hyperlink [^>]*>/mg; var dimregex = /"(\w*:\w*)"/; var colregex = /<(?:\w:)?col[^>]*[\/]?>/g; +var afregex = /<(?:\w:)?autoFilter[^>]*([\/]|>([^\u2603]*)<\/(?:\w:)?autoFilter)>/g; /* 18.3 Worksheets */ function parse_ws_xml(data, opts, rels, wb, themes, styles) { if(!data) return data; if(DENSE != null && opts.dense == null) opts.dense = DENSE; + /* 18.3.1.99 worksheet CT_Worksheet */ var s = opts.dense ? ([]) : ({}); + var refguess = ({s: {r:2000000, c:2000000}, e: {r:0, c:0} }); + + var data1 = "", data2 = ""; + var mtch=data.match(sheetdataregex); + if(mtch) { + data1 = data.substr(0, mtch.index); + data2 = data.substr(mtch.index + mtch[0].length); + } else data1 = data2 = data; /* 18.3.1.35 dimension CT_SheetDimension ? */ // $FlowIgnore - var ridx = (data.match(/<(?:\w*:)?dimension/)||{index:-1}).index; + var ridx = (data1.match(/<(?:\w*:)?dimension/)||{index:-1}).index; if(ridx > 0) { - var ref = data.substr(ridx,50).match(dimregex); - if(ref != null) parse_ws_xml_dim(s, ref[1]); + var ref = data1.substr(ridx,50).match(dimregex); + if(ref) parse_ws_xml_dim(s, ref[1]); } - /* 18.3.1.55 mergeCells CT_MergeCells */ - var mergecells = []; - var merges = data.match(mergecregex); - if(merges) for(ridx = 0; ridx != merges.length; ++ridx) - mergecells[ridx] = safe_decode_range(merges[ridx].substr(merges[ridx].indexOf("\"")+1)); - /* 18.3.1.17 cols CT_Cols */ var columns = []; if(opts.cellStyles) { /* 18.3.1.13 col CT_Col */ - var cols = data.match(colregex); + var cols = data1.match(colregex); if(cols) parse_ws_xml_cols(columns, cols); } - var refguess = ({s: {r:2000000, c:2000000}, e: {r:0, c:0} }); - /* 18.3.1.80 sheetData CT_SheetData ? */ - var mtch=data.match(sheetdataregex); if(mtch) parse_ws_xml_data(mtch[1], s, opts, refguess, themes, styles); + /* 18.3.1.2 autoFilter CT_AutoFilter */ + var afilter = data2.match(afregex); + if(afilter) s['!autofilter'] = parse_ws_xml_autofilter(afilter[0]); + + /* 18.3.1.55 mergeCells CT_MergeCells */ + var mergecells = []; + var merges = data2.match(mergecregex); + if(merges) for(ridx = 0; ridx != merges.length; ++ridx) + mergecells[ridx] = safe_decode_range(merges[ridx].substr(merges[ridx].indexOf("\"")+1)); + /* 18.3.1.48 hyperlinks CT_Hyperlinks */ - var hlink = data.match(hlinkregex); + var hlink = data2.match(hlinkregex); if(hlink) parse_ws_xml_hlinks(s, hlink, rels); if(!s["!ref"] && refguess.e.c >= refguess.s.c && refguess.e.r >= refguess.s.r) s["!ref"] = encode_range(refguess); @@ -9765,6 +9804,14 @@ function write_ws_xml_cols(ws, cols) { return o.join(""); } +function parse_ws_xml_autofilter(data) { + var o = { ref: (data.match(/ref="([^"]*)"/)||[])[1]}; + return o; +} +function write_ws_xml_autofilter(data) { + return writextag("autoFilter", null, {ref:data.ref}); +} + function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return ""; var vv = ""; @@ -9884,7 +9931,6 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { p.F = arrayf[i][1]; } - /* SCHEMA IS ACTUALLY INCORRECT HERE. IF A CELL HAS NO T, EMIT "" */ if(tag.t === undefined && p.v === undefined) { if(!opts.sheetStubs) continue; p.t = "z"; @@ -9916,7 +9962,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { cref = d.match(isregex); p.t = 's'; if(cref != null && (sstr = parse_si(cref[1]))) p.v = sstr.t; else p.v = ""; - break; // inline string + break; case 'b': p.v = parsexmlbool(p.v); break; case 'd': if(!opts.cellDates) { p.v = datenum(parseDate(p.v)); p.t = 'n'; } @@ -9984,19 +10030,23 @@ function write_ws_xml(idx, opts, wb, rels) { var o = [XML_HEADER, WS_XML_ROOT]; var s = wb.SheetNames[idx], sidx = 0, rdata = ""; var ws = wb.Sheets[s]; - if(ws === undefined) ws = {}; - var ref = ws['!ref']; if(ref === undefined) ref = 'A1'; + if(ws == null) ws = {}; + var ref = ws['!ref']; if(ref == null) ref = 'A1'; if(!rels) rels = {}; ws['!comments'] = []; ws['!drawing'] = []; o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])})); + o[o.length] = (writextag('dimension', null, {'ref': ref})); + /* sheetViews */ + /* TODO: store in WB, process styles */ if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' })); - if(ws['!cols'] !== undefined && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); + if(ws['!cols'] != null && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); + o[sidx = o.length] = ''; ws['!links'] = []; if(ws['!ref'] != null) { @@ -10005,10 +10055,25 @@ function write_ws_xml(idx, opts, wb, rels) { } if(o.length>sidx+1) { o[o.length] = (''); o[sidx]=o[sidx].replace("/>",">"); } + /* sheetCalcPr */ + if(ws['!protect'] != null) o[o.length] = write_ws_xml_protection(ws['!protect']); + /* protectedRanges */ + /* scenarios */ + + if(ws['!autofilter'] != null) o[o.length] = write_ws_xml_autofilter(ws['!autofilter']); + + /* sortState */ + /* dataConsolidate */ + /* customSheetViews */ + if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); + /* phoneticPr */ + /* conditionalFormatting */ + /* dataValidations */ + var relc = -1, rel, rId = -1; if(ws['!links'].length > 0) { o[o.length] = ""; @@ -10024,21 +10089,41 @@ function write_ws_xml(idx, opts, wb, rels) { } delete ws['!links']; + /* printOptions */ + /* pageMargins */ + /* pageSetup */ + var hfidx = o.length; o[o.length] = ""; + /* rowBreaks */ + /* colBreaks */ + /* customProperties */ + /* cellWatches */ + /* ignoredErrors */ + /* smartTags */ + if(ws['!drawing'].length > 0) { rId = add_rels(rels, -1, "../drawings/drawing" + (idx+1) + ".xml", RELS.DRAW); ws['!drawing'].rid = rId; o[o.length] = writextag("drawing", null, {"r:id":"rId" + rId}); } else delete ws['!drawing']; + if(ws['!comments'].length > 0) { rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); o[o.length] = writextag("legacyDrawing", null, {"r:id":"rId" + rId}); ws['!legacy'] = rId; } + /* drawingHF */ + /* picture */ + /* oleObjects */ + /* controls */ + /* webPublishItems */ + /* tableParts */ + /* extList */ + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -10333,6 +10418,35 @@ function write_BrtColInfo(C, col, o) { return o; } +/* [MS-XLSB] 2.4.740 BrtSheetProtection */ +function write_BrtSheetProtection(sp, o) { + if(o == null) o = new_buf(16*4+2); + o.write_shift(2, sp.password ? crypto_CreatePasswordVerifier_Method1(sp.password) : 0); + o.write_shift(4, 1); // this record should not be written if no protection + [ + ["objects", false], // fObjects + ["scenarios", false], // fScenarios + ["formatCells", true], // fFormatCells + ["formatColumns", true], // fFormatColumns + ["formatRows", true], // fFormatRows + ["insertColumns", true], // fInsertColumns + ["insertRows", true], // fInsertRows + ["insertHyperlinks", true], // fInsertHyperlinks + ["deleteColumns", true], // fDeleteColumns + ["deleteRows", true], // fDeleteRows + ["selectLockedCells", false], // fSelLockedCells + ["sort", true], // fSort + ["autoFilter", true], // fAutoFilter + ["pivotTables", true], // fPivotTables + ["selectUnlockedCells", false] // fSelUnlockedCells + ].forEach(function(n) { + o.write_shift(4, 1); + if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0); + else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1); + }); + return o; +} + /* [MS-XLSB] 2.1.7.61 Worksheet */ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { if(!data) return data; @@ -10478,6 +10592,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { } break; + case 0x00A1: /* 'BrtBeginAFilter' */ + s['!autofilter'] = { ref:encode_range(val) }; + break; + case 0x00AF: /* 'BrtAFilterDateGroupItem' */ case 0x0284: /* 'BrtActiveX' */ case 0x0271: /* 'BrtBigName' */ @@ -10672,6 +10790,21 @@ function write_LEGACYDRAWING(ba, ws, idx, rels) { } } +function write_AUTOFILTER(ba, ws) { + if(!ws['!autofilter']) return; + write_record(ba, "BrtBeginAFilter", write_UncheckedRfX(decode_range(ws['!autofilter'].ref))); + /* *FILTERCOLUMN */ + /* [SORTSTATE] */ + /* BrtEndAFilter */ + write_record(ba, "BrtEndAFilter"); +} + +function write_SHEETPROTECT(ba, ws) { + if(!ws['!protect']) return; + /* [BrtSheetProtectionIso] */ + write_record(ba, "BrtSheetProtection", write_BrtSheetProtection(ws['!protect'])); +} + function write_ws_bin(idx, opts, wb, rels) { var ba = buf_array(); var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {}; @@ -10687,10 +10820,10 @@ function write_ws_bin(idx, opts, wb, rels) { write_COLINFOS(ba, ws, idx, opts, wb); write_CELLTABLE(ba, ws, idx, opts, wb); /* [BrtSheetCalcProp] */ - /* [[BrtSheetProtectionIso] BrtSheetProtection] */ + write_SHEETPROTECT(ba, ws); /* *([BrtRangeProtectionIso] BrtRangeProtection) */ /* [SCENMAN] */ - /* [AUTOFILTER] */ + write_AUTOFILTER(ba, ws); /* [SORTSTATE] */ /* [DCON] */ /* [USERSHVIEWS] */ @@ -11128,7 +11261,15 @@ function safe1904(wb) { function write_wb_xml(wb, opts) { var o = [XML_HEADER]; o[o.length] = WB_XML_ROOT; + + /* fileVersion */ + /* fileSharing */ + o[o.length] = (writextag('workbookPr', null, {date1904:safe1904(wb), codeName:"ThisWorkbook"})); + + /* workbookProtection */ + /* bookViews */ + o[o.length] = ""; var sheets = wb.Workbook && wb.Workbook.Sheets || []; for(var i = 0; i != wb.SheetNames.length; ++i) { @@ -11142,6 +11283,21 @@ function write_wb_xml(wb, opts) { o[o.length] = (writextag('sheet',null,sht)); } o[o.length] = ""; + + /* functionGroups */ + /* externalReferences */ + /* definedNames */ + /* calcPr */ + /* oleSize */ + /* customWorkbookViews */ + /* pivotCaches */ + /* smartTagPr */ + /* smartTagTypes */ + /* webPublishing */ + /* fileRecoveryPr */ + /* webPublishObjects */ + /* extLst */ + if(o.length>2){ o[o.length] = ''; o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -11818,6 +11974,9 @@ for(var cma = c; cma <= cc; ++cma) { case 'TotalTime': case 'HyperlinkBase': case 'Manager': + case 'ContentStatus': + case 'Identifier': + case 'Language': if(Rn[0].slice(-2) === "/>") break; else if(Rn[1]==="/") xlml_set_prop(Props, Rn[3], str.slice(pidx, Rn.index)); else pidx = Rn.index + Rn[0].length; @@ -11842,6 +12001,15 @@ for(var cma = c; cma <= cc; ++cma) { } break; + case 'AutoFilter': + if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw new Error("Bad state: "+tmp.join("|"));} + else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + var AutoFilter = xlml_parsexmltag(Rn[0]); + cursheet['!autofilter'] = { ref:rc_to_a1(AutoFilter.Range).replace(/\$/g,"") }; + state.push([Rn[3], true]); + } + break; + case 'Name': break; case 'ComponentOptions': @@ -11855,7 +12023,6 @@ for(var cma = c; cma <= cc; ++cma) { case 'PageBreaks': case 'QueryTable': case 'DataValidation': - case 'AutoFilter': case 'Sorting': case 'Schema': case 'data': @@ -12249,7 +12416,7 @@ function parse_xlml(data, opts) { function write_props_xlml(wb, opts) { var o = []; /* DocumentProperties */ - if(wb.Props) o.push(xlml_write_docprops(wb.Props)); + if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); /* CustomDocumentProperties */ if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops)); return o.join(""); @@ -12577,6 +12744,7 @@ function parse_workbook(blob, options) { var last_Rn = ''; var file_depth = 0; /* TODO: make a real stack */ var BIFF2Fmt = 0; + var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ /* explicit override for some broken writers */ opts.codepage = 1200; @@ -12652,7 +12820,14 @@ function parse_workbook(blob, options) { case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; case 'ExternName': supbooks[sbc][++sbci] = val; break; case 'Index': break; // TODO - case 'Lbl': supbooks[0][++sbcli] = val; break; + case 'Lbl': + supbooks[0][++sbcli] = val; // TODO: local formula storage in stringify_formula + if(!supbooks[val.itab]) supbooks[val.itab] = []; + supbooks[val.itab].push(val); + if(val.Name == "\r" && val.itab > 0) + if(val.rgce && val.rgce[0] && val.rgce[0][0] && val.rgce[0][0][0] == 'PtgArea3d') + FilterDatabases[val.itab - 1] = { ref: encode_range(val.rgce[0][0][1][2]) }; + break; case 'ExternSheet': supbooks[sbc] = supbooks[sbc].concat(val); sbci += val.length; break; case 'Protect': out["!protect"] = val; break; /* for sheet or book */ @@ -13112,6 +13287,7 @@ function parse_workbook(blob, options) { wb.Directory=sheetnamesraw; wb.SheetNames=sheetnamesraw; if(!options.bookSheets) wb.Sheets=Sheets; + if(wb.Sheets) FilterDatabases.forEach(function(r,i) { wb.Sheets[wb.SheetNames[i]]['!autofilter'] = r; }); wb.Preamble=Preamble; wb.Strings = sst; wb.SSF = SSF.get_table(); @@ -13269,7 +13445,7 @@ var XLSBRecordEnum = { 0x009E: { n:"BrtBookView", f:parsenoop }, 0x009F: { n:"BrtBeginSst", f:parse_BrtBeginSst }, 0x00A0: { n:"BrtEndSst", f:parsenoop }, -0x00A1: { n:"BrtBeginAFilter", f:parsenoop }, +0x00A1: { n:"BrtBeginAFilter", f:parse_UncheckedRfX }, 0x00A2: { n:"BrtEndAFilter", f:parsenoop }, 0x00A3: { n:"BrtBeginFilterColumn", f:parsenoop }, 0x00A4: { n:"BrtEndFilterColumn", f:parsenoop }, @@ -14920,6 +15096,15 @@ var parse_content_xml = (function() { if(Rn[1]==='/') textp = (textp.length > 0 ? textp + "\n" : "") + parse_text_p(str.slice(textpidx,Rn.index), textptag); else { textptag = parsexmltag(Rn[0], false); textpidx = Rn.index + Rn[0].length; } break; // + + case 'database-range': // 9.4.15 + if(Rn[1]==='/') break; + try { + var AutoFilter = ods_to_csf_range_3D(parsexmltag(Rn[0])['target-range-address']); + Sheets[AutoFilter[0]]['!autofilter'] = { ref: AutoFilter[1] }; + } catch(e) { } + break; + case 's': break; // case 'date': break; // <*:date> @@ -14934,7 +15119,6 @@ var parse_content_xml = (function() { case 'content-validation': break; // 9.4.5 - case 'database-range': break; // 9.4.15 case 'filter': break; // 9.5.2 case 'filter-and': break; // 9.5.3 case 'filter-or': break; // 9.5.4