Defined Names

- XLSX read/write defined names
- XLSB/XLS/XLML read defined names

Issues:
- fixes  h/t @developergdd
- fixes  , fixes 
This commit is contained in:
SheetJS 2017-04-11 18:15:36 -04:00
parent 5187bc0b63
commit 0189bc23ca
18 changed files with 404 additions and 143 deletions

@ -45,6 +45,8 @@ with a unified JS representation, and ES3/ES5 browser compatibility back to IE6.
+ [Chartsheet Object](#chartsheet-object) + [Chartsheet Object](#chartsheet-object)
* [Workbook Object](#workbook-object) * [Workbook Object](#workbook-object)
+ [Workbook File Properties](#workbook-file-properties) + [Workbook File Properties](#workbook-file-properties)
* [Workbook-Level Attributes](#workbook-level-attributes)
+ [Defined Names](#defined-names)
* [Document Features](#document-features) * [Document Features](#document-features)
+ [Formulae](#formulae) + [Formulae](#formulae)
+ [Column Properties](#column-properties) + [Column Properties](#column-properties)
@ -669,6 +671,26 @@ Writers will process the `Props` key of the options object:
/* force the Author to be "SheetJS" */ /* force the Author to be "SheetJS" */
XLSX.write(wb, {Props:{Author:"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 ### Document Features
Even for basic features like date storage, the official Excel formats store the Even for basic features like date storage, the official Excel formats store the

@ -39,7 +39,7 @@ function xlml_write_docprops(Props, opts) {
case 'date': m = new Date(m).toISOString(); break; case 'date': m = new Date(m).toISOString(); break;
} }
if(typeof m == 'number') m = String(m); 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(); else if(m instanceof Date) m = new Date(m).toISOString();
o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m));
}); });

@ -278,13 +278,6 @@ function parslurp(blob, length, cb) {
return arr; 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 parsebool(blob, length) { return blob.read_shift(length) === 0x1; }
function parseuint16(blob) { return blob.read_shift(2, 'u'); } function parseuint16(blob) { return blob.read_shift(2, 'u'); }

@ -442,6 +442,7 @@ function parse_ExternName(blob, length, opts) {
if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts); if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts);
//else throw new Error("unsupported SupBook cch: " + opts.sbcch); //else throw new Error("unsupported SupBook cch: " + opts.sbcch);
o.body = body || blob.read_shift(length-2); o.body = body || blob.read_shift(length-2);
if(typeof body === "string") o.Name = body;
return o; return o;
} }
@ -472,13 +473,21 @@ function parse_Lbl(blob, length, opts) {
/* 2.4.106 TODO: verify supbook manipulation */ /* 2.4.106 TODO: verify supbook manipulation */
function parse_ExternSheet(blob, length, opts) { function parse_ExternSheet(blob, length, opts) {
if(opts.biff < 8) return parse_ShortXLUnicodeString(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 = []; var oo = [];
if(opts.sbcch === 0x0401) { return o;
for(var i = 0; i != o.length; ++i) oo.push(opts.snames[o[i][1]]); }
return oo;
} /* 2.4.176 TODO: check older biff */
else return o; 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 */ /* 2.4.260 */
@ -894,7 +903,6 @@ var parse_TableStyles = parsenoop;
var parse_TableStyle = parsenoop; var parse_TableStyle = parsenoop;
var parse_TableStyleElement = parsenoop; var parse_TableStyleElement = parsenoop;
var parse_NamePublish = parsenoop; var parse_NamePublish = parsenoop;
var parse_NameCmt = parsenoop;
var parse_SortData = parsenoop; var parse_SortData = parsenoop;
var parse_GUIDTypeLib = parsenoop; var parse_GUIDTypeLib = parsenoop;
var parse_FnGrp12 = parsenoop; var parse_FnGrp12 = parsenoop;

@ -769,13 +769,13 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
break; break;
/* 2.5.198.88 */ /* 2.5.198.88 */
case 'PtgRefN': 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)); stack.push(encode_cell_xls(c));
break; break;
case 'PtgRef3d': // TODO: lots of stuff case 'PtgRef3d': // TODO: lots of stuff
type = f[1][0]; ixti = /*::Number(*/f[1][1]/*::)*/; c = shift_cell_xls(f[1][2], _range, opts); 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**"); sname = supbooks.SheetNames[ixti];
stack.push(sname + "!" + encode_cell(c)); stack.push(sname + "!" + encode_cell_xls(c));
break; break;
/* 2.5.198.62 */ /* 2.5.198.62 */
@ -831,7 +831,7 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
case 'PtgName': case 'PtgName':
/* f[1] = type, 0, nameindex */ /* f[1] = type, 0, nameindex */
nameidx = f[1][2]; 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); var name = lbl ? lbl.Name : "**MISSING**" + String(nameidx);
if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name]; if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name];
stack.push(name); stack.push(name);
@ -843,15 +843,27 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
var bookidx/*:number*/ = (f[1][1]/*:any*/); nameidx = f[1][2]; var externbook; var bookidx/*:number*/ = (f[1][1]/*:any*/); nameidx = f[1][2]; var externbook;
/* TODO: Properly handle missing values */ /* TODO: Properly handle missing values */
//console.log(bookidx, supbooks); //console.log(bookidx, supbooks);
if(opts.biff == 5) { if(opts.biff <= 5) {
if(bookidx < 0) bookidx = -bookidx; if(bookidx < 0) bookidx = -bookidx;
if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx]; if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx];
} else { } else {
if(supbooks[bookidx+1]) externbook = supbooks[bookidx+1][nameidx]; var pnxname = supbooks.SheetNames[bookidx];
else if(supbooks[bookidx-1]) externbook = supbooks[bookidx-1][nameidx]; 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??"}; if(!externbook) externbook = {Name: "??NAMEX??"};
stack.push(externbook.body); stack.push(externbook.Name);
break; break;
/* 2.5.198.80 */ /* 2.5.198.80 */
@ -937,6 +949,9 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
/* 2.5.198.29 */ /* 2.5.198.29 */
case 'PtgAreaErr': stack.push("#REF!"); break; case 'PtgAreaErr': stack.push("#REF!"); break;
/* 2.5.198.30 */
case 'PtgAreaErr3d': stack.push("#REF!"); break;
/* 2.5.198.72 TODO */ /* 2.5.198.72 TODO */
case 'PtgMemFunc': break; case 'PtgMemFunc': break;

@ -342,9 +342,9 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ {
var supbooks = ([[]]/*:any*/); var supbooks = ([[]]/*:any*/);
supbooks.sharedf = shared_formulae; supbooks.sharedf = shared_formulae;
supbooks.arrayf = array_formulae; supbooks.arrayf = array_formulae;
supbooks.SheetNames = wb.SheetNames || wb.Sheets.map(function(x) { return x.name; });
opts.supbooks = supbooks; opts.supbooks = supbooks;
for(var i = 0; i < wb.Names.length; ++i) supbooks[0][i+1] = wb.Names[i];
for(var i = 0; i < wb.Names['!names'].length; ++i) supbooks[0][i+1] = wb.Names[wb.Names['!names'][i]];
var colinfo = [], rowinfo = []; var colinfo = [], rowinfo = [];
var defwidth = 0, defheight = 0; // twips / MDW respectively var defwidth = 0, defheight = 0; // twips / MDW respectively

@ -2,7 +2,7 @@
var wbnsregex = /<\w+:workbook/; var wbnsregex = /<\w+:workbook/;
function parse_wb_xml(data, opts)/*:WorkbookFile*/ { function parse_wb_xml(data, opts)/*:WorkbookFile*/ {
if(!data) throw new Error("Could not find file"); 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 pass = false, xmlns = "xmlns";
var dname = {}, dnstart = 0; var dname = {}, dnstart = 0;
/*(data.match(tagregex)||[]).forEach */ /*(data.match(tagregex)||[]).forEach */
@ -73,12 +73,12 @@ function parse_wb_xml(data, opts)/*:WorkbookFile*/ {
dname = {}; dname = {};
dname.Name = y.name; dname.Name = y.name;
if(y.comment) dname.Comment = y.comment; if(y.comment) dname.Comment = y.comment;
if(y.localSheetId) dname.Sheet = +y.localSheetId;
dnstart = idx + x.length; dnstart = idx + x.length;
} break; } break;
case '</definedName>': { case '</definedName>': {
dname.Ref = data.slice(dnstart, idx); dname.Ref = data.slice(dnstart, idx);
wb.Names[dname.Name] = dname; wb.Names.push(dname);
wb.Names['!names'].push(dname.Name);
} break; } break;
case '<definedName/>': break; case '<definedName/>': break;
@ -184,7 +184,19 @@ function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ {
/* functionGroups */ /* functionGroups */
/* externalReferences */ /* externalReferences */
/* definedNames */
if(wb.Workbook && (wb.Workbook.Names||[]).length > 0) {
o[o.length] = "<definedNames>";
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] = "</definedNames>";
}
/* calcPr */ /* calcPr */
/* oleSize */ /* oleSize */
/* customWorkbookViews */ /* customWorkbookViews */

@ -55,7 +55,9 @@ function parse_BrtName(data, length, opts) {
// unusedstring2: XLNullableWideString // unusedstring2: XLNullableWideString
//} //}
data.l = end; 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 */ /* [MS-XLSB] 2.1.7.60 Workbook */
@ -66,15 +68,20 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ {
if(!opts) opts = {}; if(!opts) opts = {};
opts.biff = 12; opts.biff = 12;
var Names = {}, NameList = []; var Names = [];
var supbooks = [];
supbooks.SheetNames = [];
recordhopper(data, function hopper_wb(val, R_n, RT) { recordhopper(data, function hopper_wb(val, R_n, RT) {
switch(RT) { switch(RT) {
case 0x009C: /* 'BrtBundleSh' */ case 0x009C: /* 'BrtBundleSh' */
supbooks.SheetNames.push(val.name);
wb.Sheets.push(val); break; wb.Sheets.push(val); break;
case 0x0027: /* 'BrtName' */ 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; break;
case 0x040C: /* 'BrtNameExt' */ break; case 0x040C: /* 'BrtNameExt' */ break;
@ -133,7 +140,6 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ {
parse_wb_defaults(wb); parse_wb_defaults(wb);
Names['!names'] = NameList;
// $FlowIgnore // $FlowIgnore
wb.Names = Names; wb.Names = Names;

@ -312,7 +312,17 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty); for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty);
break; 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 'NamedCell': break;
case 'B': break; case 'B': break;
case 'I': break; case 'I': break;
@ -796,7 +806,7 @@ function write_props_xlml(wb, opts) {
/* DocumentProperties */ /* DocumentProperties */
if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts));
/* CustomDocumentProperties */ /* 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(""); return o.join("");
} }
/* TODO */ /* TODO */

@ -169,15 +169,17 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
var colinfo = [], rowinfo = []; var colinfo = [], rowinfo = [];
var defwidth = 0, defheight = 0; // twips / MDW respectively var defwidth = 0, defheight = 0; // twips / MDW respectively
var seencol = false; var seencol = false;
var supbooks = ([[]]/*:any*/); // 1-indexed, will hold extern names var supbooks = ([]/*:any*/); // 1-indexed, will hold extern names
var sbc = 0, sbci = 0, sbcli = 0;
supbooks.SheetNames = opts.snames; supbooks.SheetNames = opts.snames;
supbooks.sharedf = opts.sharedf; supbooks.sharedf = opts.sharedf;
supbooks.arrayf = opts.arrayf; supbooks.arrayf = opts.arrayf;
supbooks.names = [];
supbooks.XTI = [];
var last_Rn = ''; var last_Rn = '';
var file_depth = 0; /* TODO: make a real stack */ var file_depth = 0; /* TODO: make a real stack */
var BIFF2Fmt = 0; var BIFF2Fmt = 0;
var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */
var last_lbl;
/* explicit override for some broken writers */ /* explicit override for some broken writers */
opts.codepage = 1200; opts.codepage = 1200;
@ -250,18 +252,35 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
case 'RichTextStream': break; case 'RichTextStream': break;
case 'BkHim': break; case 'BkHim': break;
case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; case 'SupBook':
case 'ExternName': supbooks[sbc][++sbci] = val; break; supbooks.push([val]);
supbooks[supbooks.length-1].XTI = [];
break;
case 'ExternName':
supbooks[supbooks.length-1].push(val);
break;
case 'Index': break; // TODO case 'Index': break; // TODO
case 'Lbl': case 'Lbl':
supbooks[0][++sbcli] = val; // TODO: local formula storage in stringify_formula last_lbl = {
if(!supbooks[val.itab]) supbooks[val.itab] = []; Name: val.Name,
supbooks[val.itab].push(val); 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.Name == "\r" && val.itab > 0)
if(val.rgce && val.rgce[0] && val.rgce[0][0] && val.rgce[0][0][0] == 'PtgArea3d') 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]) }; FilterDatabases[val.itab - 1] = { ref: encode_range(val.rgce[0][0][1][2]) };
break; 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 'Protect': out["!protect"] = val; break; /* for sheet or book */
case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break; 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; } break;
case 'Row': break; // TODO case 'Row': break; // TODO
case 'NameCmt': break;
case 'Header': break; // TODO case 'Header': break; // TODO
case 'Footer': break; // TODO case 'Footer': break; // TODO
case 'HCenter': break; // TODO case 'HCenter': break; // TODO
@ -727,6 +745,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
if(opts.enc) wb.Encryption = opts.enc; if(opts.enc) wb.Encryption = opts.enc;
wb.Metadata = {}; wb.Metadata = {};
if(country !== undefined) wb.Metadata.Country = country; if(country !== undefined) wb.Metadata.Country = country;
if(supbooks.names.length > 0) Workbook.Names = supbooks.names;
wb.Workbook = Workbook; wb.Workbook = Workbook;
return wb; return wb;
} }

@ -1103,7 +1103,7 @@ var XLSRecordEnum = {
/*::[*/0x0890/*::]*/: { n:"TableStyleElement", f:parse_TableStyleElement }, /*::[*/0x0890/*::]*/: { n:"TableStyleElement", f:parse_TableStyleElement },
/*::[*/0x0892/*::]*/: { n:"StyleExt", f:parse_StyleExt }, /*::[*/0x0892/*::]*/: { n:"StyleExt", f:parse_StyleExt },
/*::[*/0x0893/*::]*/: { n:"NamePublish", f:parse_NamePublish }, /*::[*/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 }, /*::[*/0x0895/*::]*/: { n:"SortData", f:parse_SortData },
/*::[*/0x0896/*::]*/: { n:"Theme", f:parse_Theme, r:12 }, /*::[*/0x0896/*::]*/: { n:"Theme", f:parse_Theme, r:12 },
/*::[*/0x0897/*::]*/: { n:"GUIDTypeLib", f:parse_GUIDTypeLib }, /*::[*/0x0897/*::]*/: { n:"GUIDTypeLib", f:parse_GUIDTypeLib },

@ -36,3 +36,4 @@ Writers will process the `Props` key of the options object:
/* force the Author to be "SheetJS" */ /* force the Author to be "SheetJS" */
XLSX.write(wb, {Props:{Author:"SheetJS"}}); XLSX.write(wb, {Props:{Author:"SheetJS"}});
``` ```

19
docbits/57_wbbook.md Normal file

@ -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.

@ -22,6 +22,8 @@
+ [Chartsheet Object](README.md#chartsheet-object) + [Chartsheet Object](README.md#chartsheet-object)
* [Workbook Object](README.md#workbook-object) * [Workbook Object](README.md#workbook-object)
+ [Workbook File Properties](README.md#workbook-file-properties) + [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) * [Document Features](README.md#document-features)
+ [Formulae](README.md#formulae) + [Formulae](README.md#formulae)
+ [Column Properties](README.md#column-properties) + [Column Properties](README.md#column-properties)

@ -1,7 +1,10 @@
# This file controls the multiformat tests # This file controls the multiformat tests
# vim: set ts=4:
# Format: <basename> <ext> <ext> [ext..] # Format: <basename> <ext> <ext> [ext..]
# yes-formula
AutoFilter .xls .xlsb .xlsx .xml 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 NumberFormatCondition .xls .xlsb .xlsm .xml
RkNumber .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml RkNumber .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml
#calendar_stress_test .xls .xlsb .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 comments_stress_test .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml
# yes-csv # yes-csv
custom_properties .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml custom_properties .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml
# no-formula (defined names)
defined_names_simple .xls .xlsb .xlsx .xml defined_names_simple .xls .xlsb .xlsx .xml
# yes-formula
# no-csv (randbetween) note: ODS does not support many XLSX functions # 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 # yes-csv
formulae_test_simple .xls .xlsb .xlsx .xml formulae_test_simple .xls .xlsb .xlsx .xml
hyperlink_stress_test_2011 .xls .xlsb .xlsx .xml hyperlink_stress_test_2011 .xls .xlsb .xlsx .xml
#large_strings .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml #large_strings .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml
merge_cells .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 named_ranges_2011 .xls .xlsb .xlsx .xls.xml .xlsb.xml .xlsx.xml
# yes-formula # yes-formula
# no-csv (macro serialization in xml) # no-csv (macro serialization in xml)

27
test.js

@ -50,6 +50,10 @@ var paths = {
cstxlsx: dir + 'comments_stress_test.xlsx', cstxlsx: dir + 'comments_stress_test.xlsx',
cstxlsb: dir + 'comments_stress_test.xlsb', cstxlsb: dir + 'comments_stress_test.xlsb',
cstods: dir + 'comments_stress_test.ods', 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', fstxls: dir + 'formula_stress_test.xls',
fstxml: dir + 'formula_stress_test.xls.xml', fstxml: dir + 'formula_stress_test.xls.xml',
fstxlsx: dir + 'formula_stress_test.xlsx', 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() { describe('auto filter', function() {
[ [
['xlsx', paths.afxlsx], ['xlsx', paths.afxlsx],

@ -3273,7 +3273,7 @@ function xlml_write_docprops(Props, opts) {
case 'date': m = new Date(m).toISOString(); break; case 'date': m = new Date(m).toISOString(); break;
} }
if(typeof m == 'number') m = String(m); 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(); else if(m instanceof Date) m = new Date(m).toISOString();
o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m));
}); });
@ -3590,13 +3590,6 @@ function parslurp(blob, length, cb) {
return arr; 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 parsebool(blob, length) { return blob.read_shift(length) === 0x1; }
function parseuint16(blob) { return blob.read_shift(2, 'u'); } 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); if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts);
//else throw new Error("unsupported SupBook cch: " + opts.sbcch); //else throw new Error("unsupported SupBook cch: " + opts.sbcch);
o.body = body || blob.read_shift(length-2); o.body = body || blob.read_shift(length-2);
if(typeof body === "string") o.Name = body;
return o; return o;
} }
@ -4230,13 +4224,21 @@ function parse_Lbl(blob, length, opts) {
/* 2.4.106 TODO: verify supbook manipulation */ /* 2.4.106 TODO: verify supbook manipulation */
function parse_ExternSheet(blob, length, opts) { function parse_ExternSheet(blob, length, opts) {
if(opts.biff < 8) return parse_ShortXLUnicodeString(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 = []; var oo = [];
if(opts.sbcch === 0x0401) { return o;
for(var i = 0; i != o.length; ++i) oo.push(opts.snames[o[i][1]]); }
return oo;
} /* 2.4.176 TODO: check older biff */
else return o; 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 */ /* 2.4.260 */
@ -4652,7 +4654,6 @@ var parse_TableStyles = parsenoop;
var parse_TableStyle = parsenoop; var parse_TableStyle = parsenoop;
var parse_TableStyleElement = parsenoop; var parse_TableStyleElement = parsenoop;
var parse_NamePublish = parsenoop; var parse_NamePublish = parsenoop;
var parse_NameCmt = parsenoop;
var parse_SortData = parsenoop; var parse_SortData = parsenoop;
var parse_GUIDTypeLib = parsenoop; var parse_GUIDTypeLib = parsenoop;
var parse_FnGrp12 = parsenoop; var parse_FnGrp12 = parsenoop;
@ -8130,13 +8131,13 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
break; break;
/* 2.5.198.88 */ /* 2.5.198.88 */
case 'PtgRefN': 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)); stack.push(encode_cell_xls(c));
break; break;
case 'PtgRef3d': // TODO: lots of stuff case 'PtgRef3d': // TODO: lots of stuff
type = f[1][0]; ixti = /*::Number(*/f[1][1]/*::)*/; c = shift_cell_xls(f[1][2], _range, opts); 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**"); sname = supbooks.SheetNames[ixti];
stack.push(sname + "!" + encode_cell(c)); stack.push(sname + "!" + encode_cell_xls(c));
break; break;
/* 2.5.198.62 */ /* 2.5.198.62 */
@ -8192,7 +8193,7 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
case 'PtgName': case 'PtgName':
/* f[1] = type, 0, nameindex */ /* f[1] = type, 0, nameindex */
nameidx = f[1][2]; 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); var name = lbl ? lbl.Name : "**MISSING**" + String(nameidx);
if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name]; if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name];
stack.push(name); stack.push(name);
@ -8204,15 +8205,27 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
var bookidx/*:number*/ = (f[1][1]/*:any*/); nameidx = f[1][2]; var externbook; var bookidx/*:number*/ = (f[1][1]/*:any*/); nameidx = f[1][2]; var externbook;
/* TODO: Properly handle missing values */ /* TODO: Properly handle missing values */
//console.log(bookidx, supbooks); //console.log(bookidx, supbooks);
if(opts.biff == 5) { if(opts.biff <= 5) {
if(bookidx < 0) bookidx = -bookidx; if(bookidx < 0) bookidx = -bookidx;
if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx]; if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx];
} else { } else {
if(supbooks[bookidx+1]) externbook = supbooks[bookidx+1][nameidx]; var pnxname = supbooks.SheetNames[bookidx];
else if(supbooks[bookidx-1]) externbook = supbooks[bookidx-1][nameidx]; 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??"}; if(!externbook) externbook = {Name: "??NAMEX??"};
stack.push(externbook.body); stack.push(externbook.Name);
break; break;
/* 2.5.198.80 */ /* 2.5.198.80 */
@ -8298,6 +8311,9 @@ function stringify_formula(formula/*Array<any>*/, range, cell/*:any*/, supbooks,
/* 2.5.198.29 */ /* 2.5.198.29 */
case 'PtgAreaErr': stack.push("#REF!"); break; case 'PtgAreaErr': stack.push("#REF!"); break;
/* 2.5.198.30 */
case 'PtgAreaErr3d': stack.push("#REF!"); break;
/* 2.5.198.72 TODO */ /* 2.5.198.72 TODO */
case 'PtgMemFunc': break; case 'PtgMemFunc': break;
@ -10528,9 +10544,9 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ {
var supbooks = ([[]]/*:any*/); var supbooks = ([[]]/*:any*/);
supbooks.sharedf = shared_formulae; supbooks.sharedf = shared_formulae;
supbooks.arrayf = array_formulae; supbooks.arrayf = array_formulae;
supbooks.SheetNames = wb.SheetNames || wb.Sheets.map(function(x) { return x.name; });
opts.supbooks = supbooks; opts.supbooks = supbooks;
for(var i = 0; i < wb.Names.length; ++i) supbooks[0][i+1] = wb.Names[i];
for(var i = 0; i < wb.Names['!names'].length; ++i) supbooks[0][i+1] = wb.Names[wb.Names['!names'][i]];
var colinfo = [], rowinfo = []; var colinfo = [], rowinfo = [];
var defwidth = 0, defheight = 0; // twips / MDW respectively var defwidth = 0, defheight = 0; // twips / MDW respectively
@ -11161,7 +11177,7 @@ function check_wb(wb) {
var wbnsregex = /<\w+:workbook/; var wbnsregex = /<\w+:workbook/;
function parse_wb_xml(data, opts)/*:WorkbookFile*/ { function parse_wb_xml(data, opts)/*:WorkbookFile*/ {
if(!data) throw new Error("Could not find file"); 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 pass = false, xmlns = "xmlns";
var dname = {}, dnstart = 0; var dname = {}, dnstart = 0;
/*(data.match(tagregex)||[]).forEach */ /*(data.match(tagregex)||[]).forEach */
@ -11232,12 +11248,12 @@ function parse_wb_xml(data, opts)/*:WorkbookFile*/ {
dname = {}; dname = {};
dname.Name = y.name; dname.Name = y.name;
if(y.comment) dname.Comment = y.comment; if(y.comment) dname.Comment = y.comment;
if(y.localSheetId) dname.Sheet = +y.localSheetId;
dnstart = idx + x.length; dnstart = idx + x.length;
} break; } break;
case '</definedName>': { case '</definedName>': {
dname.Ref = data.slice(dnstart, idx); dname.Ref = data.slice(dnstart, idx);
wb.Names[dname.Name] = dname; wb.Names.push(dname);
wb.Names['!names'].push(dname.Name);
} break; } break;
case '<definedName/>': break; case '<definedName/>': break;
@ -11343,7 +11359,19 @@ function write_wb_xml(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:string*/ {
/* functionGroups */ /* functionGroups */
/* externalReferences */ /* externalReferences */
/* definedNames */
if(wb.Workbook && (wb.Workbook.Names||[]).length > 0) {
o[o.length] = "<definedNames>";
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] = "</definedNames>";
}
/* calcPr */ /* calcPr */
/* oleSize */ /* oleSize */
/* customWorkbookViews */ /* customWorkbookViews */
@ -11415,7 +11443,9 @@ function parse_BrtName(data, length, opts) {
// unusedstring2: XLNullableWideString // unusedstring2: XLNullableWideString
//} //}
data.l = end; 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 */ /* [MS-XLSB] 2.1.7.60 Workbook */
@ -11426,15 +11456,20 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ {
if(!opts) opts = {}; if(!opts) opts = {};
opts.biff = 12; opts.biff = 12;
var Names = {}, NameList = []; var Names = [];
var supbooks = [];
supbooks.SheetNames = [];
recordhopper(data, function hopper_wb(val, R_n, RT) { recordhopper(data, function hopper_wb(val, R_n, RT) {
switch(RT) { switch(RT) {
case 0x009C: /* 'BrtBundleSh' */ case 0x009C: /* 'BrtBundleSh' */
supbooks.SheetNames.push(val.name);
wb.Sheets.push(val); break; wb.Sheets.push(val); break;
case 0x0027: /* 'BrtName' */ 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; break;
case 0x040C: /* 'BrtNameExt' */ break; case 0x040C: /* 'BrtNameExt' */ break;
@ -11493,7 +11528,6 @@ function parse_wb_bin(data, opts)/*:WorkbookFile*/ {
parse_wb_defaults(wb); parse_wb_defaults(wb);
Names['!names'] = NameList;
// $FlowIgnore // $FlowIgnore
wb.Names = Names; 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); for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty);
break; 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 'NamedCell': break;
case 'B': break; case 'B': break;
case 'I': break; case 'I': break;
@ -12479,7 +12523,7 @@ function write_props_xlml(wb, opts) {
/* DocumentProperties */ /* DocumentProperties */
if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts));
/* CustomDocumentProperties */ /* 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(""); return o.join("");
} }
/* TODO */ /* TODO */
@ -12797,15 +12841,17 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
var colinfo = [], rowinfo = []; var colinfo = [], rowinfo = [];
var defwidth = 0, defheight = 0; // twips / MDW respectively var defwidth = 0, defheight = 0; // twips / MDW respectively
var seencol = false; var seencol = false;
var supbooks = ([[]]/*:any*/); // 1-indexed, will hold extern names var supbooks = ([]/*:any*/); // 1-indexed, will hold extern names
var sbc = 0, sbci = 0, sbcli = 0;
supbooks.SheetNames = opts.snames; supbooks.SheetNames = opts.snames;
supbooks.sharedf = opts.sharedf; supbooks.sharedf = opts.sharedf;
supbooks.arrayf = opts.arrayf; supbooks.arrayf = opts.arrayf;
supbooks.names = [];
supbooks.XTI = [];
var last_Rn = ''; var last_Rn = '';
var file_depth = 0; /* TODO: make a real stack */ var file_depth = 0; /* TODO: make a real stack */
var BIFF2Fmt = 0; var BIFF2Fmt = 0;
var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */
var last_lbl;
/* explicit override for some broken writers */ /* explicit override for some broken writers */
opts.codepage = 1200; opts.codepage = 1200;
@ -12878,18 +12924,35 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
case 'RichTextStream': break; case 'RichTextStream': break;
case 'BkHim': break; case 'BkHim': break;
case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; case 'SupBook':
case 'ExternName': supbooks[sbc][++sbci] = val; break; supbooks.push([val]);
supbooks[supbooks.length-1].XTI = [];
break;
case 'ExternName':
supbooks[supbooks.length-1].push(val);
break;
case 'Index': break; // TODO case 'Index': break; // TODO
case 'Lbl': case 'Lbl':
supbooks[0][++sbcli] = val; // TODO: local formula storage in stringify_formula last_lbl = {
if(!supbooks[val.itab]) supbooks[val.itab] = []; Name: val.Name,
supbooks[val.itab].push(val); 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.Name == "\r" && val.itab > 0)
if(val.rgce && val.rgce[0] && val.rgce[0][0] && val.rgce[0][0][0] == 'PtgArea3d') 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]) }; FilterDatabases[val.itab - 1] = { ref: encode_range(val.rgce[0][0][1][2]) };
break; 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 'Protect': out["!protect"] = val; break; /* for sheet or book */
case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break; 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; } break;
case 'Row': break; // TODO case 'Row': break; // TODO
case 'NameCmt': break;
case 'Header': break; // TODO case 'Header': break; // TODO
case 'Footer': break; // TODO case 'Footer': break; // TODO
case 'HCenter': break; // TODO case 'HCenter': break; // TODO
@ -13355,6 +13417,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
if(opts.enc) wb.Encryption = opts.enc; if(opts.enc) wb.Encryption = opts.enc;
wb.Metadata = {}; wb.Metadata = {};
if(country !== undefined) wb.Metadata.Country = country; if(country !== undefined) wb.Metadata.Country = country;
if(supbooks.names.length > 0) Workbook.Names = supbooks.names;
wb.Workbook = Workbook; wb.Workbook = Workbook;
return wb; return wb;
} }
@ -14515,7 +14578,7 @@ var XLSRecordEnum = {
/*::[*/0x0890/*::]*/: { n:"TableStyleElement", f:parse_TableStyleElement }, /*::[*/0x0890/*::]*/: { n:"TableStyleElement", f:parse_TableStyleElement },
/*::[*/0x0892/*::]*/: { n:"StyleExt", f:parse_StyleExt }, /*::[*/0x0892/*::]*/: { n:"StyleExt", f:parse_StyleExt },
/*::[*/0x0893/*::]*/: { n:"NamePublish", f:parse_NamePublish }, /*::[*/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 }, /*::[*/0x0895/*::]*/: { n:"SortData", f:parse_SortData },
/*::[*/0x0896/*::]*/: { n:"Theme", f:parse_Theme, r:12 }, /*::[*/0x0896/*::]*/: { n:"Theme", f:parse_Theme, r:12 },
/*::[*/0x0897/*::]*/: { n:"GUIDTypeLib", f:parse_GUIDTypeLib }, /*::[*/0x0897/*::]*/: { n:"GUIDTypeLib", f:parse_GUIDTypeLib },

155
xlsx.js

@ -3219,7 +3219,7 @@ function xlml_write_docprops(Props, opts) {
case 'date': m = new Date(m).toISOString(); break; case 'date': m = new Date(m).toISOString(); break;
} }
if(typeof m == 'number') m = String(m); 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(); else if(m instanceof Date) m = new Date(m).toISOString();
o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m)); o.push(writetag(XLMLDocPropsMap[p[1]] || p[1], m));
}); });
@ -3534,13 +3534,6 @@ function parslurp(blob, length, cb) {
return arr; 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 parsebool(blob, length) { return blob.read_shift(length) === 0x1; }
function parseuint16(blob) { return blob.read_shift(2, 'u'); } 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); if(opts.sbcch === 0x3A01) body = parse_AddinUdf(blob, length-2, opts);
//else throw new Error("unsupported SupBook cch: " + opts.sbcch); //else throw new Error("unsupported SupBook cch: " + opts.sbcch);
o.body = body || blob.read_shift(length-2); o.body = body || blob.read_shift(length-2);
if(typeof body === "string") o.Name = body;
return o; return o;
} }
@ -4174,13 +4168,21 @@ function parse_Lbl(blob, length, opts) {
/* 2.4.106 TODO: verify supbook manipulation */ /* 2.4.106 TODO: verify supbook manipulation */
function parse_ExternSheet(blob, length, opts) { function parse_ExternSheet(blob, length, opts) {
if(opts.biff < 8) return parse_ShortXLUnicodeString(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 = []; var oo = [];
if(opts.sbcch === 0x0401) { return o;
for(var i = 0; i != o.length; ++i) oo.push(opts.snames[o[i][1]]); }
return oo;
} /* 2.4.176 TODO: check older biff */
else return o; 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 */ /* 2.4.260 */
@ -4596,7 +4598,6 @@ var parse_TableStyles = parsenoop;
var parse_TableStyle = parsenoop; var parse_TableStyle = parsenoop;
var parse_TableStyleElement = parsenoop; var parse_TableStyleElement = parsenoop;
var parse_NamePublish = parsenoop; var parse_NamePublish = parsenoop;
var parse_NameCmt = parsenoop;
var parse_SortData = parsenoop; var parse_SortData = parsenoop;
var parse_GUIDTypeLib = parsenoop; var parse_GUIDTypeLib = parsenoop;
var parse_FnGrp12 = parsenoop; var parse_FnGrp12 = parsenoop;
@ -8073,13 +8074,13 @@ function stringify_formula(formula/*Array<any>*/, range, cell, supbooks, opts) {
break; break;
/* 2.5.198.88 */ /* 2.5.198.88 */
case 'PtgRefN': 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)); stack.push(encode_cell_xls(c));
break; break;
case 'PtgRef3d': // TODO: lots of stuff case 'PtgRef3d': // TODO: lots of stuff
type = f[1][0]; ixti = f[1][1]; c = shift_cell_xls(f[1][2], _range, opts); 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**"); sname = supbooks.SheetNames[ixti];
stack.push(sname + "!" + encode_cell(c)); stack.push(sname + "!" + encode_cell_xls(c));
break; break;
/* 2.5.198.62 */ /* 2.5.198.62 */
@ -8135,7 +8136,7 @@ function stringify_formula(formula/*Array<any>*/, range, cell, supbooks, opts) {
case 'PtgName': case 'PtgName':
/* f[1] = type, 0, nameindex */ /* f[1] = type, 0, nameindex */
nameidx = f[1][2]; 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); var name = lbl ? lbl.Name : "**MISSING**" + String(nameidx);
if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name]; if(name in XLSXFutureFunctions) name = XLSXFutureFunctions[name];
stack.push(name); stack.push(name);
@ -8147,15 +8148,27 @@ function stringify_formula(formula/*Array<any>*/, range, cell, supbooks, opts) {
var bookidx = (f[1][1]); nameidx = f[1][2]; var externbook; var bookidx = (f[1][1]); nameidx = f[1][2]; var externbook;
/* TODO: Properly handle missing values */ /* TODO: Properly handle missing values */
//console.log(bookidx, supbooks); //console.log(bookidx, supbooks);
if(opts.biff == 5) { if(opts.biff <= 5) {
if(bookidx < 0) bookidx = -bookidx; if(bookidx < 0) bookidx = -bookidx;
if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx]; if(supbooks[bookidx]) externbook = supbooks[bookidx][nameidx];
} else { } else {
if(supbooks[bookidx+1]) externbook = supbooks[bookidx+1][nameidx]; var pnxname = supbooks.SheetNames[bookidx];
else if(supbooks[bookidx-1]) externbook = supbooks[bookidx-1][nameidx]; 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??"}; if(!externbook) externbook = {Name: "??NAMEX??"};
stack.push(externbook.body); stack.push(externbook.Name);
break; break;
/* 2.5.198.80 */ /* 2.5.198.80 */
@ -8241,6 +8254,9 @@ function stringify_formula(formula/*Array<any>*/, range, cell, supbooks, opts) {
/* 2.5.198.29 */ /* 2.5.198.29 */
case 'PtgAreaErr': stack.push("#REF!"); break; case 'PtgAreaErr': stack.push("#REF!"); break;
/* 2.5.198.30 */
case 'PtgAreaErr3d': stack.push("#REF!"); break;
/* 2.5.198.72 TODO */ /* 2.5.198.72 TODO */
case 'PtgMemFunc': break; case 'PtgMemFunc': break;
@ -10471,9 +10487,9 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) {
var supbooks = ([[]]); var supbooks = ([[]]);
supbooks.sharedf = shared_formulae; supbooks.sharedf = shared_formulae;
supbooks.arrayf = array_formulae; supbooks.arrayf = array_formulae;
supbooks.SheetNames = wb.SheetNames || wb.Sheets.map(function(x) { return x.name; });
opts.supbooks = supbooks; opts.supbooks = supbooks;
for(var i = 0; i < wb.Names.length; ++i) supbooks[0][i+1] = wb.Names[i];
for(var i = 0; i < wb.Names['!names'].length; ++i) supbooks[0][i+1] = wb.Names[wb.Names['!names'][i]];
var colinfo = [], rowinfo = []; var colinfo = [], rowinfo = [];
var defwidth = 0, defheight = 0; // twips / MDW respectively var defwidth = 0, defheight = 0; // twips / MDW respectively
@ -11104,7 +11120,7 @@ function check_wb(wb) {
var wbnsregex = /<\w+:workbook/; var wbnsregex = /<\w+:workbook/;
function parse_wb_xml(data, opts) { function parse_wb_xml(data, opts) {
if(!data) throw new Error("Could not find file"); 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 pass = false, xmlns = "xmlns";
var dname = {}, dnstart = 0; var dname = {}, dnstart = 0;
/*(data.match(tagregex)||[]).forEach */ /*(data.match(tagregex)||[]).forEach */
@ -11175,12 +11191,12 @@ function parse_wb_xml(data, opts) {
dname = {}; dname = {};
dname.Name = y.name; dname.Name = y.name;
if(y.comment) dname.Comment = y.comment; if(y.comment) dname.Comment = y.comment;
if(y.localSheetId) dname.Sheet = +y.localSheetId;
dnstart = idx + x.length; dnstart = idx + x.length;
} break; } break;
case '</definedName>': { case '</definedName>': {
dname.Ref = data.slice(dnstart, idx); dname.Ref = data.slice(dnstart, idx);
wb.Names[dname.Name] = dname; wb.Names.push(dname);
wb.Names['!names'].push(dname.Name);
} break; } break;
case '<definedName/>': break; case '<definedName/>': break;
@ -11286,7 +11302,19 @@ function write_wb_xml(wb, opts) {
/* functionGroups */ /* functionGroups */
/* externalReferences */ /* externalReferences */
/* definedNames */
if(wb.Workbook && (wb.Workbook.Names||[]).length > 0) {
o[o.length] = "<definedNames>";
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] = "</definedNames>";
}
/* calcPr */ /* calcPr */
/* oleSize */ /* oleSize */
/* customWorkbookViews */ /* customWorkbookViews */
@ -11358,7 +11386,9 @@ function parse_BrtName(data, length, opts) {
// unusedstring2: XLNullableWideString // unusedstring2: XLNullableWideString
//} //}
data.l = end; 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 */ /* [MS-XLSB] 2.1.7.60 Workbook */
@ -11369,15 +11399,20 @@ function parse_wb_bin(data, opts) {
if(!opts) opts = {}; if(!opts) opts = {};
opts.biff = 12; opts.biff = 12;
var Names = {}, NameList = []; var Names = [];
var supbooks = [];
supbooks.SheetNames = [];
recordhopper(data, function hopper_wb(val, R_n, RT) { recordhopper(data, function hopper_wb(val, R_n, RT) {
switch(RT) { switch(RT) {
case 0x009C: /* 'BrtBundleSh' */ case 0x009C: /* 'BrtBundleSh' */
supbooks.SheetNames.push(val.name);
wb.Sheets.push(val); break; wb.Sheets.push(val); break;
case 0x0027: /* 'BrtName' */ 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; break;
case 0x040C: /* 'BrtNameExt' */ break; case 0x040C: /* 'BrtNameExt' */ break;
@ -11436,7 +11471,6 @@ function parse_wb_bin(data, opts) {
parse_wb_defaults(wb); parse_wb_defaults(wb);
Names['!names'] = NameList;
// $FlowIgnore // $FlowIgnore
wb.Names = Names; 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); for(var i = 0; i < +csty.Span; ++i) cstys[cstys.length] = dup(csty);
break; 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 'NamedCell': break;
case 'B': break; case 'B': break;
case 'I': break; case 'I': break;
@ -12418,7 +12462,7 @@ function write_props_xlml(wb, opts) {
/* DocumentProperties */ /* DocumentProperties */
if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts)); if(wb.Props) o.push(xlml_write_docprops(wb.Props, opts));
/* CustomDocumentProperties */ /* 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(""); return o.join("");
} }
/* TODO */ /* TODO */
@ -12736,15 +12780,17 @@ function parse_workbook(blob, options) {
var colinfo = [], rowinfo = []; var colinfo = [], rowinfo = [];
var defwidth = 0, defheight = 0; // twips / MDW respectively var defwidth = 0, defheight = 0; // twips / MDW respectively
var seencol = false; var seencol = false;
var supbooks = ([[]]); // 1-indexed, will hold extern names var supbooks = ([]); // 1-indexed, will hold extern names
var sbc = 0, sbci = 0, sbcli = 0;
supbooks.SheetNames = opts.snames; supbooks.SheetNames = opts.snames;
supbooks.sharedf = opts.sharedf; supbooks.sharedf = opts.sharedf;
supbooks.arrayf = opts.arrayf; supbooks.arrayf = opts.arrayf;
supbooks.names = [];
supbooks.XTI = [];
var last_Rn = ''; var last_Rn = '';
var file_depth = 0; /* TODO: make a real stack */ var file_depth = 0; /* TODO: make a real stack */
var BIFF2Fmt = 0; var BIFF2Fmt = 0;
var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */ var FilterDatabases = []; /* TODO: sort out supbooks and process elsewhere */
var last_lbl;
/* explicit override for some broken writers */ /* explicit override for some broken writers */
opts.codepage = 1200; opts.codepage = 1200;
@ -12817,18 +12863,35 @@ function parse_workbook(blob, options) {
case 'RichTextStream': break; case 'RichTextStream': break;
case 'BkHim': break; case 'BkHim': break;
case 'SupBook': supbooks[++sbc] = [val]; sbci = 0; break; case 'SupBook':
case 'ExternName': supbooks[sbc][++sbci] = val; break; supbooks.push([val]);
supbooks[supbooks.length-1].XTI = [];
break;
case 'ExternName':
supbooks[supbooks.length-1].push(val);
break;
case 'Index': break; // TODO case 'Index': break; // TODO
case 'Lbl': case 'Lbl':
supbooks[0][++sbcli] = val; // TODO: local formula storage in stringify_formula last_lbl = {
if(!supbooks[val.itab]) supbooks[val.itab] = []; Name: val.Name,
supbooks[val.itab].push(val); 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.Name == "\r" && val.itab > 0)
if(val.rgce && val.rgce[0] && val.rgce[0][0] && val.rgce[0][0][0] == 'PtgArea3d') 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]) }; FilterDatabases[val.itab - 1] = { ref: encode_range(val.rgce[0][0][1][2]) };
break; 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 'Protect': out["!protect"] = val; break; /* for sheet or book */
case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break; case 'Password': if(val !== 0 && opts.WTF) console.error("Password verifier: " + val); break;
@ -13051,7 +13114,6 @@ function parse_workbook(blob, options) {
} break; } break;
case 'Row': break; // TODO case 'Row': break; // TODO
case 'NameCmt': break;
case 'Header': break; // TODO case 'Header': break; // TODO
case 'Footer': break; // TODO case 'Footer': break; // TODO
case 'HCenter': break; // TODO case 'HCenter': break; // TODO
@ -13294,6 +13356,7 @@ function parse_workbook(blob, options) {
if(opts.enc) wb.Encryption = opts.enc; if(opts.enc) wb.Encryption = opts.enc;
wb.Metadata = {}; wb.Metadata = {};
if(country !== undefined) wb.Metadata.Country = country; if(country !== undefined) wb.Metadata.Country = country;
if(supbooks.names.length > 0) Workbook.Names = supbooks.names;
wb.Workbook = Workbook; wb.Workbook = Workbook;
return wb; return wb;
} }
@ -14454,7 +14517,7 @@ var XLSRecordEnum = {
0x0890: { n:"TableStyleElement", f:parse_TableStyleElement }, 0x0890: { n:"TableStyleElement", f:parse_TableStyleElement },
0x0892: { n:"StyleExt", f:parse_StyleExt }, 0x0892: { n:"StyleExt", f:parse_StyleExt },
0x0893: { n:"NamePublish", f:parse_NamePublish }, 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 }, 0x0895: { n:"SortData", f:parse_SortData },
0x0896: { n:"Theme", f:parse_Theme, r:12 }, 0x0896: { n:"Theme", f:parse_Theme, r:12 },
0x0897: { n:"GUIDTypeLib", f:parse_GUIDTypeLib }, 0x0897: { n:"GUIDTypeLib", f:parse_GUIDTypeLib },