diff --git a/README.md b/README.md index b5c39a5..ad52de7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ with a unified JS representation, and ES3/ES5 browser compatibility back to IE6. + [Chartsheet Object](#chartsheet-object) * [Workbook Object](#workbook-object) + [Workbook File Properties](#workbook-file-properties) + * [Workbook-Level Attributes](#workbook-level-attributes) + + [Defined Names](#defined-names) * [Document Features](#document-features) + [Formulae](#formulae) + [Column Properties](#column-properties) @@ -669,6 +671,26 @@ Writers will process the `Props` key of the options object: /* force the Author to be "SheetJS" */ XLSX.write(wb, {Props:{Author:"SheetJS"}}); ``` + +### Workbook-Level Attributes + +`wb.Workbook` stores workbook level attributes. + +#### Defined Names + +`wb.Workbook.Names` is an array of defined name objects which have the keys: + +| Key | Description | +|:----------|:-----------------------------------------------------------------| +| `Sheet` | Name scope. Sheet Index (0 = first sheet) or `null` (Workbook) | +| `Name` | Case-sensitive name. Standard rules apply ** | +| `Ref` | A1-style Reference (e.g. `"Sheet1!$A$1:$D$20"`) | +| `Comment` | Comment (only applicable for XLS/XLSX/XLSB) | + +Excel allows two sheet-scoped defined names to share the same name. However, a +sheet-scoped name cannot collide with a workbook-scope name. Workbook writers +may not enforce this constraint. + ### Document Features Even for basic features like date storage, the official Excel formats store the diff --git a/bits/36_xlsprops.js b/bits/36_xlsprops.js index 451715b..617acf8 100644 --- a/bits/36_xlsprops.js +++ b/bits/36_xlsprops.js @@ -39,7 +39,7 @@ function xlml_write_docprops(Props, opts) { case 'date': m = new Date(m).toISOString(); break; } if(typeof m == 'number') m = String(m); - else if(m === true || m === false) { t = "boolean"; m = m ? "1" : "0"; } + else if(m === true || m === false) { m = m ? "1" : "0"; } else if(m instanceof Date) m = new Date(m).toISOString(); o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); }); diff --git a/bits/38_xlstypes.js b/bits/38_xlstypes.js index f81c5f7..26628ae 100644 --- a/bits/38_xlstypes.js +++ b/bits/38_xlstypes.js @@ -278,13 +278,6 @@ function parslurp(blob, length, cb) { return arr; } -function parslurp2(blob, length, cb) { - var arr = [], target = blob.l + length, len = blob.read_shift(2); - while(len-- !== 0) arr.push(cb(blob, target - blob.l)); - if(target !== blob.l) throw new Error("Slurp error"); - return arr; -} - function parsebool(blob, length) { return blob.read_shift(length) === 0x1; } function parseuint16(blob) { return blob.read_shift(2, 'u'); } diff --git a/bits/39_xlsbiff.js b/bits/39_xlsbiff.js index 7c5fe8c..eba93a7 100644 --- a/bits/39_xlsbiff.js +++ b/bits/39_xlsbiff.js @@ -442,6 +442,7 @@ function parse_ExternName(blob, length, opts) { if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts); //else throw new Error("unsupported SupBook cch: " + opts.sbcch); o.body = body || blob.read_shift(length-2); + if(typeof body === "string") o.Name = body; return o; } @@ -472,13 +473,21 @@ function parse_Lbl(blob, length, opts) { /* 2.4.106 TODO: verify supbook manipulation */ function parse_ExternSheet(blob, length, opts) { if(opts.biff < 8) return parse_ShortXLUnicodeString(blob, length, opts); - var o = parslurp2(blob,length,parse_XTI); + var o = [], target = blob.l + length, len = blob.read_shift(2); + while(len-- !== 0) o.push(parse_XTI(blob, 6)); + // [iSupBook, itabFirst, itabLast]; var oo = []; - if(opts.sbcch === 0x0401) { - for(var i = 0; i != o.length; ++i) oo.push(opts.snames[o[i][1]]); - return oo; - } - else return o; + return o; +} + +/* 2.4.176 TODO: check older biff */ +function parse_NameCmt(blob, length, opts) { + if(opts.biff < 8) { blob.l += length; return; } + var cchName = blob.read_shift(2); + var cchComment = blob.read_shift(2); + var name = parse_XLUnicodeStringNoCch(blob, cchName, opts); + var comment = parse_XLUnicodeStringNoCch(blob, cchComment, opts); + return [name, comment]; } /* 2.4.260 */ @@ -894,7 +903,6 @@ var parse_TableStyles = parsenoop; var parse_TableStyle = parsenoop; var parse_TableStyleElement = parsenoop; var parse_NamePublish = parsenoop; -var parse_NameCmt = parsenoop; var parse_SortData = parsenoop; var parse_GUIDTypeLib = parsenoop; var parse_FnGrp12 = parsenoop; diff --git a/bits/62_fxls.js b/bits/62_fxls.js index 0b1d9dd..914c36d 100644 --- a/bits/62_fxls.js +++ b/bits/62_fxls.js @@ -769,13 +769,13 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, break; /* 2.5.198.88 */ case 'PtgRefN': - type = f[1][0]; c = shift_cell_xls(f[1][1], cell, opts); + type = f[1][0]; c = cell ? shift_cell_xls(f[1][1], cell, opts) : f[1][1]; stack.push(encode_cell_xls(c)); break; case 'PtgRef3d': // TODO: lots of stuff type = f[1][0]; ixti = /*::Number(*/f[1][1]/*::)*/; c = shift_cell_xls(f[1][2], _range, opts); - sname = (supbooks && supbooks[1] ? supbooks[1][ixti+1] : "**MISSING**"); - stack.push(sname + "!" + encode_cell(c)); + sname = supbooks.SheetNames[ixti]; + stack.push(sname + "!" + encode_cell_xls(c)); break; /* 2.5.198.62 */ @@ -831,7 +831,7 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, case 'PtgName': /* f[1] = type, 0, nameindex */ nameidx = f[1][2]; - var lbl = supbooks[0][nameidx]; + var lbl = (supbooks.names||[])[nameidx-1] || (supbooks[0]||[])[nameidx]; var name = lbl ? lbl.Name : "**MISSING**" + String(nameidx); if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name]; stack.push(name); @@ -843,15 +843,27 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, var bookidx/*:number*/ = (f[1][1]/*:any*/); nameidx = f[1][2]; var externbook; /* TODO: Properly handle missing values */ //console.log(bookidx, supbooks); - if(opts.biff == 5) { + if(opts.biff <= 5) { if(bookidx < 0) bookidx = -bookidx; if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx]; } else { - if(supbooks[bookidx+1]) externbook = supbooks[bookidx+1][nameidx]; - else if(supbooks[bookidx-1]) externbook = supbooks[bookidx-1][nameidx]; + var pnxname = supbooks.SheetNames[bookidx]; + var o = ""; + if(((supbooks[bookidx]||[])[0]||[])[0] == 0x3A01){} + else if(((supbooks[bookidx]||[])[0]||[])[0] == 0x0401){ + if(supbooks[bookidx][nameidx] && supbooks[bookidx][nameidx].itab > 0) { + o = supbooks.SheetNames[supbooks[bookidx][nameidx].itab-1] + "!"; + } + } + else o = supbooks.SheetNames[nameidx-1]+ "!"; + if(supbooks[bookidx] && supbooks[bookidx][nameidx]) o += supbooks[bookidx][nameidx].Name; + else if(supbooks[0] && supbooks[0][nameidx]) o += supbooks[0][nameidx].Name; + else o += "??NAMEX??"; + stack.push(o); + break; } - if(!externbook) externbook = {body: "??NAMEX??"}; - stack.push(externbook.body); + if(!externbook) externbook = {Name: "??NAMEX??"}; + stack.push(externbook.Name); break; /* 2.5.198.80 */ @@ -937,6 +949,9 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, /* 2.5.198.29 */ case 'PtgAreaErr': stack.push("#REF!"); break; + /* 2.5.198.30 */ + case 'PtgAreaErr3d': stack.push("#REF!"); break; + /* 2.5.198.72 TODO */ case 'PtgMemFunc': break; diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index 108c18a..7cd97b3 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -342,9 +342,9 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { var supbooks = ([[]]/*:any*/); supbooks.sharedf = shared_formulae; supbooks.arrayf = array_formulae; + supbooks.SheetNames = wb.SheetNames || wb.Sheets.map(function(x) { return x.name; }); opts.supbooks = supbooks; - - for(var i = 0; i < wb.Names['!names'].length; ++i) supbooks[0][i+1] = wb.Names[wb.Names['!names'][i]]; + for(var i = 0; i < wb.Names.length; ++i) supbooks[0][i+1] = wb.Names[i]; var colinfo = [], rowinfo = []; var defwidth = 0, defheight = 0; // twips / MDW respectively diff --git a/bits/72_wbxml.js b/bits/72_wbxml.js index a1469bd..b08a69e 100644 --- a/bits/72_wbxml.js +++ b/bits/72_wbxml.js @@ -2,7 +2,7 @@ var wbnsregex = /<\w+:workbook/; function parse_wb_xml(data, opts)/*:WorkbookFile*/ { if(!data) throw new Error("Could not find file"); - var wb = { AppVersion:{}, WBProps:{}, WBView:[], Sheets:[], CalcPr:{}, Names:{'!names':[]}, xmlns: "" }; + var wb = { AppVersion:{}, WBProps:{}, WBView:[], Sheets:[], CalcPr:{}, Names:[], xmlns: "" }; var pass = false, xmlns = "xmlns"; var dname = {}, dnstart = 0; /*(data.match(tagregex)||[]).forEach */ @@ -73,12 +73,12 @@ function parse_wb_xml(data, opts)/*:WorkbookFile*/ { dname = {}; dname.Name = y.name; if(y.comment) dname.Comment = y.comment; + if(y.localSheetId) dname.Sheet = +y.localSheetId; dnstart = idx + x.length; } break; case '': { dname.Ref = data.slice(dnstart, idx); - wb.Names[dname.Name] = dname; - wb.Names['!names'].push(dname.Name); + wb.Names.push(dname); } break; case '': break; @@ -184,7 +184,19 @@ function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { /* functionGroups */ /* externalReferences */ - /* definedNames */ + + if(wb.Workbook && (wb.Workbook.Names||[]).length > 0) { + o[o.length] = ""; + wb.Workbook.Names.forEach(function(n) { + var d = {name:n.Name}; + if(n.Comment) d.comment = n.Comment; + if(n.Sheet != null) d.localSheetId = ""+n.Sheet; + if(!n.Ref) return; + o[o.length] = writextag('definedName', String(n.Ref), d); + }); + o[o.length] = ""; + } + /* calcPr */ /* oleSize */ /* customWorkbookViews */ diff --git a/bits/73_wbbin.js b/bits/73_wbbin.js index 244305e..8a8a805 100644 --- a/bits/73_wbbin.js +++ b/bits/73_wbbin.js @@ -55,7 +55,9 @@ function parse_BrtName(data, length, opts) { // unusedstring2: XLNullableWideString //} data.l = end; - return {Name:name, Ptg:formula, Comment:comment}; + var out = ({Name:name, Ptg:formula, Comment:comment}/*:any*/); + if(itab < 0xFFFFFFF) out.Sheet = itab; + return out; } /* [MS-XLSB] 2.1.7.60 Workbook */ @@ -66,15 +68,20 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { if(!opts) opts = {}; opts.biff = 12; - var Names = {}, NameList = []; + var Names = []; + var supbooks = []; + supbooks.SheetNames = []; recordhopper(data, function hopper_wb(val, R_n, RT) { switch(RT) { case 0x009C: /* 'BrtBundleSh' */ + supbooks.SheetNames.push(val.name); wb.Sheets.push(val); break; case 0x0027: /* 'BrtName' */ - Names[val.Name] = val; NameList.push(val.Name); + val.Ref = stringify_formula(val.Ptg, null, null, supbooks, opts); + delete val.Ptg; + Names.push(val); break; case 0x040C: /* 'BrtNameExt' */ break; @@ -133,7 +140,6 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { parse_wb_defaults(wb); - Names['!names'] = NameList; // $FlowIgnore wb.Names = Names; diff --git a/bits/75_xlml.js b/bits/75_xlml.js index 0b6943c..1c995b5 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -312,7 +312,17 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty); break; - case 'NamedRange': break; + case 'NamedRange': + if(!Workbook.Names) Workbook.Names = []; + var _NamedRange = parsexmltag(Rn[0]); + var _DefinedName = { + Name: _NamedRange.Name, + Ref: rc_to_a1(_NamedRange.RefersTo.substr(1)) + }; + if(Workbook.Sheets.length>0) _DefinedName.Sheet=Workbook.Sheets.length-1; + Workbook.Names.push(_DefinedName); + break; + case 'NamedCell': break; case 'B': break; case 'I': break; @@ -796,7 +806,7 @@ function write_props_xlml(wb, opts) { /* DocumentProperties */ if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); /* CustomDocumentProperties */ - if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops)); + if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops, opts)); return o.join(""); } /* TODO */ diff --git a/bits/76_xls.js b/bits/76_xls.js index ac45635..9814d37 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -169,15 +169,17 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { var colinfo = [], rowinfo = []; var defwidth = 0, defheight = 0; // twips / MDW respectively var seencol = false; - var supbooks = ([[]]/*:any*/); // 1-indexed, will hold extern names - var sbc = 0, sbci = 0, sbcli = 0; + var supbooks = ([]/*:any*/); // 1-indexed, will hold extern names supbooks.SheetNames = opts.snames; supbooks.sharedf = opts.sharedf; supbooks.arrayf = opts.arrayf; + supbooks.names = []; + supbooks.XTI = []; var last_Rn = ''; var file_depth = 0; /* TODO: make a real stack */ var BIFF2Fmt = 0; var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ + var last_lbl; /* explicit override for some broken writers */ opts.codepage = 1200; @@ -250,18 +252,35 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'RichTextStream': break; case 'BkHim': break; - case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; - case 'ExternName': supbooks[sbc][++sbci] = val; break; + case 'SupBook': + supbooks.push([val]); + supbooks[supbooks.length-1].XTI = []; + break; + case 'ExternName': + supbooks[supbooks.length-1].push(val); + break; case 'Index': break; // TODO 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); + last_lbl = { + Name: val.Name, + Ref: stringify_formula(val.rgce,range,null,supbooks,opts) + }; + if(val.itab > 0) last_lbl.Sheet = val.itab - 1; + supbooks.names.push(last_lbl); + if(!supbooks[0]) supbooks[0] = []; + supbooks[supbooks.length-1].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 'ExternSheet': + if(supbooks.length == 0) { supbooks[0] = []; supbooks[0].XTI = []; } + supbooks[supbooks.length - 1].XTI = supbooks[supbooks.length - 1].XTI.concat(val); supbooks.XTI = supbooks.XTI.concat(val); break; + case 'NameCmt': + /* TODO: search for correct name */ + if(opts.biff < 8) break; + last_lbl.Comment = val[1]; + break; case 'Protect': out["!protect"] = val; break; /* for sheet or book */ case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break; @@ -484,7 +503,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Row': break; // TODO - case 'NameCmt': break; case 'Header': break; // TODO case 'Footer': break; // TODO case 'HCenter': break; // TODO @@ -727,6 +745,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { if(opts.enc) wb.Encryption = opts.enc; wb.Metadata = {}; if(country !== undefined) wb.Metadata.Country = country; + if(supbooks.names.length > 0) Workbook.Names = supbooks.names; wb.Workbook = Workbook; return wb; } diff --git a/bits/77_parsetab.js b/bits/77_parsetab.js index ee01f39..977e3d0 100644 --- a/bits/77_parsetab.js +++ b/bits/77_parsetab.js @@ -1103,7 +1103,7 @@ var XLSRecordEnum = { /*::[*/0x0890/*::]*/: { n:"TableStyleElement", f:parse_TableStyleElement }, /*::[*/0x0892/*::]*/: { n:"StyleExt", f:parse_StyleExt }, /*::[*/0x0893/*::]*/: { n:"NamePublish", f:parse_NamePublish }, - /*::[*/0x0894/*::]*/: { n:"NameCmt", f:parse_NameCmt }, + /*::[*/0x0894/*::]*/: { n:"NameCmt", f:parse_NameCmt, r:12 }, /*::[*/0x0895/*::]*/: { n:"SortData", f:parse_SortData }, /*::[*/0x0896/*::]*/: { n:"Theme", f:parse_Theme, r:12 }, /*::[*/0x0897/*::]*/: { n:"GUIDTypeLib", f:parse_GUIDTypeLib }, diff --git a/docbits/56_wbprops.md b/docbits/56_wbprops.md index 9ffffb3..9c7bce0 100644 --- a/docbits/56_wbprops.md +++ b/docbits/56_wbprops.md @@ -36,3 +36,4 @@ Writers will process the `Props` key of the options object: /* force the Author to be "SheetJS" */ XLSX.write(wb, {Props:{Author:"SheetJS"}}); ``` + diff --git a/docbits/57_wbbook.md b/docbits/57_wbbook.md new file mode 100644 index 0000000..0c4e81c --- /dev/null +++ b/docbits/57_wbbook.md @@ -0,0 +1,19 @@ +### Workbook-Level Attributes + +`wb.Workbook` stores workbook level attributes. + +#### Defined Names + +`wb.Workbook.Names` is an array of defined name objects which have the keys: + +| Key | Description | +|:----------|:-----------------------------------------------------------------| +| `Sheet` | Name scope. Sheet Index (0 = first sheet) or `null` (Workbook) | +| `Name` | Case-sensitive name. Standard rules apply ** | +| `Ref` | A1-style Reference (e.g. `"Sheet1!$A$1:$D$20"`) | +| `Comment` | Comment (only applicable for XLS/XLSX/XLSB) | + +Excel allows two sheet-scoped defined names to share the same name. However, a +sheet-scoped name cannot collide with a workbook-scope name. Workbook writers +may not enforce this constraint. + diff --git a/misc/docs/SUMMARY.md b/misc/docs/SUMMARY.md index fbc9c01..740ceb4 100644 --- a/misc/docs/SUMMARY.md +++ b/misc/docs/SUMMARY.md @@ -22,6 +22,8 @@ + [Chartsheet Object](README.md#chartsheet-object) * [Workbook Object](README.md#workbook-object) + [Workbook File Properties](README.md#workbook-file-properties) + * [Workbook-Level Attributes](README.md#workbook-level-attributes) + + [Defined Names](README.md#defined-names) * [Document Features](README.md#document-features) + [Formulae](README.md#formulae) + [Column Properties](README.md#column-properties) diff --git a/multiformat.lst b/multiformat.lst index 64d2900..1f2790c 100644 --- a/multiformat.lst +++ b/multiformat.lst @@ -1,7 +1,10 @@ # This file controls the multiformat tests +# vim: set ts=4: # Format: [ext..] +# yes-formula AutoFilter .xls .xlsb .xlsx .xml -#BlankSheetTypes .xls .xlsb .xlsx .xml +# note: XLML only supports sheets, ods does not support dialog +BlankSheetTypes .xls .xlsb .xlsm NumberFormatCondition .xls .xlsb .xlsm .xml RkNumber .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml #calendar_stress_test .xls .xlsb .xlsx .xml @@ -10,17 +13,15 @@ cell_style_simple .xls .xlsb .xlsx .xml comments_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml # yes-csv custom_properties .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml -# no-formula (defined names) defined_names_simple .xls .xlsb .xlsx .xml -# yes-formula # no-csv (randbetween) note: ODS does not support many XLSX functions -formula_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml +formula_stress_test .xls .xlsb .xlsx .xlsb.xml # yes-csv formulae_test_simple .xls .xlsb .xlsx .xml hyperlink_stress_test_2011 .xls .xlsb .xlsx .xml #large_strings .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml merge_cells .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml -# no-formula (defined names) +# no-formula (filename-references in XLSX encoding as [0]) named_ranges_2011 .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml # yes-formula # no-csv (macro serialization in xml) diff --git a/test.js b/test.js index e749cf7..3a904da 100644 --- a/test.js +++ b/test.js @@ -50,6 +50,10 @@ var paths = { cstxlsx: dir + 'comments_stress_test.xlsx', cstxlsb: dir + 'comments_stress_test.xlsb', cstods: dir + 'comments_stress_test.ods', + dnsxls: dir + 'defined_names_simple.xls', + dnsxml: dir + 'defined_names_simple.xml', + dnsxlsx: dir + 'defined_names_simple.xlsx', + dnsxlsb: dir + 'defined_names_simple.xlsb', fstxls: dir + 'formula_stress_test.xls', fstxml: dir + 'formula_stress_test.xls.xml', fstxlsx: dir + 'formula_stress_test.xlsx', @@ -893,6 +897,29 @@ describe('parse features', function() { }); }); }); + describe('defined names', function() { + [ + /* desc path cmnt */ + ['xlsx', paths.dnsxlsx, true], + ['xlsb', paths.dnsxlsb, true], + ['xls', paths.dnsxls, true], + ['xlml', paths.dnsxml, false], + ].forEach(function(m) { it(m[0], function() { + var wb = X.readFile(m[1]); + var names = wb.Workbook.Names; + for(var i = 0; i < names.length; ++i) if(names[i].Name == "SheetJS") break; + assert(i < names.length, "Missing name"); + assert.equal(names[i].Sheet, null); + assert.equal(names[i].Ref, "Sheet1!$A$1"); + if(m[2]) assert.equal(names[i].Comment, "defined names just suck excel formulae are bad MS should feel bad"); + + for(i = 0; i < names.length; ++i) if(names[i].Name == "SHEETjs") break; + assert(i < names.length, "Missing name"); + assert.equal(names[i].Sheet, 0); + assert.equal(names[i].Ref, "Sheet1!$A$2"); + }); }); + }); + describe('auto filter', function() { [ ['xlsx', paths.afxlsx], diff --git a/xlsx.flow.js b/xlsx.flow.js index 166ec95..3272cba 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -3273,7 +3273,7 @@ function xlml_write_docprops(Props, opts) { case 'date': m = new Date(m).toISOString(); break; } if(typeof m == 'number') m = String(m); - else if(m === true || m === false) { t = "boolean"; m = m ? "1" : "0"; } + else if(m === true || m === false) { m = m ? "1" : "0"; } else if(m instanceof Date) m = new Date(m).toISOString(); o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); }); @@ -3590,13 +3590,6 @@ function parslurp(blob, length, cb) { return arr; } -function parslurp2(blob, length, cb) { - var arr = [], target = blob.l + length, len = blob.read_shift(2); - while(len-- !== 0) arr.push(cb(blob, target - blob.l)); - if(target !== blob.l) throw new Error("Slurp error"); - return arr; -} - function parsebool(blob, length) { return blob.read_shift(length) === 0x1; } function parseuint16(blob) { return blob.read_shift(2, 'u'); } @@ -4200,6 +4193,7 @@ function parse_ExternName(blob, length, opts) { if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts); //else throw new Error("unsupported SupBook cch: " + opts.sbcch); o.body = body || blob.read_shift(length-2); + if(typeof body === "string") o.Name = body; return o; } @@ -4230,13 +4224,21 @@ function parse_Lbl(blob, length, opts) { /* 2.4.106 TODO: verify supbook manipulation */ function parse_ExternSheet(blob, length, opts) { if(opts.biff < 8) return parse_ShortXLUnicodeString(blob, length, opts); - var o = parslurp2(blob,length,parse_XTI); + var o = [], target = blob.l + length, len = blob.read_shift(2); + while(len-- !== 0) o.push(parse_XTI(blob, 6)); + // [iSupBook, itabFirst, itabLast]; var oo = []; - if(opts.sbcch === 0x0401) { - for(var i = 0; i != o.length; ++i) oo.push(opts.snames[o[i][1]]); - return oo; - } - else return o; + return o; +} + +/* 2.4.176 TODO: check older biff */ +function parse_NameCmt(blob, length, opts) { + if(opts.biff < 8) { blob.l += length; return; } + var cchName = blob.read_shift(2); + var cchComment = blob.read_shift(2); + var name = parse_XLUnicodeStringNoCch(blob, cchName, opts); + var comment = parse_XLUnicodeStringNoCch(blob, cchComment, opts); + return [name, comment]; } /* 2.4.260 */ @@ -4652,7 +4654,6 @@ var parse_TableStyles = parsenoop; var parse_TableStyle = parsenoop; var parse_TableStyleElement = parsenoop; var parse_NamePublish = parsenoop; -var parse_NameCmt = parsenoop; var parse_SortData = parsenoop; var parse_GUIDTypeLib = parsenoop; var parse_FnGrp12 = parsenoop; @@ -8130,13 +8131,13 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, break; /* 2.5.198.88 */ case 'PtgRefN': - type = f[1][0]; c = shift_cell_xls(f[1][1], cell, opts); + type = f[1][0]; c = cell ? shift_cell_xls(f[1][1], cell, opts) : f[1][1]; stack.push(encode_cell_xls(c)); break; case 'PtgRef3d': // TODO: lots of stuff type = f[1][0]; ixti = /*::Number(*/f[1][1]/*::)*/; c = shift_cell_xls(f[1][2], _range, opts); - sname = (supbooks && supbooks[1] ? supbooks[1][ixti+1] : "**MISSING**"); - stack.push(sname + "!" + encode_cell(c)); + sname = supbooks.SheetNames[ixti]; + stack.push(sname + "!" + encode_cell_xls(c)); break; /* 2.5.198.62 */ @@ -8192,7 +8193,7 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, case 'PtgName': /* f[1] = type, 0, nameindex */ nameidx = f[1][2]; - var lbl = supbooks[0][nameidx]; + var lbl = (supbooks.names||[])[nameidx-1] || (supbooks[0]||[])[nameidx]; var name = lbl ? lbl.Name : "**MISSING**" + String(nameidx); if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name]; stack.push(name); @@ -8204,15 +8205,27 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, var bookidx/*:number*/ = (f[1][1]/*:any*/); nameidx = f[1][2]; var externbook; /* TODO: Properly handle missing values */ //console.log(bookidx, supbooks); - if(opts.biff == 5) { + if(opts.biff <= 5) { if(bookidx < 0) bookidx = -bookidx; if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx]; } else { - if(supbooks[bookidx+1]) externbook = supbooks[bookidx+1][nameidx]; - else if(supbooks[bookidx-1]) externbook = supbooks[bookidx-1][nameidx]; + var pnxname = supbooks.SheetNames[bookidx]; + var o = ""; + if(((supbooks[bookidx]||[])[0]||[])[0] == 0x3A01){} + else if(((supbooks[bookidx]||[])[0]||[])[0] == 0x0401){ + if(supbooks[bookidx][nameidx] && supbooks[bookidx][nameidx].itab > 0) { + o = supbooks.SheetNames[supbooks[bookidx][nameidx].itab-1] + "!"; + } + } + else o = supbooks.SheetNames[nameidx-1]+ "!"; + if(supbooks[bookidx] && supbooks[bookidx][nameidx]) o += supbooks[bookidx][nameidx].Name; + else if(supbooks[0] && supbooks[0][nameidx]) o += supbooks[0][nameidx].Name; + else o += "??NAMEX??"; + stack.push(o); + break; } - if(!externbook) externbook = {body: "??NAMEX??"}; - stack.push(externbook.body); + if(!externbook) externbook = {Name: "??NAMEX??"}; + stack.push(externbook.Name); break; /* 2.5.198.80 */ @@ -8298,6 +8311,9 @@ function stringify_formula(formula/*Array*/, range, cell/*:any*/, supbooks, /* 2.5.198.29 */ case 'PtgAreaErr': stack.push("#REF!"); break; + /* 2.5.198.30 */ + case 'PtgAreaErr3d': stack.push("#REF!"); break; + /* 2.5.198.72 TODO */ case 'PtgMemFunc': break; @@ -10528,9 +10544,9 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ { var supbooks = ([[]]/*:any*/); supbooks.sharedf = shared_formulae; supbooks.arrayf = array_formulae; + supbooks.SheetNames = wb.SheetNames || wb.Sheets.map(function(x) { return x.name; }); opts.supbooks = supbooks; - - for(var i = 0; i < wb.Names['!names'].length; ++i) supbooks[0][i+1] = wb.Names[wb.Names['!names'][i]]; + for(var i = 0; i < wb.Names.length; ++i) supbooks[0][i+1] = wb.Names[i]; var colinfo = [], rowinfo = []; var defwidth = 0, defheight = 0; // twips / MDW respectively @@ -11161,7 +11177,7 @@ function check_wb(wb) { var wbnsregex = /<\w+:workbook/; function parse_wb_xml(data, opts)/*:WorkbookFile*/ { if(!data) throw new Error("Could not find file"); - var wb = { AppVersion:{}, WBProps:{}, WBView:[], Sheets:[], CalcPr:{}, Names:{'!names':[]}, xmlns: "" }; + var wb = { AppVersion:{}, WBProps:{}, WBView:[], Sheets:[], CalcPr:{}, Names:[], xmlns: "" }; var pass = false, xmlns = "xmlns"; var dname = {}, dnstart = 0; /*(data.match(tagregex)||[]).forEach */ @@ -11232,12 +11248,12 @@ function parse_wb_xml(data, opts)/*:WorkbookFile*/ { dname = {}; dname.Name = y.name; if(y.comment) dname.Comment = y.comment; + if(y.localSheetId) dname.Sheet = +y.localSheetId; dnstart = idx + x.length; } break; case '': { dname.Ref = data.slice(dnstart, idx); - wb.Names[dname.Name] = dname; - wb.Names['!names'].push(dname.Name); + wb.Names.push(dname); } break; case '': break; @@ -11343,7 +11359,19 @@ function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ { /* functionGroups */ /* externalReferences */ - /* definedNames */ + + if(wb.Workbook && (wb.Workbook.Names||[]).length > 0) { + o[o.length] = ""; + wb.Workbook.Names.forEach(function(n) { + var d = {name:n.Name}; + if(n.Comment) d.comment = n.Comment; + if(n.Sheet != null) d.localSheetId = ""+n.Sheet; + if(!n.Ref) return; + o[o.length] = writextag('definedName', String(n.Ref), d); + }); + o[o.length] = ""; + } + /* calcPr */ /* oleSize */ /* customWorkbookViews */ @@ -11415,7 +11443,9 @@ function parse_BrtName(data, length, opts) { // unusedstring2: XLNullableWideString //} data.l = end; - return {Name:name, Ptg:formula, Comment:comment}; + var out = ({Name:name, Ptg:formula, Comment:comment}/*:any*/); + if(itab < 0xFFFFFFF) out.Sheet = itab; + return out; } /* [MS-XLSB] 2.1.7.60 Workbook */ @@ -11426,15 +11456,20 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { if(!opts) opts = {}; opts.biff = 12; - var Names = {}, NameList = []; + var Names = []; + var supbooks = []; + supbooks.SheetNames = []; recordhopper(data, function hopper_wb(val, R_n, RT) { switch(RT) { case 0x009C: /* 'BrtBundleSh' */ + supbooks.SheetNames.push(val.name); wb.Sheets.push(val); break; case 0x0027: /* 'BrtName' */ - Names[val.Name] = val; NameList.push(val.Name); + val.Ref = stringify_formula(val.Ptg, null, null, supbooks, opts); + delete val.Ptg; + Names.push(val); break; case 0x040C: /* 'BrtNameExt' */ break; @@ -11493,7 +11528,6 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ { parse_wb_defaults(wb); - Names['!names'] = NameList; // $FlowIgnore wb.Names = Names; @@ -11995,7 +12029,17 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ { for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty); break; - case 'NamedRange': break; + case 'NamedRange': + if(!Workbook.Names) Workbook.Names = []; + var _NamedRange = parsexmltag(Rn[0]); + var _DefinedName = { + Name: _NamedRange.Name, + Ref: rc_to_a1(_NamedRange.RefersTo.substr(1)) + }; + if(Workbook.Sheets.length>0) _DefinedName.Sheet=Workbook.Sheets.length-1; + Workbook.Names.push(_DefinedName); + break; + case 'NamedCell': break; case 'B': break; case 'I': break; @@ -12479,7 +12523,7 @@ function write_props_xlml(wb, opts) { /* DocumentProperties */ if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); /* CustomDocumentProperties */ - if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops)); + if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops, opts)); return o.join(""); } /* TODO */ @@ -12797,15 +12841,17 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { var colinfo = [], rowinfo = []; var defwidth = 0, defheight = 0; // twips / MDW respectively var seencol = false; - var supbooks = ([[]]/*:any*/); // 1-indexed, will hold extern names - var sbc = 0, sbci = 0, sbcli = 0; + var supbooks = ([]/*:any*/); // 1-indexed, will hold extern names supbooks.SheetNames = opts.snames; supbooks.sharedf = opts.sharedf; supbooks.arrayf = opts.arrayf; + supbooks.names = []; + supbooks.XTI = []; var last_Rn = ''; var file_depth = 0; /* TODO: make a real stack */ var BIFF2Fmt = 0; var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ + var last_lbl; /* explicit override for some broken writers */ opts.codepage = 1200; @@ -12878,18 +12924,35 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { case 'RichTextStream': break; case 'BkHim': break; - case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; - case 'ExternName': supbooks[sbc][++sbci] = val; break; + case 'SupBook': + supbooks.push([val]); + supbooks[supbooks.length-1].XTI = []; + break; + case 'ExternName': + supbooks[supbooks.length-1].push(val); + break; case 'Index': break; // TODO 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); + last_lbl = { + Name: val.Name, + Ref: stringify_formula(val.rgce,range,null,supbooks,opts) + }; + if(val.itab > 0) last_lbl.Sheet = val.itab - 1; + supbooks.names.push(last_lbl); + if(!supbooks[0]) supbooks[0] = []; + supbooks[supbooks.length-1].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 'ExternSheet': + if(supbooks.length == 0) { supbooks[0] = []; supbooks[0].XTI = []; } + supbooks[supbooks.length - 1].XTI = supbooks[supbooks.length - 1].XTI.concat(val); supbooks.XTI = supbooks.XTI.concat(val); break; + case 'NameCmt': + /* TODO: search for correct name */ + if(opts.biff < 8) break; + last_lbl.Comment = val[1]; + break; case 'Protect': out["!protect"] = val; break; /* for sheet or book */ case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break; @@ -13112,7 +13175,6 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Row': break; // TODO - case 'NameCmt': break; case 'Header': break; // TODO case 'Footer': break; // TODO case 'HCenter': break; // TODO @@ -13355,6 +13417,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { if(opts.enc) wb.Encryption = opts.enc; wb.Metadata = {}; if(country !== undefined) wb.Metadata.Country = country; + if(supbooks.names.length > 0) Workbook.Names = supbooks.names; wb.Workbook = Workbook; return wb; } @@ -14515,7 +14578,7 @@ var XLSRecordEnum = { /*::[*/0x0890/*::]*/: { n:"TableStyleElement", f:parse_TableStyleElement }, /*::[*/0x0892/*::]*/: { n:"StyleExt", f:parse_StyleExt }, /*::[*/0x0893/*::]*/: { n:"NamePublish", f:parse_NamePublish }, - /*::[*/0x0894/*::]*/: { n:"NameCmt", f:parse_NameCmt }, + /*::[*/0x0894/*::]*/: { n:"NameCmt", f:parse_NameCmt, r:12 }, /*::[*/0x0895/*::]*/: { n:"SortData", f:parse_SortData }, /*::[*/0x0896/*::]*/: { n:"Theme", f:parse_Theme, r:12 }, /*::[*/0x0897/*::]*/: { n:"GUIDTypeLib", f:parse_GUIDTypeLib }, diff --git a/xlsx.js b/xlsx.js index f4ca499..c7916f8 100644 --- a/xlsx.js +++ b/xlsx.js @@ -3219,7 +3219,7 @@ function xlml_write_docprops(Props, opts) { case 'date': m = new Date(m).toISOString(); break; } if(typeof m == 'number') m = String(m); - else if(m === true || m === false) { t = "boolean"; m = m ? "1" : "0"; } + else if(m === true || m === false) { m = m ? "1" : "0"; } else if(m instanceof Date) m = new Date(m).toISOString(); o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); }); @@ -3534,13 +3534,6 @@ function parslurp(blob, length, cb) { return arr; } -function parslurp2(blob, length, cb) { - var arr = [], target = blob.l + length, len = blob.read_shift(2); - while(len-- !== 0) arr.push(cb(blob, target - blob.l)); - if(target !== blob.l) throw new Error("Slurp error"); - return arr; -} - function parsebool(blob, length) { return blob.read_shift(length) === 0x1; } function parseuint16(blob) { return blob.read_shift(2, 'u'); } @@ -4144,6 +4137,7 @@ function parse_ExternName(blob, length, opts) { if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts); //else throw new Error("unsupported SupBook cch: " + opts.sbcch); o.body = body || blob.read_shift(length-2); + if(typeof body === "string") o.Name = body; return o; } @@ -4174,13 +4168,21 @@ function parse_Lbl(blob, length, opts) { /* 2.4.106 TODO: verify supbook manipulation */ function parse_ExternSheet(blob, length, opts) { if(opts.biff < 8) return parse_ShortXLUnicodeString(blob, length, opts); - var o = parslurp2(blob,length,parse_XTI); + var o = [], target = blob.l + length, len = blob.read_shift(2); + while(len-- !== 0) o.push(parse_XTI(blob, 6)); + // [iSupBook, itabFirst, itabLast]; var oo = []; - if(opts.sbcch === 0x0401) { - for(var i = 0; i != o.length; ++i) oo.push(opts.snames[o[i][1]]); - return oo; - } - else return o; + return o; +} + +/* 2.4.176 TODO: check older biff */ +function parse_NameCmt(blob, length, opts) { + if(opts.biff < 8) { blob.l += length; return; } + var cchName = blob.read_shift(2); + var cchComment = blob.read_shift(2); + var name = parse_XLUnicodeStringNoCch(blob, cchName, opts); + var comment = parse_XLUnicodeStringNoCch(blob, cchComment, opts); + return [name, comment]; } /* 2.4.260 */ @@ -4596,7 +4598,6 @@ var parse_TableStyles = parsenoop; var parse_TableStyle = parsenoop; var parse_TableStyleElement = parsenoop; var parse_NamePublish = parsenoop; -var parse_NameCmt = parsenoop; var parse_SortData = parsenoop; var parse_GUIDTypeLib = parsenoop; var parse_FnGrp12 = parsenoop; @@ -8073,13 +8074,13 @@ function stringify_formula(formula/*Array*/, range, cell, supbooks, opts) { break; /* 2.5.198.88 */ case 'PtgRefN': - type = f[1][0]; c = shift_cell_xls(f[1][1], cell, opts); + type = f[1][0]; c = cell ? shift_cell_xls(f[1][1], cell, opts) : f[1][1]; stack.push(encode_cell_xls(c)); break; case 'PtgRef3d': // TODO: lots of stuff type = f[1][0]; ixti = f[1][1]; c = shift_cell_xls(f[1][2], _range, opts); - sname = (supbooks && supbooks[1] ? supbooks[1][ixti+1] : "**MISSING**"); - stack.push(sname + "!" + encode_cell(c)); + sname = supbooks.SheetNames[ixti]; + stack.push(sname + "!" + encode_cell_xls(c)); break; /* 2.5.198.62 */ @@ -8135,7 +8136,7 @@ function stringify_formula(formula/*Array*/, range, cell, supbooks, opts) { case 'PtgName': /* f[1] = type, 0, nameindex */ nameidx = f[1][2]; - var lbl = supbooks[0][nameidx]; + var lbl = (supbooks.names||[])[nameidx-1] || (supbooks[0]||[])[nameidx]; var name = lbl ? lbl.Name : "**MISSING**" + String(nameidx); if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name]; stack.push(name); @@ -8147,15 +8148,27 @@ function stringify_formula(formula/*Array*/, range, cell, supbooks, opts) { var bookidx = (f[1][1]); nameidx = f[1][2]; var externbook; /* TODO: Properly handle missing values */ //console.log(bookidx, supbooks); - if(opts.biff == 5) { + if(opts.biff <= 5) { if(bookidx < 0) bookidx = -bookidx; if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx]; } else { - if(supbooks[bookidx+1]) externbook = supbooks[bookidx+1][nameidx]; - else if(supbooks[bookidx-1]) externbook = supbooks[bookidx-1][nameidx]; + var pnxname = supbooks.SheetNames[bookidx]; + var o = ""; + if(((supbooks[bookidx]||[])[0]||[])[0] == 0x3A01){} + else if(((supbooks[bookidx]||[])[0]||[])[0] == 0x0401){ + if(supbooks[bookidx][nameidx] && supbooks[bookidx][nameidx].itab > 0) { + o = supbooks.SheetNames[supbooks[bookidx][nameidx].itab-1] + "!"; + } + } + else o = supbooks.SheetNames[nameidx-1]+ "!"; + if(supbooks[bookidx] && supbooks[bookidx][nameidx]) o += supbooks[bookidx][nameidx].Name; + else if(supbooks[0] && supbooks[0][nameidx]) o += supbooks[0][nameidx].Name; + else o += "??NAMEX??"; + stack.push(o); + break; } - if(!externbook) externbook = {body: "??NAMEX??"}; - stack.push(externbook.body); + if(!externbook) externbook = {Name: "??NAMEX??"}; + stack.push(externbook.Name); break; /* 2.5.198.80 */ @@ -8241,6 +8254,9 @@ function stringify_formula(formula/*Array*/, range, cell, supbooks, opts) { /* 2.5.198.29 */ case 'PtgAreaErr': stack.push("#REF!"); break; + /* 2.5.198.30 */ + case 'PtgAreaErr3d': stack.push("#REF!"); break; + /* 2.5.198.72 TODO */ case 'PtgMemFunc': break; @@ -10471,9 +10487,9 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { var supbooks = ([[]]); supbooks.sharedf = shared_formulae; supbooks.arrayf = array_formulae; + supbooks.SheetNames = wb.SheetNames || wb.Sheets.map(function(x) { return x.name; }); opts.supbooks = supbooks; - - for(var i = 0; i < wb.Names['!names'].length; ++i) supbooks[0][i+1] = wb.Names[wb.Names['!names'][i]]; + for(var i = 0; i < wb.Names.length; ++i) supbooks[0][i+1] = wb.Names[i]; var colinfo = [], rowinfo = []; var defwidth = 0, defheight = 0; // twips / MDW respectively @@ -11104,7 +11120,7 @@ function check_wb(wb) { var wbnsregex = /<\w+:workbook/; function parse_wb_xml(data, opts) { if(!data) throw new Error("Could not find file"); - var wb = { AppVersion:{}, WBProps:{}, WBView:[], Sheets:[], CalcPr:{}, Names:{'!names':[]}, xmlns: "" }; + var wb = { AppVersion:{}, WBProps:{}, WBView:[], Sheets:[], CalcPr:{}, Names:[], xmlns: "" }; var pass = false, xmlns = "xmlns"; var dname = {}, dnstart = 0; /*(data.match(tagregex)||[]).forEach */ @@ -11175,12 +11191,12 @@ function parse_wb_xml(data, opts) { dname = {}; dname.Name = y.name; if(y.comment) dname.Comment = y.comment; + if(y.localSheetId) dname.Sheet = +y.localSheetId; dnstart = idx + x.length; } break; case '': { dname.Ref = data.slice(dnstart, idx); - wb.Names[dname.Name] = dname; - wb.Names['!names'].push(dname.Name); + wb.Names.push(dname); } break; case '': break; @@ -11286,7 +11302,19 @@ function write_wb_xml(wb, opts) { /* functionGroups */ /* externalReferences */ - /* definedNames */ + + if(wb.Workbook && (wb.Workbook.Names||[]).length > 0) { + o[o.length] = ""; + wb.Workbook.Names.forEach(function(n) { + var d = {name:n.Name}; + if(n.Comment) d.comment = n.Comment; + if(n.Sheet != null) d.localSheetId = ""+n.Sheet; + if(!n.Ref) return; + o[o.length] = writextag('definedName', String(n.Ref), d); + }); + o[o.length] = ""; + } + /* calcPr */ /* oleSize */ /* customWorkbookViews */ @@ -11358,7 +11386,9 @@ function parse_BrtName(data, length, opts) { // unusedstring2: XLNullableWideString //} data.l = end; - return {Name:name, Ptg:formula, Comment:comment}; + var out = ({Name:name, Ptg:formula, Comment:comment}); + if(itab < 0xFFFFFFF) out.Sheet = itab; + return out; } /* [MS-XLSB] 2.1.7.60 Workbook */ @@ -11369,15 +11399,20 @@ function parse_wb_bin(data, opts) { if(!opts) opts = {}; opts.biff = 12; - var Names = {}, NameList = []; + var Names = []; + var supbooks = []; + supbooks.SheetNames = []; recordhopper(data, function hopper_wb(val, R_n, RT) { switch(RT) { case 0x009C: /* 'BrtBundleSh' */ + supbooks.SheetNames.push(val.name); wb.Sheets.push(val); break; case 0x0027: /* 'BrtName' */ - Names[val.Name] = val; NameList.push(val.Name); + val.Ref = stringify_formula(val.Ptg, null, null, supbooks, opts); + delete val.Ptg; + Names.push(val); break; case 0x040C: /* 'BrtNameExt' */ break; @@ -11436,7 +11471,6 @@ function parse_wb_bin(data, opts) { parse_wb_defaults(wb); - Names['!names'] = NameList; // $FlowIgnore wb.Names = Names; @@ -11935,7 +11969,17 @@ for(var cma = c; cma <= cc; ++cma) { for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty); break; - case 'NamedRange': break; + case 'NamedRange': + if(!Workbook.Names) Workbook.Names = []; + var _NamedRange = parsexmltag(Rn[0]); + var _DefinedName = { + Name: _NamedRange.Name, + Ref: rc_to_a1(_NamedRange.RefersTo.substr(1)) + }; + if(Workbook.Sheets.length>0) _DefinedName.Sheet=Workbook.Sheets.length-1; + Workbook.Names.push(_DefinedName); + break; + case 'NamedCell': break; case 'B': break; case 'I': break; @@ -12418,7 +12462,7 @@ function write_props_xlml(wb, opts) { /* DocumentProperties */ if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); /* CustomDocumentProperties */ - if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops)); + if(wb.Custprops) o.push(xlml_write_custprops(wb.Props, wb.Custprops, opts)); return o.join(""); } /* TODO */ @@ -12736,15 +12780,17 @@ function parse_workbook(blob, options) { var colinfo = [], rowinfo = []; var defwidth = 0, defheight = 0; // twips / MDW respectively var seencol = false; - var supbooks = ([[]]); // 1-indexed, will hold extern names - var sbc = 0, sbci = 0, sbcli = 0; + var supbooks = ([]); // 1-indexed, will hold extern names supbooks.SheetNames = opts.snames; supbooks.sharedf = opts.sharedf; supbooks.arrayf = opts.arrayf; + supbooks.names = []; + supbooks.XTI = []; var last_Rn = ''; var file_depth = 0; /* TODO: make a real stack */ var BIFF2Fmt = 0; var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ + var last_lbl; /* explicit override for some broken writers */ opts.codepage = 1200; @@ -12817,18 +12863,35 @@ function parse_workbook(blob, options) { case 'RichTextStream': break; case 'BkHim': break; - case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; - case 'ExternName': supbooks[sbc][++sbci] = val; break; + case 'SupBook': + supbooks.push([val]); + supbooks[supbooks.length-1].XTI = []; + break; + case 'ExternName': + supbooks[supbooks.length-1].push(val); + break; case 'Index': break; // TODO 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); + last_lbl = { + Name: val.Name, + Ref: stringify_formula(val.rgce,range,null,supbooks,opts) + }; + if(val.itab > 0) last_lbl.Sheet = val.itab - 1; + supbooks.names.push(last_lbl); + if(!supbooks[0]) supbooks[0] = []; + supbooks[supbooks.length-1].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 'ExternSheet': + if(supbooks.length == 0) { supbooks[0] = []; supbooks[0].XTI = []; } + supbooks[supbooks.length - 1].XTI = supbooks[supbooks.length - 1].XTI.concat(val); supbooks.XTI = supbooks.XTI.concat(val); break; + case 'NameCmt': + /* TODO: search for correct name */ + if(opts.biff < 8) break; + last_lbl.Comment = val[1]; + break; case 'Protect': out["!protect"] = val; break; /* for sheet or book */ case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break; @@ -13051,7 +13114,6 @@ function parse_workbook(blob, options) { } break; case 'Row': break; // TODO - case 'NameCmt': break; case 'Header': break; // TODO case 'Footer': break; // TODO case 'HCenter': break; // TODO @@ -13294,6 +13356,7 @@ function parse_workbook(blob, options) { if(opts.enc) wb.Encryption = opts.enc; wb.Metadata = {}; if(country !== undefined) wb.Metadata.Country = country; + if(supbooks.names.length > 0) Workbook.Names = supbooks.names; wb.Workbook = Workbook; return wb; } @@ -14454,7 +14517,7 @@ var XLSRecordEnum = { 0x0890: { n:"TableStyleElement", f:parse_TableStyleElement }, 0x0892: { n:"StyleExt", f:parse_StyleExt }, 0x0893: { n:"NamePublish", f:parse_NamePublish }, -0x0894: { n:"NameCmt", f:parse_NameCmt }, +0x0894: { n:"NameCmt", f:parse_NameCmt, r:12 }, 0x0895: { n:"SortData", f:parse_SortData }, 0x0896: { n:"Theme", f:parse_Theme, r:12 }, 0x0897: { n:"GUIDTypeLib", f:parse_GUIDTypeLib },