From d28cc872c7c128ef56181917ebf73815b0c3d546 Mon Sep 17 00:00:00 2001 From: Danwakeem Date: Sun, 9 Jul 2017 12:37:45 -0500 Subject: [PATCH] XLS/XLSX/XLSB Row Outline Levels (closes #723) note: @sheetjsdev rewrote implementation, original PR author used --- README.md | 5 +++++ bits/39_xlsbiff.js | 2 ++ bits/67_wsxml.js | 23 ++++++++++++++++++++-- bits/68_wsbin.js | 12 ++++++++---- bits/76_xls.js | 1 + bits/80_parseods.js | 3 +++ docbits/62_colrow.md | 5 +++++ misc/docs/README.md | 5 +++++ test.js | 33 +++++++++++++++++++++++++++++++- test_files | 2 +- tests/write.js | 2 +- xlsx.flow.js | 45 ++++++++++++++++++++++++++++++++++++-------- xlsx.js | 45 ++++++++++++++++++++++++++++++++++++-------- 13 files changed, 158 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d5f0be1..1a9417d 100644 --- a/README.md +++ b/README.md @@ -1204,9 +1204,14 @@ type RowInfo = { /* row height is specified in one of the following ways: */ hpx?: number; // height in screen pixels hpt?: number; // height in points + + level?: number; // 0-indexed outline / group level }; ``` +Note: Excel UI displays the base outline level as `1` and the max level as `8`. +The `level` field stores the base outline as `0` and the max level as `7`. +
Implementation details (click to show) diff --git a/bits/39_xlsbiff.js b/bits/39_xlsbiff.js index 0b70683..5587180 100644 --- a/bits/39_xlsbiff.js +++ b/bits/39_xlsbiff.js @@ -224,6 +224,8 @@ function parse_Row(blob, length) { blob.l += 4; // reserved(2), unused(2) var flags = blob.read_shift(1); // various flags blob.l += 3; // reserved(8), ixfe(12), flags(4) + if(flags & 0x07) z.level = flags & 0x07; + // collapsed: flags & 0x10 if(flags & 0x20) z.hidden = true; if(flags & 0x40) z.hpt = miyRw / 20; return z; diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 5a5782a..c5836a1 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -262,6 +262,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { 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(tag.outlineLevel != null) { rowrite = true; rowobj.level = +tag.outlineLevel; } if(rowrite) rows[tagr-1] = rowobj; } @@ -391,7 +392,7 @@ function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook if(_cell === undefined) continue; if((cell = write_ws_xml_cell(_cell, ref, ws, opts, idx, wb)) != null) r.push(cell); } - if(r.length > 0) { + if(r.length > 0 || rows && rows[R]) { var params = ({r:rr}/*:any*/); if(rows && rows[R]) { var row = rows[R]; @@ -400,10 +401,24 @@ function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook if (row.hpx) height = px2pt(row.hpx); else if (row.hpt) height = row.hpt; if (height > -1) { params.ht = height; params.customHeight = 1; } + if (row.level) { params.outlineLevel = row.level; } } o[o.length] = (writextag('row', r.join(""), params)); } } + if(rows) for(; R < rows.length; ++R) { + if(rows && rows[R]) { + var params = ({r:R+1}/*:any*/); + var row = rows[R]; + if(row.hidden) params.hidden = 1; + var height = -1; + if (row.hpx) height = px2pt(row.hpx); + else if (row.hpt) height = row.hpt; + if (height > -1) { params.ht = height; params.customHeight = 1; } + if (row.level) { params.outlineLevel = row.level; } + o[o.length] = (writextag('row', "", params)); + } + } return o.join(""); } @@ -429,7 +444,11 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { 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' })); + if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, { + defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', + baseColWidth:opts.sheetFormat.baseColWidth||'10', + outlineLevelRow:opts.sheetFormat.outlineLevelRow||'7' + })); if(ws['!cols'] != null && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index d95e3be..6ddafa4 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -9,6 +9,7 @@ function parse_BrtRowHdr(data, length) { data.l += 1; // TODO: top/bot padding var flags = data.read_shift(1); data.l = tgt; + if(flags & 0x07) z.level = flags & 0x07; if(flags & 0x10) z.hidden = true; if(flags & 0x20) z.hpt = miyRw / 20; return z; @@ -28,6 +29,7 @@ function write_BrtRowHdr(R/*:number*/, range, ws) { o.write_shift(1, 0); /* top/bot padding */ var flags = 0x0; + if(row.level) flags |= row.level; if(row.hidden) flags |= 0x10; if(row.hpx || row.hpt) flags |= 0x20; o.write_shift(1, flags); @@ -62,7 +64,7 @@ function write_BrtRowHdr(R/*:number*/, range, ws) { } function write_row_header(ba, ws, range, R) { var o = write_BrtRowHdr(R, range, ws); - if(o.length > 17) write_record(ba, 'BrtRowHdr', o); + if(o.length > 17 || (ws['!rows']||[])[R]) write_record(ba, 'BrtRowHdr', o); } /* [MS-XLSB] 2.4.812 BrtWsDim */ @@ -426,7 +428,7 @@ 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.hidden || val.hpt || val.level != null) { if(val.hpt) val.hpx = pt2px(val.hpt); rowinfo[val.r] = val; } @@ -684,12 +686,14 @@ function write_CELLTABLE(ba, ws/*:Worksheet*/, idx/*:number*/, opts, wb/*:Workbo var range = safe_decode_range(ws['!ref'] || "A1"), ref, rr = "", cols = []; write_record(ba, 'BrtBeginSheetData'); var dense = Array.isArray(ws); - for(var R = range.s.r; R <= range.e.r; ++R) { + var cap = range.e.r; + if(ws['!rows']) cap = Math.max(range.e.r, ws['!rows'].length - 1); + for(var R = range.s.r; R <= cap; ++R) { rr = encode_row(R); /* [ACCELLTABLE] */ /* BrtRowHdr */ write_row_header(ba, ws, range, R); - for(var C = range.s.c; C <= range.e.c; ++C) { + if(R <= range.e.r) for(var C = range.s.c; C <= range.e.c; ++C) { /* *16384CELL */ if(R === range.s.r) cols[C] = encode_col(C); ref = cols[C] + rr; diff --git a/bits/76_xls.js b/bits/76_xls.js index e5dabcc..e948151 100644 --- a/bits/76_xls.js +++ b/bits/76_xls.js @@ -521,6 +521,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Row': { var rowobj = {}; + if(val.level != null) { rowinfo[val.r] = rowobj; rowobj.level = val.level; } if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; } if(val.hpt) { rowinfo[val.r] = rowobj; diff --git a/bits/80_parseods.js b/bits/80_parseods.js index 9a5b209..3d1b808 100644 --- a/bits/80_parseods.js +++ b/bits/80_parseods.js @@ -289,6 +289,9 @@ var parse_content_xml = (function() { case 'forms': break; // 12.25.2 13.2 case 'table-column': break; // 9.1.6 + /* TODO: outline levels */ + case 'table-row-group': break; // 9.1.9 + case 'table-column-group': break; // 9.1.10 case 'null-date': break; // 9.4.2 TODO: date1904 diff --git a/docbits/62_colrow.md b/docbits/62_colrow.md index 4d9051b..71bca9a 100644 --- a/docbits/62_colrow.md +++ b/docbits/62_colrow.md @@ -75,9 +75,14 @@ type RowInfo = { /* row height is specified in one of the following ways: */ hpx?: number; // height in screen pixels hpt?: number; // height in points + + level?: number; // 0-indexed outline / group level }; ``` +Note: Excel UI displays the base outline level as `1` and the max level as `8`. +The `level` field stores the base outline as `0` and the max level as `7`. +
Implementation details (click to show) diff --git a/misc/docs/README.md b/misc/docs/README.md index 43e1598..1de47f4 100644 --- a/misc/docs/README.md +++ b/misc/docs/README.md @@ -1105,9 +1105,14 @@ type RowInfo = { /* row height is specified in one of the following ways: */ hpx?: number; // height in screen pixels hpt?: number; // height in points + + level?: number; // 0-indexed outline / group level }; ``` +Note: Excel UI displays the base outline level as `1` and the max level as `8`. +The `level` field stores the base outline as `0` and the max level as `7`. + 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 diff --git a/test.js b/test.js index 813dd8b..8dcf36c 100644 --- a/test.js +++ b/test.js @@ -97,6 +97,12 @@ var paths = { nfxlsx: dir + 'number_format.xlsm', nfxlsb: dir + 'number_format.xlsb', + olxls: dir + 'outline.xls', + olxls5: dir + 'outline.biff5', + olxlsx: dir + 'outline.xlsx', + olxlsb: dir + 'outline.xlsb', + olods: dir + 'outline.ods', + pmxls: dir + 'page_margins_2016.xls', pmxls5: dir + 'page_margins_2016_5.xls', pmxml: dir + 'page_margins_2016.xml', @@ -901,6 +907,7 @@ describe('parse features', function() { describe('row properties', function() { var wb1, wb2, wb3, wb4, wb5, wb6; + var ol1, ol2, ol3, ol4, ol5; var bef = (function() { X = require(modp); wb1 = X.readFile(paths.rhxlsx, {cellStyles:true}); @@ -909,6 +916,11 @@ describe('parse features', function() { wb4 = X.readFile(paths.rhxls5, {cellStyles:true}); wb5 = X.readFile(paths.rhxml, {cellStyles:true}); wb6 = X.readFile(paths.rhslk, {cellStyles:true}); + ol1 = X.readFile(paths.olxlsx, {cellStyles:true}); + ol2 = X.readFile(paths.olxlsb, {cellStyles:true}); + ol3 = X.readFile(paths.olxls, {cellStyles:true}); + ol4 = X.readFile(paths.olxls5, {cellStyles:true}); + ol5 = X.readFile(paths.olods, {cellStyles:true}); }); if(typeof before != 'undefined') before(bef); else it('before', bef); @@ -930,6 +942,22 @@ describe('parse features', function() { assert.equal(x[3].hpx, 100); }); }); + it('should have correct outline levels', function() { + /* TODO: ODS */ + [ol1, ol2, ol3, ol4/*, ol5*/].map(function(x) { return x.Sheets.Sheet1; }).forEach(function(ws) { + var rows = ws['!rows']; + for(var i = 0; i < 29; ++i) { + var cell = get_cell(ws, "A" + X.utils.encode_row(i)); + var lvl = (rows[i]||{}).level||0; + if(!cell || cell.t == 's') assert.equal(lvl, 0); + else if(cell.t == 'n') { + if(cell.v == 0) assert.equal(lvl, 0); + else assert.equal(lvl, cell.v); + } + } + assert.equal(rows[29].level, 7); + }); + }); }); describe('merge cells',function() { @@ -1400,18 +1428,21 @@ describe('roundtrip features', function() { }); }); }); + /* TODO: ODS and BIFF5/8 */ 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}]; + for(var i = 0; i <= 7; ++i) ws1['!rows'].push({level:i}); var wb1 = {SheetNames:["Sheet1"], Sheets:{Sheet1:ws1}}; - var wb2 = X.read(X.write(wb1, {bookType:w, type:"buffer"}), {type:"buffer", cellStyles:true}); + var wb2 = X.read(X.write(wb1, {bookType:w, type:"buffer", cellStyles:true}), {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); + if(w == 'xlsb' || w == 'xlsx') for(i = 0; i <= 7; ++i) assert.equal((ws2['!rows'][4+i]||{}).level||0, i); }); }); }); diff --git a/test_files b/test_files index 249b005..d0f58a9 160000 --- a/test_files +++ b/test_files @@ -1 +1 @@ -Subproject commit 249b005fddf7cea0b2c2d1aff5a2414e47e70c0e +Subproject commit d0f58a9e4b0519a513a1e44d1b28109fd4a8e13e diff --git a/tests/write.js b/tests/write.js index 0637e62..5e9d2d1 100644 --- a/tests/write.js +++ b/tests/write.js @@ -25,7 +25,7 @@ var wsrows = [ {hpt: 12}, // "points" {hpx: 16}, // "pixels" , - {hpx: 24}, + {hpx: 24, level:3}, {hidden: true}, // hide row {hidden: false} ]; diff --git a/xlsx.flow.js b/xlsx.flow.js index 6af38d5..883d9b4 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -4292,6 +4292,8 @@ function parse_Row(blob, length) { blob.l += 4; // reserved(2), unused(2) var flags = blob.read_shift(1); // various flags blob.l += 3; // reserved(8), ixfe(12), flags(4) + if(flags & 0x07) z.level = flags & 0x07; + // collapsed: flags & 0x10 if(flags & 0x20) z.hidden = true; if(flags & 0x40) z.hpt = miyRw / 20; return z; @@ -5730,7 +5732,8 @@ var PRN = (function() { function finish_cell() { var s = str.slice(start, end); var cell = ({}/*:any*/); - if(s.charCodeAt(0) == 0x3D) { cell.t = 'n'; cell.f = s.substr(1); } + if(o.raw) { cell.t = 's'; cell.v = s; } + else if(s.charCodeAt(0) == 0x3D) { cell.t = 'n'; cell.f = s.substr(1); } else if(s == "TRUE") { cell.t = 'b'; cell.v = true; } else if(s == "FALSE") { cell.t = 'b'; cell.v = false; } else if(!isNaN(v = +s)) { cell.t = 'n'; cell.w = s; cell.v = v; } @@ -5824,7 +5827,6 @@ function read_wb_ID(d, opts) { return PRN.to_workbook(d, opts); } } - var WK_ = (function() { function lotushopper(data, cb/*:RecordHopperCB*/, opts/*:any*/) { if(!data) return; @@ -10999,6 +11001,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { 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(tag.outlineLevel != null) { rowrite = true; rowobj.level = +tag.outlineLevel; } if(rowrite) rows[tagr-1] = rowobj; } @@ -11128,7 +11131,7 @@ function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook if(_cell === undefined) continue; if((cell = write_ws_xml_cell(_cell, ref, ws, opts, idx, wb)) != null) r.push(cell); } - if(r.length > 0) { + if(r.length > 0 || rows && rows[R]) { var params = ({r:rr}/*:any*/); if(rows && rows[R]) { var row = rows[R]; @@ -11137,10 +11140,24 @@ function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook if (row.hpx) height = px2pt(row.hpx); else if (row.hpt) height = row.hpt; if (height > -1) { params.ht = height; params.customHeight = 1; } + if (row.level) { params.outlineLevel = row.level; } } o[o.length] = (writextag('row', r.join(""), params)); } } + if(rows) for(; R < rows.length; ++R) { + if(rows && rows[R]) { + var params = ({r:R+1}/*:any*/); + var row = rows[R]; + if(row.hidden) params.hidden = 1; + var height = -1; + if (row.hpx) height = px2pt(row.hpx); + else if (row.hpt) height = row.hpt; + if (height > -1) { params.ht = height; params.customHeight = 1; } + if (row.level) { params.outlineLevel = row.level; } + o[o.length] = (writextag('row', "", params)); + } + } return o.join(""); } @@ -11166,7 +11183,11 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { 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' })); + if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, { + defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', + baseColWidth:opts.sheetFormat.baseColWidth||'10', + outlineLevelRow:opts.sheetFormat.outlineLevelRow||'7' + })); if(ws['!cols'] != null && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); @@ -11260,6 +11281,7 @@ function parse_BrtRowHdr(data, length) { data.l += 1; // TODO: top/bot padding var flags = data.read_shift(1); data.l = tgt; + if(flags & 0x07) z.level = flags & 0x07; if(flags & 0x10) z.hidden = true; if(flags & 0x20) z.hpt = miyRw / 20; return z; @@ -11279,6 +11301,7 @@ function write_BrtRowHdr(R/*:number*/, range, ws) { o.write_shift(1, 0); /* top/bot padding */ var flags = 0x0; + if(row.level) flags |= row.level; if(row.hidden) flags |= 0x10; if(row.hpx || row.hpt) flags |= 0x20; o.write_shift(1, flags); @@ -11313,7 +11336,7 @@ function write_BrtRowHdr(R/*:number*/, range, ws) { } function write_row_header(ba, ws, range, R) { var o = write_BrtRowHdr(R, range, ws); - if(o.length > 17) write_record(ba, 'BrtRowHdr', o); + if(o.length > 17 || (ws['!rows']||[])[R]) write_record(ba, 'BrtRowHdr', o); } /* [MS-XLSB] 2.4.812 BrtWsDim */ @@ -11677,7 +11700,7 @@ 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.hidden || val.hpt || val.level != null) { if(val.hpt) val.hpx = pt2px(val.hpt); rowinfo[val.r] = val; } @@ -11935,12 +11958,14 @@ function write_CELLTABLE(ba, ws/*:Worksheet*/, idx/*:number*/, opts, wb/*:Workbo var range = safe_decode_range(ws['!ref'] || "A1"), ref, rr = "", cols = []; write_record(ba, 'BrtBeginSheetData'); var dense = Array.isArray(ws); - for(var R = range.s.r; R <= range.e.r; ++R) { + var cap = range.e.r; + if(ws['!rows']) cap = Math.max(range.e.r, ws['!rows'].length - 1); + for(var R = range.s.r; R <= cap; ++R) { rr = encode_row(R); /* [ACCELLTABLE] */ /* BrtRowHdr */ write_row_header(ba, ws, range, R); - for(var C = range.s.c; C <= range.e.c; ++C) { + if(R <= range.e.r) for(var C = range.s.c; C <= range.e.c; ++C) { /* *16384CELL */ if(R === range.s.r) cols[C] = encode_col(C); ref = cols[C] + rr; @@ -14524,6 +14549,7 @@ function parse_workbook(blob, options/*:ParseOpts*/)/*:Workbook*/ { } break; case 'Row': { var rowobj = {}; + if(val.level != null) { rowinfo[val.r] = rowobj; rowobj.level = val.level; } if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; } if(val.hpt) { rowinfo[val.r] = rowobj; @@ -16644,6 +16670,9 @@ var parse_content_xml = (function() { case 'forms': break; // 12.25.2 13.2 case 'table-column': break; // 9.1.6 + /* TODO: outline levels */ + case 'table-row-group': break; // 9.1.9 + case 'table-column-group': break; // 9.1.10 case 'null-date': break; // 9.4.2 TODO: date1904 diff --git a/xlsx.js b/xlsx.js index 6e503ca..3d81f76 100644 --- a/xlsx.js +++ b/xlsx.js @@ -4230,6 +4230,8 @@ function parse_Row(blob, length) { blob.l += 4; // reserved(2), unused(2) var flags = blob.read_shift(1); // various flags blob.l += 3; // reserved(8), ixfe(12), flags(4) + if(flags & 0x07) z.level = flags & 0x07; + // collapsed: flags & 0x10 if(flags & 0x20) z.hidden = true; if(flags & 0x40) z.hpt = miyRw / 20; return z; @@ -5668,7 +5670,8 @@ var PRN = (function() { function finish_cell() { var s = str.slice(start, end); var cell = ({}); - if(s.charCodeAt(0) == 0x3D) { cell.t = 'n'; cell.f = s.substr(1); } + if(o.raw) { cell.t = 's'; cell.v = s; } + else if(s.charCodeAt(0) == 0x3D) { cell.t = 'n'; cell.f = s.substr(1); } else if(s == "TRUE") { cell.t = 'b'; cell.v = true; } else if(s == "FALSE") { cell.t = 'b'; cell.v = false; } else if(!isNaN(v = +s)) { cell.t = 'n'; cell.w = s; cell.v = v; } @@ -5762,7 +5765,6 @@ function read_wb_ID(d, opts) { return PRN.to_workbook(d, opts); } } - var WK_ = (function() { function lotushopper(data, cb, opts) { if(!data) return; @@ -10933,6 +10935,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) { 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(tag.outlineLevel != null) { rowrite = true; rowobj.level = +tag.outlineLevel; } if(rowrite) rows[tagr-1] = rowobj; } @@ -11062,7 +11065,7 @@ function write_ws_xml_data(ws, opts, idx, wb, rels) { if(_cell === undefined) continue; if((cell = write_ws_xml_cell(_cell, ref, ws, opts, idx, wb)) != null) r.push(cell); } - if(r.length > 0) { + if(r.length > 0 || rows && rows[R]) { var params = ({r:rr}); if(rows && rows[R]) { var row = rows[R]; @@ -11071,10 +11074,24 @@ function write_ws_xml_data(ws, opts, idx, wb, rels) { if (row.hpx) height = px2pt(row.hpx); else if (row.hpt) height = row.hpt; if (height > -1) { params.ht = height; params.customHeight = 1; } + if (row.level) { params.outlineLevel = row.level; } } o[o.length] = (writextag('row', r.join(""), params)); } } + if(rows) for(; R < rows.length; ++R) { + if(rows && rows[R]) { + var params = ({r:R+1}); + var row = rows[R]; + if(row.hidden) params.hidden = 1; + var height = -1; + if (row.hpx) height = px2pt(row.hpx); + else if (row.hpt) height = row.hpt; + if (height > -1) { params.ht = height; params.customHeight = 1; } + if (row.level) { params.outlineLevel = row.level; } + o[o.length] = (writextag('row', "", params)); + } + } return o.join(""); } @@ -11100,7 +11117,11 @@ function write_ws_xml(idx, opts, wb, rels) { 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' })); + if(opts.sheetFormat) o[o.length] = (writextag('sheetFormatPr', null, { + defaultRowHeight:opts.sheetFormat.defaultRowHeight||'16', + baseColWidth:opts.sheetFormat.baseColWidth||'10', + outlineLevelRow:opts.sheetFormat.outlineLevelRow||'7' + })); if(ws['!cols'] != null && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols'])); @@ -11194,6 +11215,7 @@ function parse_BrtRowHdr(data, length) { data.l += 1; // TODO: top/bot padding var flags = data.read_shift(1); data.l = tgt; + if(flags & 0x07) z.level = flags & 0x07; if(flags & 0x10) z.hidden = true; if(flags & 0x20) z.hpt = miyRw / 20; return z; @@ -11213,6 +11235,7 @@ function write_BrtRowHdr(R, range, ws) { o.write_shift(1, 0); /* top/bot padding */ var flags = 0x0; + if(row.level) flags |= row.level; if(row.hidden) flags |= 0x10; if(row.hpx || row.hpt) flags |= 0x20; o.write_shift(1, flags); @@ -11247,7 +11270,7 @@ function write_BrtRowHdr(R, range, ws) { } function write_row_header(ba, ws, range, R) { var o = write_BrtRowHdr(R, range, ws); - if(o.length > 17) write_record(ba, 'BrtRowHdr', o); + if(o.length > 17 || (ws['!rows']||[])[R]) write_record(ba, 'BrtRowHdr', o); } /* [MS-XLSB] 2.4.812 BrtWsDim */ @@ -11610,7 +11633,7 @@ function parse_ws_bin(data, _opts, rels, wb, themes, styles) { 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.hidden || val.hpt || val.level != null) { if(val.hpt) val.hpx = pt2px(val.hpt); rowinfo[val.r] = val; } @@ -11868,12 +11891,14 @@ function write_CELLTABLE(ba, ws, idx, opts, wb) { var range = safe_decode_range(ws['!ref'] || "A1"), ref, rr = "", cols = []; write_record(ba, 'BrtBeginSheetData'); var dense = Array.isArray(ws); - for(var R = range.s.r; R <= range.e.r; ++R) { + var cap = range.e.r; + if(ws['!rows']) cap = Math.max(range.e.r, ws['!rows'].length - 1); + for(var R = range.s.r; R <= cap; ++R) { rr = encode_row(R); /* [ACCELLTABLE] */ /* BrtRowHdr */ write_row_header(ba, ws, range, R); - for(var C = range.s.c; C <= range.e.c; ++C) { + if(R <= range.e.r) for(var C = range.s.c; C <= range.e.c; ++C) { /* *16384CELL */ if(R === range.s.r) cols[C] = encode_col(C); ref = cols[C] + rr; @@ -14450,6 +14475,7 @@ wb.opts.Date1904 = Workbook.WBProps.date1904 = val; break; } break; case 'Row': { var rowobj = {}; + if(val.level != null) { rowinfo[val.r] = rowobj; rowobj.level = val.level; } if(val.hidden) { rowinfo[val.r] = rowobj; rowobj.hidden = true; } if(val.hpt) { rowinfo[val.r] = rowobj; @@ -16570,6 +16596,9 @@ var parse_content_xml = (function() { case 'forms': break; // 12.25.2 13.2 case 'table-column': break; // 9.1.6 + /* TODO: outline levels */ + case 'table-row-group': break; // 9.1.9 + case 'table-column-group': break; // 9.1.10 case 'null-date': break; // 9.4.2 TODO: date1904