diff --git a/CHANGELOG.md b/CHANGELOG.md index 5364f3c0..f0877048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.1-mki.1] +### Fixed +- Column widths & row heights parsing (cols/rows width/ht + defaults). +- More robust cell parsing: formula-first (), safe sharedStrings lookup, full inlineStr handling, and bounds checks for numFmtId and style indices. + +### Added +- Added toJson / fromJson + toString = jsonEncode(toJson()) methods +- Added generateXFile wich generate a XFile with mimeType + ## [5.0.0] - Update packages to latest versions - Migrate from `dart:html` to `package:web` diff --git a/lib/excel.dart b/lib/excel.dart index 170157e2..79202297 100644 --- a/lib/excel.dart +++ b/lib/excel.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:archive/archive.dart'; import 'package:collection/collection.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:equatable/equatable.dart'; import 'package:xml/xml.dart'; import 'src/web_helper/client_save_excel.dart' diff --git a/lib/src/excel.dart b/lib/src/excel.dart index 41d77263..8547c10a 100644 --- a/lib/src/excel.dart +++ b/lib/src/excel.dart @@ -79,7 +79,176 @@ class Excel { } factory Excel.decodeBuffer(InputStream input) { - return _newExcel(ZipDecoder().decodeStream(input)); + return _newExcel(ZipDecoder().decodeStream(input, verify: true)); + } + + factory Excel.fromJson(Map json) { + final excel = Excel.createExcel(); + + final defaultName = excel.sheets.keys.first; + final firstTarget = json.keys.first; + if (defaultName != firstTarget) { + excel.rename(defaultName, firstTarget); + } + + if (json.isEmpty) return excel; + + json.forEach((sheetName, rawRows) { + if (rawRows is! List) { + excel._availSheet(sheetName); + return; + } + + final rows = + rawRows.whereType>().toList(growable: false); + + excel._availSheet(sheetName); + + if (rows.isEmpty) return; + + final headers = rows.first.keys.toList(growable: false); + + excel.appendRow( + sheetName, + [for (final h in headers) TextCellValue(h)], + ); + + for (final record in rows) { + excel.appendRow( + sheetName, + [for (final h in headers) _toCellValue(record[h])], + ); + } + }); + + if (excel.sheets.isNotEmpty) { + excel.setDefaultSheet(excel.sheets.keys.first); + } + + return excel; + } + + static CellValue? _toCellValue(dynamic v) { + if (v == null) return null; + + if (v is CellValue) return v; + + if (v is bool) return BoolCellValue(v); + + if (v is num) { + final isInt = v == v.truncate(); + return isInt ? IntCellValue(v.toInt()) : DoubleCellValue(v.toDouble()); + } + + if (v is String) { + final s = v.trim(); + + if (s.startsWith('=')) return FormulaCellValue(s); + + final b = bool.tryParse(s); + if (b != null) return BoolCellValue(b); + + final i = int.tryParse(s); + if (i != null) return IntCellValue(i); + final d = double.tryParse(s); + if (d != null) return DoubleCellValue(d); + + final timeRe = RegExp(r'^\d{2}:\d{2}:\d{2}(?:\.\d+)?$'); + if (timeRe.hasMatch(s)) { + final parts = s.split(':'); + final secParts = parts[2].split('.'); + final h = int.parse(parts[0]); + final m = int.parse(parts[1]); + final sec = int.parse(secParts[0]); + final ms = secParts.length > 1 + ? int.parse(secParts[1].padRight(3, '0').substring(0, 3)) + : 0; + return TimeCellValue(hour: h, minute: m, second: sec, millisecond: ms); + } + + final dt = DateTime.tryParse(s); + if (dt != null) { + final u = dt.toUtc(); + final isMidnight = u.hour == 0 && + u.minute == 0 && + u.second == 0 && + u.millisecond == 0 && + u.microsecond == 0; + if (isMidnight) { + return DateCellValue(year: u.year, month: u.month, day: u.day); + } else { + return DateTimeCellValue( + year: u.year, + month: u.month, + day: u.day, + hour: u.hour, + minute: u.minute, + second: u.second, + millisecond: u.millisecond, + microsecond: u.microsecond, + ); + } + } + + return TextCellValue(s); + } + + return TextCellValue(v.toString()); + } + + Map toJson() { + int index = 0; + Map json = {}; + + for (final sheet in this.sheets.keys) { + List keys = []; + json.addAll({sheet: []}); + final rows = sheets[sheet]?.rows ?? []; + + for (final row in rows) { + try { + if (index == 0) { + keys = row; + index++; + } else { + Map temp = {}; + int index = 0; + String tk = ''; + + for (final key in keys) { + if (key != null && key.value != null) { + tk = key.value.toString(); + + temp[tk] = switch (row[index]?.value) { + (final v) when v is IntCellValue => v.value, + (final v) when v is DoubleCellValue => v.value, + (final v) when v is BoolCellValue => v.value, + (final v) when v is FormulaCellValue => v.formula, + (final v) when v is TextCellValue => v.value.toString(), + (final v) when v is TimeCellValue => v.toString(), + (final v) when v is DateCellValue => v.toString(), + (final v) when v is DateTimeCellValue => v.toString(), + _ => null, + }; + + index++; + } + } + + json[sheet].add(temp); + } + } catch (e) { + rethrow; + } + } + index = 0; + } + return json; + } + + @override + String toString() { + return jsonEncode(toJson()); } /// @@ -331,11 +500,16 @@ class Excel { /// ///``` List? save({String fileName = 'FlutterExcel.xlsx'}) { - Save s = Save._(this, parser); - var onValue = s._save(); + var onValue = encode(); return helper.SavingHelper.saveFile(onValue, fileName); } + /// Generate XFile with mimeType and path = fileName. + XFile? generateXFile({String fileName = 'FlutterExcel.xlsx'}) { + var onValue = encode(); + return helper.SavingHelper.generateXFile(onValue, fileName); + } + /// ///returns the name of the `defaultSheet` (the sheet which opens firstly when xlsx file is opened in `excel based software`). /// diff --git a/lib/src/parser/parse.dart b/lib/src/parser/parse.dart index b6a71735..50b733fa 100644 --- a/lib/src/parser/parse.dart +++ b/lib/src/parser/parse.dart @@ -539,6 +539,13 @@ class Parser { var content = XmlDocument.parse(utf8.decode(file.content)); var worksheet = content.findElements('worksheet').first; + var cols = worksheet.findAllElements('cols').toList(); + if (cols.isNotEmpty) { + var colsNode = cols.first; + var columns = _findColumns(colsNode); + _parseColumns(columns, sheetObject, name); + } + /// /// check for right to left view /// @@ -548,11 +555,11 @@ class Parser { var rtl = sheetViewNode.getAttribute('rightToLeft'); sheetObject.isRTL = rtl != null && rtl == '1'; } + var sheet = worksheet.findElements('sheetData').first; - _findRows(sheet).forEach((child) { - _parseRow(child, sheetObject, name); - }); + var rows = _findRows(sheet); + _parseRows(rows, sheetObject, name); _parseHeaderFooter(worksheet, sheetObject); _parseColWidthsRowHeights(worksheet, sheetObject); @@ -565,99 +572,117 @@ class Parser { _normalizeTable(sheetObject); } - _parseRow(XmlElement node, Sheet sheetObject, String name) { - var rowIndex = (_getRowNumber(node) ?? -1) - 1; - if (rowIndex < 0) { - return; + _parseColumns(Iterable columns, Sheet sheetObject, String name) { + for (int i = 0; i < columns.length; i++) { + var autofit = _getColumnAutofit(columns.elementAt(i)); + _excel.sheets[name]!._columnAutoFit + .update(i, (value) => autofit, ifAbsent: () => autofit); + + var width = _getColumnWidth(columns.elementAt(i)); + if (width != null) { + _excel.sheets[name]!._columnWidths + .update(i, (value) => width, ifAbsent: () => width); + } } + } + + _parseRows(Iterable rows, Sheet sheetObject, String name) { + rows.forEach((row) { + var rowIndex = (_getRowNumber(row) ?? -1) - 1; + if (rowIndex < 0) { + return; + } + + var height = _getRowHeight(row); + + if (height != null) { + _excel.sheets[name]!._rowHeights + .update(rowIndex, (value) => height, ifAbsent: () => height); + } - _findCells(node).forEach((child) { - _parseCell(child, sheetObject, rowIndex, name); + var cells = _findCells(row); + cells.forEach((cell) => _parseCell(cell, sheetObject, rowIndex, name)); }); } void _parseCell( XmlElement node, Sheet sheetObject, int rowIndex, String name) { - int? columnIndex = _getCellNumber(node); - if (columnIndex == null) { - return; - } - - var s1 = node.getAttribute('s'); - int s = 0; - if (s1 != null) { - try { - s = int.parse(s1.toString()); - } catch (_) {} + final columnIndex = _getCellNumber(node); + if (columnIndex == null) return; - String rC = node.getAttribute('r').toString(); + // style index + final styleAttr = node.getAttribute('s'); + final styleIndex = int.tryParse(styleAttr ?? '') ?? 0; - if (_excel._cellStyleReferenced[name] == null) { - _excel._cellStyleReferenced[name] = {rC: s}; - } else { - _excel._cellStyleReferenced[name]![rC] = s; - } + // style by rc if possible + final rc = node.getAttribute('r'); + if (rc != null) { + (_excel._cellStyleReferenced[name] ??= {})[rc] = styleIndex; } + // formula except t + final fNode = node.findElements('f').firstOrNull; + CellValue? value; - String? type = node.getAttribute('t'); - - switch (type) { - // sharedString - case 's': - final sharedString = _excel._sharedStrings - .value(int.parse(_parseValue(node.findElements('v').first))); - value = TextCellValue.span(sharedString!.textSpan); - break; - // boolean - case 'b': - value = BoolCellValue(_parseValue(node.findElements('v').first) == '1'); - break; - // error - case 'e': - // formula - case 'str': - value = FormulaCellValue(_parseValue(node.findElements('v').first)); - break; - // inline string - case 'inlineStr': - // - // Dartonico - // - value = TextCellValue(_parseValue(node.findAllElements('t').first)); - break; - // number - case 'n': - default: - var formulaNode = node.findElements('f'); - if (formulaNode.isNotEmpty) { - value = FormulaCellValue(_parseValue(formulaNode.first).toString()); - } else { + if (fNode != null) { + value = FormulaCellValue(_parseValue(fNode)); + } else { + final t = node.getAttribute('t'); + switch (t) { + // sharedString + case 's': final vNode = node.findElements('v').firstOrNull; - if (vNode == null) { - value = null; - } else if (s1 != null) { - final v = _parseValue(vNode); - var numFmtId = _excel._numFmtIds[s]; - final numFormat = _excel._numFormats.getByNumFmtId(numFmtId); - if (numFormat == null) { - assert( - false, 'found no number format spec for numFmtId $numFmtId'); - value = NumFormat.defaultNumeric.read(v); - } else { - value = numFormat.read(v); - } + if (vNode != null) { + final idx = int.tryParse(_parseValue(vNode)); + final sh = idx != null ? _excel._sharedStrings.value(idx) : null; + if (sh != null) value = TextCellValue.span(sh.textSpan); + } + break; + // boolean + case 'b': + final v = node.findElements('v').firstOrNull; + value = BoolCellValue(v != null && _parseValue(v) == '1'); + break; + // error + case 'e': + final v = node.findElements('v').firstOrNull; + value = v != null ? TextCellValue(_parseValue(v)) : null; + break; + // formula + case 'str': + final v = node.findElements('v').firstOrNull; + value = v != null ? FormulaCellValue(_parseValue(v)) : null; + break; + // inline string + case 'inlineStr': + final text = node.findAllElements('t').map(_parseValue).join(); + value = TextCellValue(text); + break; + // numeric + default: + final vNode = node.findElements('v').firstOrNull; + if (vNode != null) { + final raw = _parseValue(vNode); + final fmtId = + (styleIndex >= 0 && styleIndex < _excel._numFmtIds.length) + ? _excel._numFmtIds[styleIndex] + : null; + final fmt = + fmtId != null ? _excel._numFormats.getByNumFmtId(fmtId) : null; + value = (fmt ?? NumFormat.defaultNumeric).read(raw); } else { - final v = _parseValue(vNode); - value = NumFormat.defaultNumeric.read(v); + value = null; } - } + break; + } } sheetObject.updateCell( CellIndex.indexByColumnRow(columnIndex: columnIndex, rowIndex: rowIndex), value, - cellStyle: _excel._cellStyleList[s], + cellStyle: (styleIndex >= 0 && styleIndex < _excel._cellStyleList.length) + ? _excel._cellStyleList[styleIndex] + : null, ); } diff --git a/lib/src/save/save_file.dart b/lib/src/save/save_file.dart index ee4ab490..109509a4 100644 --- a/lib/src/save/save_file.dart +++ b/lib/src/save/save_file.dart @@ -6,6 +6,7 @@ class Save { final List _innerCellStyle = []; final Parser parser; + Save._(this._excel, this.parser); void _addNewColumn(XmlElement columns, int min, int max, double width) { @@ -543,6 +544,7 @@ class Save { _processStylesFile(); } _setSheetElements(); + if (_excel._defaultSheet != null) { _setDefaultSheet(_excel._defaultSheet); } @@ -635,6 +637,7 @@ class Save { if (sheetObject._sheetData[rowIndex] == null) { continue; } + var foundRow = _createNewRow( _excel._sheets[sheetName]! as XmlElement, rowIndex, height); for (var columnIndex = 0; diff --git a/lib/src/sheet/data_model.dart b/lib/src/sheet/data_model.dart index 3b2caa68..947aa113 100644 --- a/lib/src/sheet/data_model.dart +++ b/lib/src/sheet/data_model.dart @@ -92,9 +92,9 @@ class Data extends Equatable { } /// sets the user defined CellStyle in this current cell - set cellStyle(CellStyle? _) { + set cellStyle(CellStyle? cellStyle) { _sheet._excel._styleChanges = true; - _cellStyle = _; + _cellStyle = cellStyle; } @override diff --git a/lib/src/sheet/sheet.dart b/lib/src/sheet/sheet.dart index c1a6906f..fba2313c 100644 --- a/lib/src/sheet/sheet.dart +++ b/lib/src/sheet/sheet.dart @@ -73,6 +73,7 @@ class Sheet { } /// copy the data objects into a temp folder and then while putting it into `_sheetData` change the data objects references. + if (sh != null) { _sheetData = >{}; Map> temp = Map>.from(sh); diff --git a/lib/src/utilities/utility.dart b/lib/src/utilities/utility.dart index 36900be4..1c09a43a 100644 --- a/lib/src/utilities/utility.dart +++ b/lib/src/utilities/utility.dart @@ -41,6 +41,10 @@ Iterable _findRows(XmlElement table) { return table.findElements('row'); } +Iterable _findColumns(XmlElement table) { + return table.findElements('col'); +} + Iterable _findCells(XmlElement row) { return row.findElements('c'); } @@ -57,6 +61,18 @@ int? _getRowNumber(XmlElement row) { return int.tryParse(row.getAttribute('r').toString()); } +double? _getRowHeight(XmlElement row) { + return double.tryParse(row.getAttribute('ht').toString()); +} + +double? _getColumnWidth(XmlElement column) { + return double.tryParse(column.getAttribute('width').toString()); +} + +bool _getColumnAutofit(XmlElement column) { + return int.tryParse(column.getAttribute('bestFit').toString()) == 1 ? true : false; +} + int _checkPosition(List list, CellStyle cellStyle) { return list.indexOf(cellStyle); } diff --git a/lib/src/web_helper/client_save_excel.dart b/lib/src/web_helper/client_save_excel.dart index c7818b89..c1ac5706 100644 --- a/lib/src/web_helper/client_save_excel.dart +++ b/lib/src/web_helper/client_save_excel.dart @@ -1,6 +1,24 @@ +import 'dart:typed_data'; + +import 'package:cross_file/cross_file.dart'; + class SavingHelper { // A wrapper to save the excel file in client static List? saveFile(List? val, String fileName) { return val; } + + static XFile? generateXFile(List? val, String fileName) { + if (val == null) return null; + final bytes = Uint8List.fromList(val); + return XFile.fromData( + bytes, + name: fileName, + length: bytes.length, + lastModified: DateTime.now(), + mimeType: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + path: fileName, + ); + } } diff --git a/lib/src/web_helper/web_save_excel_browser.dart b/lib/src/web_helper/web_save_excel_browser.dart index 861afbbd..2ae1fda0 100644 --- a/lib/src/web_helper/web_save_excel_browser.dart +++ b/lib/src/web_helper/web_save_excel_browser.dart @@ -1,6 +1,7 @@ import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:cross_file/cross_file.dart'; import 'package:web/web.dart'; // A wrapper to save the excel file in browser @@ -26,4 +27,18 @@ class SavingHelper { URL.revokeObjectURL(url); return val; } + + static XFile? generateXFile(List? val, String fileName) { + if (val == null) return null; + final bytes = Uint8List.fromList(val); + return XFile.fromData( + bytes, + name: fileName, + length: bytes.length, + lastModified: DateTime.now(), + mimeType: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + path: fileName, + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index c2d3c57f..e46db204 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: excel description: A flutter and dart library for reading, creating, editing and updating excel sheets with compatible both on client and server side. -version: 5.0.0 +version: 5.0.1-mki.1 homepage: https://github.com/justkawal/excel topics: - excel @@ -18,7 +18,7 @@ dependencies: xml: ">=5.0.0 <7.0.0" collection: ^1.15.0 equatable: ^2.0.0 + cross_file: ^0.3.4+2 dev_dependencies: - test: ^1.23.0 - lints: ^5.1.1 + test: ^1.26.3 diff --git a/test/excel_test.dart b/test/excel_test.dart index 2eff9686..4356a4d5 100644 --- a/test/excel_test.dart +++ b/test/excel_test.dart @@ -1101,4 +1101,200 @@ void main() { reason: 'Decoding the file should not throw any exception', ); }); + + /// mki + test('JSON view stable across encode/decode', () { + final src = { + 'S': [ + { + 'X': 1, + 'Y': 2.5, + 'Z': '=X1+Y1', + 'T': '08:00:00', + 'D': '2024-01-01T00:00:00.000Z' + }, + { + 'X': 3, + 'Y': 4.5, + 'Z': '=X2+Y2', + 'T': '12:34:56', + 'D': '2024-01-02T10:11:12.000Z' + }, + ], + }; + + final excel = Excel.fromJson(src); + final bytes = excel.encode()!; + final excel2 = Excel.decodeBytes(bytes); + final out = excel2.toJson(); + + expect(out, equals(src)); + }); + + test('toJson/fromJson round-trip', () { + final file = './test/test_resources/example.xlsx'; + final bytes = File(file).readAsBytesSync(); + final excel = Excel.decodeBytes(bytes); + + final out = excel.toJson(); + final excel2 = Excel.fromJson(out); + + final out2 = excel2.toJson(); + + expect(out, equals(out2)); + }); + + test('fromJson string coercion & formula', () { + final ex = Excel.fromJson({ + 'S': [ + {'B': 'true', 'I': '42', 'F': '3.14', 'Z': '=A1+B1'} + ] + }); + final r = ex['S'].rows[1]; + expect(r[0]?.value, BoolCellValue(true)); + expect(r[1]?.value, IntCellValue(42)); + expect(r[2]?.value, DoubleCellValue(3.14)); + expect(r[3]?.value, const FormulaCellValue('=A1+B1')); + }); + + test('generateXFile returns non-empty XFile with correct name', () async { + final file = './test/test_resources/example.xlsx'; + final bytes = File(file).readAsBytesSync(); + final excel = Excel.decodeBytes(bytes); + + final xf = excel.generateXFile(fileName: 'test.xlsx')!; + expect(xf.name, 'test.xlsx'); + expect(xf.mimeType, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + }); + + test('generateXFile uses provided bytes', () async { + final ex = Excel.createExcel(); + final bytes = ex.encode()!; + final xf = ex.generateXFile(fileName: 'a.xlsx')!; + expect(await xf.length(), bytes.length); + }); + + test('formula-first parsing', () { + final excel = Excel.createExcel(); + final sheet = excel['Sheet1']; + final cell = sheet.cell(CellIndex.indexByString('B2')); + cell.setFormula('=SUM(1,2)'); + + final bytes = excel.encode()!; + final excel2 = Excel.decodeBytes(bytes); + final v = excel2['Sheet1'].cell(CellIndex.indexByString('B2')).value; + + expect(v, equals(const FormulaCellValue('=SUM(1,2)'))); + }); + + group('t="inlineStr"', () { + List _rewriteZipEntry( + List zipBytes, String path, String Function(String) transform) { + final arc = ZipDecoder().decodeBytes(zipBytes); + final out = Archive(); + for (final f in arc.files) { + if (f.name == path) { + final xml = utf8.decode(f.content); + final updated = transform(xml); + final bytes = utf8.encode(updated); + out.addFile(ArchiveFile(path, bytes.length, bytes)); + } else { + out.addFile(ArchiveFile.noCompress(f.name, f.size, f.content)); + } + } + return ZipEncoder().encode(out)!; + } + + List _patchInlineStr(List bytes, String cellRef, + {String? plain, String? xmlSpacePreserve, List? runs}) { + return _rewriteZipEntry( + bytes, + 'xl/worksheets/sheet1.xml', + (xml) { + final doc = XmlDocument.parse(xml); + final c = doc.findAllElements('c').firstWhere( + (e) => e.getAttribute('r') == cellRef, + orElse: () => throw StateError('$cellRef not found'), + ); + + c.children.clear(); + c.attributes + .removeWhere((a) => a.name.local == 't' || a.name.local == 's'); + c.setAttribute('t', 'inlineStr'); + + final isNode = XmlElement(XmlName('is')); + + if (plain != null) { + isNode.children.add( + XmlElement(XmlName('t'), [], [XmlText(plain)]), + ); + } else if (xmlSpacePreserve != null) { + isNode.children.add( + XmlElement( + XmlName('t'), + [XmlAttribute(XmlName('xml:space'), 'preserve')], + [XmlText(xmlSpacePreserve)], + ), + ); + } else if (runs != null && runs.isNotEmpty) { + for (final text in runs) { + isNode.children.add( + XmlElement(XmlName('r'), [], [ + XmlElement(XmlName('t'), [], [XmlText(text)]), + ]), + ); + } + } else { + throw ArgumentError('Provide plain or xmlSpacePreserve or runs'); + } + + c.children.add(isNode); + return doc.toXmlString(); + }, + ); + } + + test('plain inlineStr', () { + final excel = Excel.createExcel(); + excel['Sheet1'] + .appendRow([TextCellValue('X'), TextCellValue('Y')]); // A1,B1 + final bytes = excel.encode()!; + + final patched = _patchInlineStr(bytes, 'B1', plain: 'HelloInline'); + final excel2 = Excel.decodeBytes(patched); + + expect( + excel2['Sheet1'].cell(CellIndex.indexByString('B1')).value, + TextCellValue('HelloInline'), + ); + }); + + test('inlineStr xml:space="preserve"', () { + final excel = Excel.createExcel(); + excel['Sheet1'] + .appendRow([TextCellValue('X'), TextCellValue('Y')]); // A1,B1 + final bytes = excel.encode()!; + + final s = ' Hello inline \n with spaces '; + final patched = _patchInlineStr(bytes, 'B1', xmlSpacePreserve: s); + final excel2 = Excel.decodeBytes(patched); + final v = excel2['Sheet1'].cell(CellIndex.indexByString('B1')).value; + + expect(v, TextCellValue(s)); + }); + + test('inlineStr rich text (multiple )', () { + final excel = Excel.createExcel(); + excel['Sheet1'] + .appendRow([TextCellValue('X'), TextCellValue('Y')]); // A1,B1 + final bytes = excel.encode()!; + + final pieces = ['Hello', ' ', 'Inline', ' ', '🚀']; + final patched = _patchInlineStr(bytes, 'B1', runs: pieces); + final excel2 = Excel.decodeBytes(patched); + final v = excel2['Sheet1'].cell(CellIndex.indexByString('B1')).value; + expect(v, TextCellValue('Hello Inline 🚀')); + }); + }); }