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`.
+
 <details>
 	<summary><b>Implementation details</b> (click to show)</summary>
 
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 <table:table-column>
+			/* TODO: outline levels */
+			case 'table-row-group': break; // 9.1.9 <table:table-row-group>
+			case 'table-column-group': break; // 9.1.10 <table:table-column-group>
 
 			case 'null-date': break; // 9.4.2 <table:null-date> 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`.
+
 <details>
 	<summary><b>Implementation details</b> (click to show)</summary>
 
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 <table:table-column>
+			/* TODO: outline levels */
+			case 'table-row-group': break; // 9.1.9 <table:table-row-group>
+			case 'table-column-group': break; // 9.1.10 <table:table-column-group>
 
 			case 'null-date': break; // 9.4.2 <table:null-date> 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 <table:table-column>
+			/* TODO: outline levels */
+			case 'table-row-group': break; // 9.1.9 <table:table-row-group>
+			case 'table-column-group': break; // 9.1.10 <table:table-column-group>
 
 			case 'null-date': break; // 9.4.2 <table:null-date> TODO: date1904