row and column size and visibility

- XLSX/XLSB/XLS/XLML/SYLK rows and columns
- corrected pixel/point calculations using PPI
- XLSX/XLSB generate sheet view
- clarified sheet protection default behavior
- fixed eslintrc semi check
This commit is contained in:
SheetJS 2017-04-28 03:28:03 -04:00
parent c6f96c3df7
commit dcee744e4e
20 changed files with 997 additions and 233 deletions

@ -13,6 +13,7 @@
"curly": 0,
"comma-style": [ 2, "last" ],
"no-trailing-spaces": 2,
"semi": [ 2, "always" ],
"comma-dangle": [ 2, "never" ]
}
}

@ -57,6 +57,7 @@ enhancements and additional features by request.
* [Document Features](#document-features)
+ [Formulae](#formulae)
+ [Column Properties](#column-properties)
+ [Row Properties](#row-properties)
+ [Hyperlinks](#hyperlinks)
+ [Cell Comments](#cell-comments)
+ [Sheet Visibility](#sheet-visibility)
@ -97,6 +98,7 @@ enhancements and additional features by request.
* [Tested Environments](#tested-environments)
* [Test Files](#test-files)
- [Contributing](#contributing)
* [Tests](#tests)
* [OSX/Linux](#osxlinux)
* [Windows](#windows)
- [License](#license)
@ -630,33 +632,37 @@ In addition to the base sheet keys, worksheets also add:
parsed, the column objects store the pixel width in the `wpx` field, character
width in the `wch` field, and the maximum digit width in the `MDW` field.
- `ws['!rows']`: array of row properties objects as explained later in the docs.
Each row object encodes properties including row height and visibility.
- `ws['!merges']`: array of range objects corresponding to the merged cells in
the worksheet. Plaintext utilities are unaware of merge cells. CSV export
will write all cells in the merge range if they exist, so be sure that only
the first cell (upper-left) in the range is set.
- `ws['protect']`: object of write sheet protection properties. The `password`
- `ws['!protect']`: object of write sheet protection properties. The `password`
key specifies the password for formats that support password-protected sheets
(XLSX/XLSB/XLS). The writer uses the XOR obfuscation method. The following
keys control the sheet protection (same as ECMA-376 18.3.1.85):
keys control the sheet protection -- set to `false` to enable a feature when
sheet is locked or set to `true` to disable a feature:
| key | functionality disabled if value is true |
|:----------------------|:-----------------------------------------------------|
| `selectLockedCells` | Select locked cells |
| `selectUnlockedCells` | Select unlocked cells |
| `formatCells` | Format cells |
| `formatColumns` | Format columns |
| `formatRows` | Format rows |
| `insertColumns` | Insert columns |
| `insertRows` | Insert rows |
| `insertHyperlinks` | Insert hyperlinks |
| `deleteColumns` | Delete columns |
| `deleteRows` | Delete rows |
| `sort` | Sort |
| `autoFilter` | Filter |
| `pivotTables` | Use PivotTable reports |
| `objects` | Edit objects |
| `scenarios` | Edit scenarios |
| key | feature (true=disabled / false=enabled) | default |
|:----------------------|:----------------------------------------|:-----------|
| `selectLockedCells` | Select locked cells | enabled |
| `selectUnlockedCells` | Select unlocked cells | enabled |
| `formatCells` | Format cells | disabled |
| `formatColumns` | Format columns | disabled |
| `formatRows` | Format rows | disabled |
| `insertColumns` | Insert columns | disabled |
| `insertRows` | Insert rows | disabled |
| `insertHyperlinks` | Insert hyperlinks | disabled |
| `deleteColumns` | Delete columns | disabled |
| `deleteRows` | Delete rows | disabled |
| `sort` | Sort | disabled |
| `autoFilter` | Filter | disabled |
| `pivotTables` | Use PivotTable reports | disabled |
| `objects` | Edit objects | enabled |
| `scenarios` | Edit scenarios | enabled |
- `ws['!autofilter']`: AutoFilter object following the schema:
@ -835,6 +841,7 @@ Since Excel prohibits named cells from colliding with names of A1 or RC style
cell references, a (not-so-simple) regex conversion is possible. BIFF Parsed
formulae have to be explicitly unwound. OpenFormula formulae can be converted
with regexes for the most part.
#### Column Properties
Excel internally stores column widths in a nebulous "Max Digit Width" form. The
@ -853,10 +860,11 @@ objects which have the following properties:
```typescript
type ColInfo = {
MDW?:number; // Excel's "Max Digit Width" unit, always integral
width:number; // width in Excel's "Max Digit Width", width*256 is integral
wpx?:number; // width in screen pixels
wch?:number; // intermediate character calculation
MDW?:number; // Excel's "Max Digit Width" unit, always integral
width:number; // width in Excel's "Max Digit Width", width*256 is integral
wpx?:number; // width in screen pixels
wch?:number; // intermediate character calculation
hidden:?boolean; // if true, the column is hidden
};
```
@ -867,6 +875,29 @@ follow the priority order:
2) use `wpx` pixel width if available
3) use `wch` character count if available
#### Row Properties
Excel internally stores row heights in points. The default resolution is 72 DPI
or 96 PPI, so the pixel and point size should agree. For different resolutions
they may not agree, so the library separates the concepts.
The `!rows` array in each worksheet, if present, is a collection of `RowInfo`
objects which have the following properties:
```typescript
type RowInfo = {
hpx?:number; // height in screen pixels
hpt?:number; // height in points
hidden:?boolean; // if true, the row is hidden
};
```
Even though all of the information is made available, writers are expected to
follow the priority order:
1) use `hpx` pixel height if available
2) use `hpt` point height if available
#### Hyperlinks
Hyperlinks are stored in the `l` key of cell objects. The `Target` field of the
@ -1520,6 +1551,25 @@ Running `make init` will refresh the `test_files` submodule and get the files.
Due to the precarious nature of the Open Specifications Promise, it is very
important to ensure code is cleanroom. Consult CONTRIBUTING.md
### Tests
The `test_misc` target (`make test_misc` on Linux/OSX / `make misc` on Windows)
runs the targeted feature tests. It should take 5-10 seconds to perform feature
tests without testing against the entire test battery. New features should be
accompanied with tests for the relevant file formats and features.
For tests involving the read side, an appropriate feature test would involve
reading an existing file and checking the resulting workbook object. If a
parameter is involved, files should be read with different values for the param
to verify that the feature is working as expected.
For tests involving a new write feature which can already be parsed, appropriate
feature tests would involve writing a workbook with the feature and then opening
and verifying that the feature is preserved.
For tests involving a new write feature without an existing read ability, please
add a feature test to the kitchen sink `tests/write.js`.
### OSX/Linux
The xlsx.js file is constructed from the files in the `bits` subdirectory. The

@ -210,14 +210,19 @@ function parse_ExtSST(blob, length) {
}
/* 2.4.221 TODO*/
/* 2.4.221 TODO: check BIFF2-4 */
function parse_Row(blob, length) {
var rw = blob.read_shift(2), col = blob.read_shift(2), Col = blob.read_shift(2), rht = blob.read_shift(2);
blob.read_shift(4); // reserved(2), unused(2)
var z = ({}/*:any*/);
z.r = blob.read_shift(2);
z.c = blob.read_shift(2);
z.cnt = blob.read_shift(2) - z.c;
var miyRw = blob.read_shift(2);
blob.l += 4; // reserved(2), unused(2)
var flags = blob.read_shift(1); // various flags
blob.read_shift(1); // reserved
blob.read_shift(2); //ixfe, other flags
return {r:rw, c:col, cnt:Col-col};
blob.l += 3; // reserved(8), ixfe(12), flags(4)
if(flags & 0x20) z.hidden = true;
if(flags & 0x40) z.hpt = miyRw / 20;
return z;
}

@ -206,19 +206,19 @@ var SYLK = (function() {
var records = str.split(/[\n\r]+/), R = -1, C = -1, ri = 0, rj = 0, arr = [];
var formats = [];
var next_cell_format = null;
var sht = {}, rowinfo = [], colinfo = [], cw = [];
var Mval = 0, j;
for (; ri !== records.length; ++ri) {
Mval = 0;
var record = records[ri].trim().split(";");
var RT = record[0], val;
if(RT === 'P') for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
case 'P':
formats.push(record[rj].substr(1));
break;
}
else if(RT !== 'C' && RT !== 'F') continue;
else for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
switch(RT) {
case 'P': if(record[1].charAt(0) == 'P') formats.push(records[ri].trim().substr(3).replace(/;;/g, ";"));
break;
case 'C': case 'F': for(rj=1; rj<record.length; ++rj) switch(record[rj].charAt(0)) {
case 'Y':
R = parseInt(record[rj].substr(1))-1; C = 0;
for(var j = arr.length; j <= R; ++j) arr[j] = [];
for(j = arr.length; j <= R; ++j) arr[j] = [];
break;
case 'X': C = parseInt(record[rj].substr(1))-1; break;
case 'K':
@ -228,7 +228,7 @@ var SYLK = (function() {
else if(val === 'FALSE') val = false;
else if(+val === +val) {
val = +val;
if(next_cell_format !== null && next_cell_format.match(/[ymdhmsYMDHMS]/)) val = numdate(val);
if(next_cell_format !== null && SSF.is_date(next_cell_format)) val = numdate(val);
}
arr[R][C] = val;
next_cell_format = null;
@ -236,12 +236,37 @@ var SYLK = (function() {
case 'P':
if(RT !== 'F') break;
next_cell_format = formats[parseInt(record[rj].substr(1))];
break;
case 'M': Mval = parseInt(record[rj].substr(1)) / 20; break;
case 'W':
if(RT !== 'F') break;
cw = record[rj].substr(1).split(" ");
for(j = parseInt(cw[0], 10); j <= parseInt(cw[1], 10); ++j) {
Mval = parseInt(cw[2], 10);
colinfo[j-1] = Mval == 0 ? {hidden:true}: {wch:Mval}; process_col(colinfo[j-1]);
} break;
case 'R':
R = parseInt(record[rj].substr(1))-1;
rowinfo[R] = {};
if(Mval > 0) { rowinfo[R].hpt = Mval; rowinfo[R].hpx = pt2px(Mval); }
else if(Mval == 0) rowinfo[R].hidden = true;
} break;
default: break;
}
}
if(rowinfo.length > 0) sht['!rows'] = rowinfo;
if(colinfo.length > 0) sht['!cols'] = colinfo;
arr[arr.length] = sht;
return arr;
}
function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { return aoa_to_sheet(sylk_to_aoa(str, opts), opts); }
function sylk_to_sheet(str/*:string*/, opts)/*:Worksheet*/ {
var aoa = sylk_to_aoa(str, opts);
var ws = aoa.pop();
var o = aoa_to_sheet(aoa, opts);
keys(ws).forEach(function(k) { o[k] = ws[k]; });
return o;
}
function sylk_to_workbook(str/*:string*/, opts)/*:Workbook*/ { return sheet_to_workbook(sylk_to_sheet(str, opts), opts); }
@ -257,11 +282,40 @@ var SYLK = (function() {
return o;
}
function write_ws_cols_sylk(out, cols) {
cols.forEach(function(col, i) {
var rec = "F;W" + (i+1) + " " + (i+1) + " ";
if(col.hidden) rec += "0";
else {
if(typeof col.width == 'number') col.wpx = width2px(col.width);
if(typeof col.wpx == 'number') col.wch = px2char(col.wpx);
if(typeof col.wch == 'number') rec += Math.round(col.wch);
}
if(rec.charAt(rec.length - 1) != " ") out.push(rec);
});
}
function write_ws_rows_sylk(out, rows) {
rows.forEach(function(row, i) {
var rec = "F;";
if(row.hidden) rec += "M0;";
else if(row.hpt) rec += "M" + 20 * row.hpt + ";";
else if(row.hpx) rec += "M" + 20 * px2pt(row.hpx) + ";";
if(rec.length > 2) out.push(rec + "R" + (i+1));
});
}
function sheet_to_sylk(ws/*:Worksheet*/, opts/*:?any*/)/*:string*/ {
var preamble/*:Array<string>*/ = ["ID;PWXL;N;E"], o/*:Array<string>*/ = [];
preamble.push("P;PGeneral");
var r = decode_range(ws['!ref']), cell/*:Cell*/;
var dense = Array.isArray(ws);
var RS = "\r\n";
preamble.push("P;PGeneral");
preamble.push("F;P0;DG0G8;M255");
if(ws['!cols']) write_ws_cols_sylk(preamble, ws['!cols']);
if(ws['!rows']) write_ws_rows_sylk(preamble, ws['!rows']);
for(var R = r.s.r; R <= r.e.r; ++R) {
for(var C = r.s.c; C <= r.e.c; ++C) {
var coord = encode_cell({r:R,c:C});
@ -270,8 +324,6 @@ var SYLK = (function() {
o.push(write_ws_cell_sylk(cell, ws, R, C, opts));
}
}
preamble.push("F;P0;DG0G8;M255");
var RS = "\r\n";
return preamble.join(RS) + RS + o.join(RS) + RS + "E" + RS;
}

@ -85,13 +85,17 @@ function process_col(coll/*:ColInfo*/) {
coll.wch = px2char(coll.wpx);
coll.width = char2width(coll.wch);
coll.MDW = MDW;
} else if(typeof coll.wch == 'number') {
coll.width = char2width(coll.wch);
coll.wpx = width2px(coll.width);
coll.MDW = MDW;
}
if(coll.customWidth) delete coll.customWidth;
}
var DEF_DPI = 96, DPI = DEF_DPI;
function px2pt(px) { return px * 72 / DPI; }
function pt2px(pt) { return pt * DPI / 72; }
var DEF_PPI = 96, PPI = DEF_PPI;
function px2pt(px) { return px * 96 / PPI; }
function pt2px(pt) { return pt * PPI / 96; }
/* [MS-EXSPXML3] 2.4.54 ST_enmPattern */
var XLMLPatternTypeMap = {

@ -14,13 +14,14 @@ function get_sst_id(sst/*:SST*/, str/*:string*/)/*:number*/ {
function col_obj_w(C/*:number*/, col) {
var p = ({min:C+1,max:C+1}/*:any*/);
/* wch (chars), wpx (pixels) */
var width = -1;
var wch = -1;
if(col.MDW) MDW = col.MDW;
if(col.width != null) p.customWidth = 1;
else if(col.wpx != null) width = px2char(col.wpx);
else if(col.wch != null) width = col.wch;
if(width > -1) { p.width = char2width(width); p.customWidth = 1; }
else p.width = col.width;
else if(col.wpx != null) wch = px2char(col.wpx);
else if(col.wch != null) wch = col.wch;
if(wch > -1) { p.width = char2width(wch); p.customWidth = 1; }
else if(col.width != null) p.width = col.width;
if(col.hidden) p.hidden = true;
return p;
}

@ -152,6 +152,7 @@ function parse_ws_xml_cols(columns, cols) {
var seencol = false;
for(var coli = 0; coli != cols.length; ++coli) {
var coll = parsexmltag(cols[coli], true);
if(coll.hidden) coll.hidden = parsexmlbool(coll.hidden);
var colm=parseInt(coll.min, 10)-1, colM=parseInt(coll.max,10)-1;
delete coll.min; delete coll.max; coll.width = +coll.width;
if(!seencol && coll.width) { seencol = true; find_mdw_colw(coll.width); }
@ -178,6 +179,12 @@ function write_ws_xml_autofilter(data)/*:string*/ {
return writextag("autoFilter", null, {ref:data.ref});
}
/* 18.3.1.88 sheetViews CT_SheetViews */
/* 18.3.1.87 sheetView CT_SheetView */
function write_ws_xml_sheetviews(ws, opts, idx, wb)/*:string*/ {
return writextag("sheetViews", writextag("sheetView", null, {workbookViewId:"0"}), {});
}
function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) {
if(cell.v === undefined && cell.f === undefined || cell.t === 'z') return "";
var vv = "";
@ -229,13 +236,14 @@ var parse_ws_xml_data = (function parse_ws_xml_data_factory() {
var match_v = matchtag("v"), match_f = matchtag("f");
return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) {
var ri = 0, x = "", cells = [], cref = [], idx = 0, i=0, cc=0, d="", p/*:any*/;
var ri = 0, x = "", cells = [], cref = [], idx=0, i=0, cc=0, d="", p/*:any*/;
var tag, tagr = 0, tagc = 0;
var sstr, ftag;
var fmtid = 0, fillid = 0, do_format = Array.isArray(styles.CellXf), cf;
var arrayf = [];
var sharedf = [];
var dense = Array.isArray(s);
var rows = [], rowobj = {}, rowrite = false;
for(var marr = sdata.split(rowregex), mt = 0, marrlen = marr.length; mt != marrlen; ++mt) {
x = marr[mt].trim();
var xlen = x.length;
@ -249,6 +257,13 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) {
if(guess.s.r > tagr - 1) guess.s.r = tagr - 1;
if(guess.e.r < tagr - 1) guess.e.r = tagr - 1;
if(opts && opts.cellStyles) {
rowobj = {}; rowrite = false;
if(tag.ht) { rowrite = true; rowobj.hpt = parseFloat(tag.ht); rowobj.hpx = pt2px(rowobj.hpt); }
if(tag.hidden == "1") { rowrite = true; rowobj.hidden = true; }
if(rowrite) rows[tagr-1] = rowobj;
}
/* 18.3.1.4 c CT_Cell */
cells = x.substr(ri).split(cellregex);
for(ri = 0; ri != cells.length; ++ri) {
@ -357,6 +372,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) {
} else s[tag.r] = p;
}
}
if(rows.length > 0) s['!rows'] = rows;
}; })();
function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/, rels)/*:string*/ {
@ -407,7 +423,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ {
o[o.length] = (writextag('dimension', null, {'ref': ref}));
/* sheetViews */
o[o.length] = write_ws_xml_sheetviews(ws, opts, idx, wb);
/* TODO: store in WB, process styles */
if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, {defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', baseColWidth:opts.sheetFormat.baseColWidth||'10' }));
@ -457,7 +473,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ {
delete ws['!links'];
/* printOptions */
if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins'])
if (ws['!margins'] != null) o[o.length] = write_ws_xml_margins(ws['!margins']);
/* pageSetup */
var hfidx = o.length;

@ -1,20 +1,38 @@
/* [MS-XLSB] 2.4.718 BrtRowHdr */
function parse_BrtRowHdr(data, length) {
var z = ([]/*:any*/);
var z = ({}/*:any*/);
var tgt = data.l + length;
z.r = data.read_shift(4);
data.l += length-4;
data.l += 4; // TODO: ixfe
var miyRw = data.read_shift(2);
data.l += 1; // TODO: top/bot padding
var flags = data.read_shift(1);
data.l = tgt;
if(flags & 0x10) z.hidden = true;
if(flags & 0x20) z.hpt = miyRw / 20;
return z;
}
function write_BrtRowHdr(R/*:number*/, range, ws) {
var o = new_buf(17+8*16);
var row = (ws['!rows']||[])[R]||{};
o.write_shift(4, R);
/* TODO: flags styles */
o.write_shift(4, 0);
o.write_shift(2, 0x0140);
o.write_shift(2, 0);
o.write_shift(1, 0);
o.write_shift(4, 0); /* TODO: ixfe */
var miyRw = 0x0140;
if(row.hpx) miyRw = px2pt(row.hpx) * 20;
else if(row.hpt) miyRw = row.hpt * 20;
o.write_shift(2, miyRw);
o.write_shift(1, 0); /* top/bot padding */
var flags = 0x0;
if(row.hidden) flags |= 0x10;
if(row.hpx || row.hpt) flags |= 0x20;
o.write_shift(1, flags);
o.write_shift(1, 0); /* phonetic guide */
/* [MS-XLSB] 2.5.8 BrtColSpan explains the mechanism */
var ncolspan = 0, lcs = o.l;
@ -282,9 +300,12 @@ function write_BrtColInfo(C/*:number*/, col, o) {
var p = col_obj_w(C, col);
o.write_shift(-4, C);
o.write_shift(-4, C);
o.write_shift(4, p.width * 256);
o.write_shift(4, (p.width || 10) * 256);
o.write_shift(4, 0/*ixfe*/); // style
o.write_shift(1, 2); // bit flag
var flags = 0;
if(col.hidden) flags |= 0x01;
if(typeof p.width == 'number') flags |= 0x02;
o.write_shift(1, flags); // bit flag
o.write_shift(1, 0); // bit flag
return o;
}
@ -312,6 +333,24 @@ function write_BrtMargins(margins, o) {
return o;
}
/* [MS-XLSB] 2.4.292 BrtBeginWsView */
function write_BrtBeginWsView(ws, o) {
if(o == null) o = new_buf(30);
o.write_shift(2, 924); // bit flag
o.write_shift(4, 0);
o.write_shift(4, 0); // view first row
o.write_shift(4, 0); // view first col
o.write_shift(1, 0); // gridline color ICV
o.write_shift(1, 0);
o.write_shift(2, 0);
o.write_shift(2, 100); // zoom scale
o.write_shift(2, 0);
o.write_shift(2, 0);
o.write_shift(2, 0);
o.write_shift(4, 0); // workbook view id
return o;
}
/* [MS-XLSB] 2.4.740 BrtSheetProtection */
function write_BrtSheetProtection(sp, o) {
if(o == null) o = new_buf(16*4+2);
@ -334,9 +373,8 @@ function write_BrtSheetProtection(sp, o) {
["pivotTables", true], // fPivotTables
["selectUnlockedCells", false] // fSelUnlockedCells
].forEach(function(n) {
o.write_shift(4, 1);
if(!n[1]) o.write_shift(4, sp[n] != null && sp[n] ? 1 : 0);
else o.write_shift(4, sp[n] != null && !sp[n] ? 0 : 1);
if(n[1]) o.write_shift(4, sp[n[0]] != null && !sp[n[0]] ? 1 : 0);
else o.write_shift(4, sp[n[0]] != null && sp[n[0]] ? 0 : 1);
});
return o;
}
@ -383,6 +421,10 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ {
if(opts.sheetRows && opts.sheetRows <= row.r) end=true;
rr = encode_row(R = row.r);
opts['!row'] = row.r;
if(val.hidden || val.hpt) {
if(val.hpt) val.hpx = pt2px(val.hpt);
rowinfo[val.r] = val;
}
break;
case 0x0002: /* 'BrtCellRk' */
@ -480,7 +522,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles)/*:Worksheet*/ {
case 0x003C: /* 'BrtColInfo' */
if(!opts.cellStyles) break;
while(val.e >= val.s) {
colinfo[val.e--] = { width: val.w/256 };
colinfo[val.e--] = { width: val.w/256, hidden: !!(val.flags & 0x01) };
if(!seencol) { seencol = true; find_mdw_colw(val.w/256); }
process_col(colinfo[val.e+1]);
}
@ -696,6 +738,21 @@ function write_AUTOFILTER(ba, ws) {
write_record(ba, "BrtEndAFilter");
}
function write_WSVIEWS2(ba, ws) {
write_record(ba, "BrtBeginWsViews");
{ /* 1*WSVIEW2 */
/* [ACUID] */
write_record(ba, "BrtBeginWsView", write_BrtBeginWsView(ws));
/* [BrtPane] */
/* *4BrtSel */
/* *4SXSELECT */
/* *FRT */
write_record(ba, "BrtEndWsView");
}
/* *FRT */
write_record(ba, "BrtEndWsViews");
}
function write_SHEETPROTECT(ba, ws) {
if(!ws['!protect']) return;
/* [BrtSheetProtectionIso] */
@ -712,7 +769,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) {
write_record(ba, "BrtBeginSheet");
write_record(ba, "BrtWsProp", write_BrtWsProp(s));
write_record(ba, "BrtWsDim", write_BrtWsDim(r));
/* [WSVIEWS2] */
write_WSVIEWS2(ba, ws);
/* [WSFMTINFO] */
write_COLINFOS(ba, ws, idx, opts, wb);
write_CELLTABLE(ba, ws, idx, opts, wb);

@ -189,7 +189,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
var comments = [], comment = {};
var cstys = [], csty, seencol = false;
var arrayf = [];
var rowinfo = [];
var rowinfo = [], rowobj = {};
var Workbook = { Sheets:[] }, wsprops = {};
xlmlregex.lastIndex = 0;
str = str.replace(/<!--([^\u2603]*?)-->/mg,"");
@ -254,6 +254,12 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
} else {
row = xlml_parsexmltag(Rn[0]);
if(row.Index) r = +row.Index - 1;
rowobj = {};
if(row.AutoFitHeight == "0") {
rowobj.hpx = parseInt(row.Height, 10); rowobj.hpt = px2pt(rowobj.hpx);
rowinfo[r] = rowobj;
}
if(row.Hidden == "1") { rowobj.hidden = true; rowinfo[r] = rowobj; }
}
break;
case 'Worksheet': /* TODO: read range from FullRows/FullColumns */
@ -304,9 +310,10 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
case 'Column':
if(state[state.length-1][0] !== 'Table') break;
csty = xlml_parsexmltag(Rn[0]);
csty.wpx = parseInt(csty.Width, 10);
if(csty.Hidden) { csty.hidden = true; delete csty.Hidden; }
if(csty.Width) csty.wpx = parseInt(csty.Width, 10);
if(!seencol && csty.wpx > 10) {
seencol = true; find_mdw_wpx(csty.wpx);
seencol = true; MDW = DEF_MDW; //find_mdw_wpx(csty.wpx);
for(var _col = 0; _col < cstys.length; ++_col) if(cstys[_col]) process_col(cstys[_col]);
}
if(seencol) process_col(csty);
@ -443,7 +450,7 @@ function parse_xlml_xml(d, opts)/*:Workbook*/ {
case 'Color': break;
case 'Index': break;
case 'RGB': break;
case 'PixelsPerInch': break;
case 'PixelsPerInch': break; // TODO: set PPI
case 'TargetScreenSize': break;
case 'ReadOnlyRecommended': break;
default: seen = false;
@ -995,6 +1002,15 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{
return writextag("Cell", m, attr);
}
function write_ws_xlml_row(R/*:number*/, row)/*:string*/ {
var o = '<Row ss:Index="' + (R+1) + '"';
if(row) {
if(row.hpt && !row.hpx) row.hpx = pt2px(row.hpt);
if(row.hpx) o += ' ss:AutoFitHeight="0" ss:Height="' + row.hpx + '"';
if(row.hidden) o += ' ss:Hidden="1"';
}
return o + '>';
}
/* TODO */
function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/)/*:string*/ {
if(!ws['!ref']) return "";
@ -1002,12 +1018,17 @@ function write_ws_xlml_table(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbo
var marr = ws['!merges'] || [], mi = 0;
var o = [];
if(ws['!cols']) ws['!cols'].forEach(function(n, i) {
process_col(n);
var w = !!n.width;
var p = col_obj_w(i, n);
o.push(writextag("Column",null, {"ss:Index":i+1, "ss:Width":width2px(p.width)}));
var k = {"ss:Index":i+1};
if(w) k['ss:Width'] = width2px(p.width);
if(n.hidden) k['ss:Hidden']="1";
o.push(writextag("Column",null,k));
});
var dense = Array.isArray(ws);
for(var R = range.s.r; R <= range.e.r; ++R) {
var row = ['<Row ss:Index="' + (R+1) + '">'];
var row = [write_ws_xlml_row(R, (ws['!rows']||[])[R])];
for(var C = range.s.c; C <= range.e.c; ++C) {
var skip = false;
for(mi = 0; mi != marr.length; ++mi) {

@ -502,7 +502,14 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ {
process_col(colinfo[val.e+1]);
}
} break;
case 'Row': break; // TODO
case 'Row': {
var rowobj = {};
if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; }
if(val.hpt) {
rowinfo[val.r] = rowobj;
rowobj.hpt = val.hpt; rowobj.hpx = pt2px(val.hpt);
}
} break;
case 'LeftMargin':
case 'RightMargin':

@ -8,33 +8,37 @@ In addition to the base sheet keys, worksheets also add:
parsed, the column objects store the pixel width in the `wpx` field, character
width in the `wch` field, and the maximum digit width in the `MDW` field.
- `ws['!rows']`: array of row properties objects as explained later in the docs.
Each row object encodes properties including row height and visibility.
- `ws['!merges']`: array of range objects corresponding to the merged cells in
the worksheet. Plaintext utilities are unaware of merge cells. CSV export
will write all cells in the merge range if they exist, so be sure that only
the first cell (upper-left) in the range is set.
- `ws['protect']`: object of write sheet protection properties. The `password`
- `ws['!protect']`: object of write sheet protection properties. The `password`
key specifies the password for formats that support password-protected sheets
(XLSX/XLSB/XLS). The writer uses the XOR obfuscation method. The following
keys control the sheet protection (same as ECMA-376 18.3.1.85):
keys control the sheet protection -- set to `false` to enable a feature when
sheet is locked or set to `true` to disable a feature:
| key | functionality disabled if value is true |
|:----------------------|:-----------------------------------------------------|
| `selectLockedCells` | Select locked cells |
| `selectUnlockedCells` | Select unlocked cells |
| `formatCells` | Format cells |
| `formatColumns` | Format columns |
| `formatRows` | Format rows |
| `insertColumns` | Insert columns |
| `insertRows` | Insert rows |
| `insertHyperlinks` | Insert hyperlinks |
| `deleteColumns` | Delete columns |
| `deleteRows` | Delete rows |
| `sort` | Sort |
| `autoFilter` | Filter |
| `pivotTables` | Use PivotTable reports |
| `objects` | Edit objects |
| `scenarios` | Edit scenarios |
| key | feature (true=disabled / false=enabled) | default |
|:----------------------|:----------------------------------------|:-----------|
| `selectLockedCells` | Select locked cells | enabled |
| `selectUnlockedCells` | Select unlocked cells | enabled |
| `formatCells` | Format cells | disabled |
| `formatColumns` | Format columns | disabled |
| `formatRows` | Format rows | disabled |
| `insertColumns` | Insert columns | disabled |
| `insertRows` | Insert rows | disabled |
| `insertHyperlinks` | Insert hyperlinks | disabled |
| `deleteColumns` | Delete columns | disabled |
| `deleteRows` | Delete rows | disabled |
| `sort` | Sort | disabled |
| `autoFilter` | Filter | disabled |
| `pivotTables` | Use PivotTable reports | disabled |
| `objects` | Edit objects | enabled |
| `scenarios` | Edit scenarios | enabled |
- `ws['!autofilter']`: AutoFilter object following the schema:

@ -77,3 +77,4 @@ Since Excel prohibits named cells from colliding with names of A1 or RC style
cell references, a (not-so-simple) regex conversion is possible. BIFF Parsed
formulae have to be explicitly unwound. OpenFormula formulae can be converted
with regexes for the most part.

@ -16,10 +16,11 @@ objects which have the following properties:
```typescript
type ColInfo = {
MDW?:number; // Excel's "Max Digit Width" unit, always integral
width:number; // width in Excel's "Max Digit Width", width*256 is integral
wpx?:number; // width in screen pixels
wch?:number; // intermediate character calculation
MDW?:number; // Excel's "Max Digit Width" unit, always integral
width:number; // width in Excel's "Max Digit Width", width*256 is integral
wpx?:number; // width in screen pixels
wch?:number; // intermediate character calculation
hidden:?boolean; // if true, the column is hidden
};
```
@ -30,3 +31,26 @@ follow the priority order:
2) use `wpx` pixel width if available
3) use `wch` character count if available
#### Row Properties
Excel internally stores row heights in points. The default resolution is 72 DPI
or 96 PPI, so the pixel and point size should agree. For different resolutions
they may not agree, so the library separates the concepts.
The `!rows` array in each worksheet, if present, is a collection of `RowInfo`
objects which have the following properties:
```typescript
type RowInfo = {
hpx?:number; // height in screen pixels
hpt?:number; // height in points
hidden:?boolean; // if true, the row is hidden
};
```
Even though all of the information is made available, writers are expected to
follow the priority order:
1) use `hpx` pixel height if available
2) use `hpt` point height if available

@ -3,6 +3,25 @@
Due to the precarious nature of the Open Specifications Promise, it is very
important to ensure code is cleanroom. Consult CONTRIBUTING.md
### Tests
The `test_misc` target (`make test_misc` on Linux/OSX / `make misc` on Windows)
runs the targeted feature tests. It should take 5-10 seconds to perform feature
tests without testing against the entire test battery. New features should be
accompanied with tests for the relevant file formats and features.
For tests involving the read side, an appropriate feature test would involve
reading an existing file and checking the resulting workbook object. If a
parameter is involved, files should be read with different values for the param
to verify that the feature is working as expected.
For tests involving a new write feature which can already be parsed, appropriate
feature tests would involve writing a workbook with the feature and then opening
and verifying that the feature is preserved.
For tests involving a new write feature without an existing read ability, please
add a feature test to the kitchen sink `tests/write.js`.
### OSX/Linux
The xlsx.js file is constructed from the files in the `bits` subdirectory. The

@ -29,6 +29,7 @@
* [Document Features](README.md#document-features)
+ [Formulae](README.md#formulae)
+ [Column Properties](README.md#column-properties)
+ [Row Properties](README.md#row-properties)
+ [Hyperlinks](README.md#hyperlinks)
+ [Cell Comments](README.md#cell-comments)
+ [Sheet Visibility](README.md#sheet-visibility)
@ -69,6 +70,7 @@
* [Tested Environments](README.md#tested-environments)
* [Test Files](README.md#test-files)
- [Contributing](README.md#contributing)
* [Tests](README.md#tests)
* [OSX/Linux](README.md#osxlinux)
* [Windows](README.md#windows)
- [License](README.md#license)

117
test.js

@ -54,11 +54,12 @@ var paths = {
cstxlsb: dir + 'comments_stress_test.xlsb',
cstods: dir + 'comments_stress_test.ods',
cwxls: dir + 'column_width.xlsx',
cwxls: dir + 'column_width.xls',
cwxls5: dir + 'column_width.biff5',
cwxml: dir + 'column_width.xml',
cwxlsx: dir + 'column_width.xlsx',
cwxlsb: dir + 'column_width.xlsx',
cwxlsb: dir + 'column_width.xlsb',
cwslk: dir + 'column_width.slk',
dnsxls: dir + 'defined_names_simple.xls',
dnsxml: dir + 'defined_names_simple.xml',
@ -101,6 +102,13 @@ var paths = {
pmxlsx: dir + 'page_margins_2016.xlsx',
pmxlsb: dir + 'page_margins_2016.xlsb',
rhxls: dir + 'row_height.xls',
rhxls5: dir + 'row_height.biff5',
rhxml: dir + 'row_height.xml',
rhxlsx: dir + 'row_height.xlsx',
rhxlsb: dir + 'row_height.xlsb',
rhslk: dir + 'row_height.slk',
svxls: dir + 'sheet_visibility.xls',
svxls5: dir + 'sheet_visibility.xls',
svxml: dir + 'sheet_visibility.xml',
@ -113,6 +121,10 @@ var paths = {
swcxlsb: dir + '2013/apachepoi_SimpleWithComments.xlsx.xlsb'
};
var FSTPaths = [paths.fstxls, paths.fstxml, paths.fstxlsx, paths.fstxlsb, paths.fstods];
var NFPaths = [paths.nfxls, paths.nfxml, paths.nfxlsx, paths.nfxlsb];
var DTPaths = [paths.dtxls, paths.dtxml, paths.dtxlsx, paths.dtxlsb];
var N1 = 'XLSX';
var N2 = 'XLSB';
var N3 = 'XLS';
@ -144,7 +156,7 @@ function parsetest(x, wb, full, ext) {
describe(x + ext + ' should generate JSON', function() {
wb.SheetNames.forEach(function(ws, i) {
it('#' + i + ' (' + ws + ')', function() {
X.utils.sheet_to_row_object_array(wb.Sheets[ws]);
X.utils.sheet_to_json(wb.Sheets[ws]);
});
});
});
@ -284,7 +296,6 @@ describe('parse options', function() {
if(typeof before != 'undefined') before(bef);
else it('before', bef);
describe('cell', function() {
var FSTPaths = [paths.fstxls, paths.fstxml, paths.fstxlsx, paths.fstxlsb, paths.fstods];
it('XLSX should generate HTML by default', function() {
var wb = X.readFile(paths.cstxlsx);
var ws = wb.Sheets.Sheet1;
@ -348,7 +359,7 @@ describe('parse options', function() {
});
});
it('should not generate number formats by default', function() {
[paths.nfxls, paths.nfxlsx, paths.nfxlsb].forEach(function(p) {
NFPaths.forEach(function(p) {
var wb = X.readFile(p);
wb.SheetNames.forEach(function(s) {
var ws = wb.Sheets[s];
@ -359,7 +370,7 @@ describe('parse options', function() {
});
});
it('should generate number formats when requested', function() {
[paths.nfxls, paths.nfxlsx, paths.nfxlsb].forEach(function(p) {
NFPaths.forEach(function(p) {
var wb = X.readFile(p, {cellNF: true});
wb.SheetNames.forEach(function(s) {
var ws = wb.Sheets[s];
@ -395,7 +406,7 @@ describe('parse options', function() {
});
});
it('should not generate cell dates by default', function() {
[paths.dtxlsx, paths.dtxlsb, paths.dtxls, paths.dtxml].forEach(function(p) {
DTPaths.forEach(function(p) {
var wb = X.readFile(p);
wb.SheetNames.forEach(function(s) {
var ws = wb.Sheets[s];
@ -405,8 +416,8 @@ describe('parse options', function() {
});
});
});
it('XLSX should generate cell dates when requested', function() {
[paths.dtxlsx, paths.dtxlsb, paths.dtxls, paths.dtxml].forEach(function(p) {
it('should generate cell dates when requested', function() {
DTPaths.forEach(function(p) {
var wb = X.readFile(paths.dtxlsx, {cellDates: true});
var found = false;
wb.SheetNames.forEach(function(s) {
@ -818,7 +829,7 @@ describe('parse features', function() {
});
describe('column properties', function() {
var wb1, wb2, wb3, wb4, wb5;
var wb1, wb2, wb3, wb4, wb5, wb6;
var bef = (function() {
X = require(modp);
wb1 = X.readFile(paths.cwxlsx, {cellStyles:true});
@ -826,21 +837,21 @@ describe('parse features', function() {
wb3 = X.readFile(paths.cwxls, {cellStyles:true});
wb4 = X.readFile(paths.cwxls5, {cellStyles:true});
wb5 = X.readFile(paths.cwxml, {cellStyles:true});
wb6 = X.readFile(paths.cwslk, {cellStyles:true});
});
if(typeof before != 'undefined') before(bef);
else it('before', bef);
it('should have "!cols"', function() {
assert(wb1.Sheets.Sheet1['!cols']);
assert(wb2.Sheets.Sheet1['!cols']);
assert(wb3.Sheets.Sheet1['!cols']);
assert(wb4.Sheets.Sheet1['!cols']);
assert(wb5.Sheets.Sheet1['!cols']);
[wb1, wb2, wb3, wb4, wb5, wb6].forEach(function(wb) { assert(wb.Sheets.Sheet1['!cols']); });
});
it('should have correct widths', function() {
/* SYLK rounds wch so skip non-integral */
[wb1, wb2, wb3, wb4, wb5].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) {
assert.equal(x[1].width, 0.1640625);
assert.equal(x[2].width, 16.6640625);
assert.equal(x[3].width, 1.6640625);
});
[wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) {
assert.equal(x[4].width, 4.83203125);
assert.equal(x[5].width, 8.83203125);
assert.equal(x[6].width, 12.83203125);
@ -848,10 +859,13 @@ describe('parse features', function() {
});
});
it('should have correct pixels', function() {
/* SYLK rounds wch so skip non-integral */
[wb1, wb2, wb3, wb4, wb5].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) {
assert.equal(x[1].wpx, 1);
assert.equal(x[2].wpx, 100);
assert.equal(x[3].wpx, 10);
});
[wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) {
assert.equal(x[4].wpx, 29);
assert.equal(x[5].wpx, 53);
assert.equal(x[6].wpx, 77);
@ -860,6 +874,39 @@ describe('parse features', function() {
});
});
describe('row properties', function() {
var wb1, wb2, wb3, wb4, wb5, wb6;
var bef = (function() {
X = require(modp);
wb1 = X.readFile(paths.rhxlsx, {cellStyles:true});
wb2 = X.readFile(paths.rhxlsb, {cellStyles:true});
wb3 = X.readFile(paths.rhxls, {cellStyles:true});
wb4 = X.readFile(paths.rhxls5, {cellStyles:true});
wb5 = X.readFile(paths.rhxml, {cellStyles:true});
wb6 = X.readFile(paths.rhslk, {cellStyles:true});
});
if(typeof before != 'undefined') before(bef);
else it('before', bef);
it('should have "!rows"', function() {
[wb1, wb2, wb3, wb4, wb5, wb6].forEach(function(wb) { assert(wb.Sheets.Sheet1['!rows']); });
});
it('should have correct points', function() {
[wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!rows']; }).forEach(function(x) {
assert.equal(x[1].hpt, 1);
assert.equal(x[2].hpt, 10);
assert.equal(x[3].hpt, 100);
});
});
it('should have correct pixels', function() {
[wb1, wb2, wb3, wb4, wb5, wb6].map(function(x) { return x.Sheets.Sheet1['!rows']; }).forEach(function(x) {
/* note: at 96 PPI hpt == hpx */
assert.equal(x[1].hpx, 1);
assert.equal(x[2].hpx, 10);
assert.equal(x[3].hpx, 100);
});
});
});
describe('merge cells',function() {
var wb1, wb2, wb3, wb4, wb5;
var bef = (function() {
@ -910,7 +957,7 @@ describe('parse features', function() {
var sheetName = 'Sheet1';
wb = X.readFile(paths.dtxlsx);
ws = wb.Sheets[sheetName];
var sheet = X.utils.sheet_to_row_object_array(ws);
var sheet = X.utils.sheet_to_json(ws);
assert.equal(sheet[3]['てすと'], '2/14/14');
});
it('cellDates should not affect formatted text', function() {
@ -1210,9 +1257,8 @@ describe('roundtrip features', function() {
});
}); });
describe('should preserve features', function() {
it('merge cells', function() {
["xlsx", "xlsb", "xlml", "ods"].forEach(function(f) {
describe('should preserve merge cells', function() {
["xlsx", "xlsb", "xlml", "ods"].forEach(function(f) { it(f, function() {
var wb1 = X.readFile(paths.mcxlsx);
var wb2 = X.read(X.write(wb1,{bookType:f,type:'binary'}),{type:'binary'});
var m1 = wb1.Sheets.Merge['!merges'].map(X.utils.encode_range);
@ -1311,6 +1357,39 @@ describe('roundtrip features', function() {
});
});
describe('should preserve column properties', function() { [
'xlml', /*'biff2', */ 'xlsx', 'xlsb', 'slk'
].forEach(function(w) { it(w, function() {
var ws1 = X.utils.aoa_to_sheet([["hpx12", "hpt24", "hpx48", "hidden"]]);
ws1['!cols'] = [{wch:9},{wpx:100},{width:80},{hidden:true}];
var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}};
var wb2 = X.read(X.write(wb1, {bookType:w, type:"buffer"}), {type:"buffer", cellStyles:true});
var ws2 = wb2.Sheets.Sheet1;
assert.equal(ws2['!cols'][3].hidden, true);
assert.equal(ws2['!cols'][0].wch, 9);
if(w == 'slk') return;
assert.equal(ws2['!cols'][1].wpx, 100);
/* xlml stores integral pixels -> approximate width */
if(w == 'xlml') assert.equal(Math.round(ws2['!cols'][2].width), 80);
else assert.equal(ws2['!cols'][2].width, 80);
}); });
});
describe('should preserve row properties', function() { [
'xlml', /*'biff2', */ 'xlsx', 'xlsb', 'slk'
].forEach(function(w) { it(w, function() {
var ws1 = X.utils.aoa_to_sheet([["hpx12"],["hpt24"],["hpx48"],["hidden"]]);
ws1['!rows'] = [{hpx:12},{hpt:24},{hpx:48},{hidden:true}];
var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}};
var wb2 = X.read(X.write(wb1, {bookType:w, type:"buffer"}), {type:"buffer", cellStyles:true});
var ws2 = wb2.Sheets.Sheet1;
assert.equal(ws2['!rows'][0].hpx, 12);
assert.equal(ws2['!rows'][1].hpt, 24);
assert.equal(ws2['!rows'][2].hpx, 48);
assert.equal(ws2['!rows'][3].hidden, true);
}); });
});
describe('should preserve cell comments', function() { [
['xlsx', paths.cstxlsx],
['xlsb', paths.cstxlsb],

@ -80,6 +80,17 @@ var N2 = 'XLSB';
var N3 = 'XLS';
var N4 = 'XML';
function get_cell(ws/*:Worksheet*/, addr/*:string*/) {
if(!Array.isArray(ws)) return ws[addr];
var a = X.utils.decode_cell(addr);
return (ws[a.r]||[])[a.c];
}
function each_cell(ws, f) {
if(Array.isArray(ws)) ws.forEach(function(row) { if(row) row.forEach(f); });
else Object.keys(ws).forEach(function(addr) { if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; f(ws[addr]); });
}
/* comments_stress_test family */
function check_comments(wb) {
var ws0 = wb.Sheets.Sheet2;
@ -516,12 +527,12 @@ describe('parse features', function() {
var wb4=X.read(fs.readFileSync(paths.swcxml), {type:"binary"});
[wb1,wb2,wb3,wb4].map(function(wb) { return wb.Sheets[sheet]; }).forEach(function(ws, i) {
assert.equal(ws.B1.c.length, 1,"must have 1 comment");
assert.equal(ws.B1.c[0].a, "Yegor Kozlov","must have the same author");
assert.equal(ws.B1.c[0].t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"), "Yegor Kozlov:\nfirst cell", "must have the concatenated texts");
assert.equal(get_cell(ws, "B1").c.length, 1,"must have 1 comment");
assert.equal(get_cell(ws, "B1").c[0].a, "Yegor Kozlov","must have the same author");
assert.equal(get_cell(ws, "B1").c[0].t, "Yegor Kozlov:\nfirst cell", "must have the concatenated texts");
if(i > 0) return;
assert.equal(ws.B1.c[0].r, '<r><rPr><b/><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t>Yegor Kozlov:</t></r><r><rPr><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t xml:space="preserve">\r\nfirst cell</t></r>', "must have the rich text representation");
assert.equal(ws.B1.c[0].h, '<span style="font-weight: bold;">Yegor Kozlov:</span><span style=""><br/>first cell</span>', "must have the html representation");
assert.equal(get_cell(ws, "B1").c[0].r, '<r><rPr><b/><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t>Yegor Kozlov:</t></r><r><rPr><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t xml:space="preserve">\r\nfirst cell</t></r>', "must have the rich text representation");
assert.equal(get_cell(ws, "B1").c[0].h, '<span style="font-size:8;"><b>Yegor Kozlov:</b></span><span style="font-size:8;"><br/>first cell</span>', "must have the html representation");
});
});
[
@ -612,11 +623,7 @@ describe('parse features', function() {
if(typeof before != 'undefined') before(bef);
else it('before', bef);
it('should have "!cols"', function() {
assert(wb1.Sheets.Sheet1['!cols']);
assert(wb2.Sheets.Sheet1['!cols']);
assert(wb3.Sheets.Sheet1['!cols']);
assert(wb4.Sheets.Sheet1['!cols']);
assert(wb5.Sheets.Sheet1['!cols']);
[wb1, wb2, wb3, wb4, wb5].forEach(function(wb) { assert(wb.Sheets.Sheet1['!cols']); });
});
it('should have correct widths', function() {
[wb1, wb2, wb3, wb4, wb5].map(function(x) { return x.Sheets.Sheet1['!cols']; }).forEach(function(x) {
@ -809,6 +816,56 @@ describe('parse features', function() {
});
});
describe('write features', function() {
describe('props', function() {
describe('core', function() {
var ws, baseprops;
var bef = (function() {
X = require(modp);
ws = X.utils.aoa_to_sheet([["a","b","c"],[1,2,3]]);
baseprops = {
Category: "C4tegory",
ContentStatus: "C0ntentStatus",
Keywords: "K3ywords",
LastAuthor: "L4stAuthor",
LastPrinted: "L4stPrinted",
RevNumber: 6969,
AppVersion: 69,
Author: "4uth0r",
Comments: "C0mments",