readXlsxFile throws on inlineStr cells with no <is> child (openpyxl empty-string cells)

Summary

read-excel-file throws on any .xlsx written by openpyxl that contains an empty-string cell. A single such cell rejects the entire workbook, so no rows are returned at all.

Version tested: 9.3.4 (also present in 9.3.x generally; the inlineStr branch is unchanged since it was added).

Error: Couldn't read "inline string" cell value

Root cause

openpyxl writes a cell holding an empty string as an inline string with no child element:

<c r="A2" t="inlineStr"><is><t>Marie</t></is></c>   <!-- populated -->
<c r="B2" t="inlineStr"></c>                        <!-- holds "" -->

In source/xlsx/parseCell.js, state.inlineString is only initialised when an <is> tag opens:

} else if (tagName === 'is') {
  state.is = true;
  state.inlineString = '';
}

For the second cell no <is> ever opens, so inlineString stays undefined, and parseCellValue.js throws:

case 'inlineStr':
  value = inlineString;
  if (value === undefined) {
    throw new Error('Couldn\'t read "inline string" cell value');
  }

The throw escapes the sheet parse, so one empty-string cell fails the whole file.

Minimal reproduction

# openpyxl 3.1.5
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.append(["Nom", "Courriel"])
ws.append(["Marie", ""])          # empty string, not None
wb.save("repro.xlsx")
import readXlsxFile from 'read-excel-file/node'
await readXlsxFile('repro.xlsx')
// Error: Couldn't read "inline string" cell value

Confirmed to throw on 9.3.4 in both openpyxl's normal and write_only modes. Neither mode emits a sharedStrings.xml, so every string in these files is an inline string.

Only an empty string triggers it

I generated each case and inspected the resulting XML:

cell value XML openpyxl writes breaks
"" <c r="B2" t="inlineStr"></c> yes
None cell omitted from the row entirely no
" " <c r="B2" t="inlineStr"><is><t xml:space="preserve"> </t></is></c> no

Real-world impact

This is not a synthetic edge case. openpyxl is the standard Python route to .xlsx, and exporters commonly emit "" rather than None for a missing field.

A concrete example from our codebase: a 200-row contact export written by openpyxl 3.1.5 contains 2211 t="inlineStr" cells, of which 587 have no <is> child. Any one of them alone is fatal, so the file is simply unreadable.

Suggested fix

Treat a missing <is> on an inlineStr cell as an empty string rather than throwing. Either initialise inlineString when the cell opens with t="inlineStr", or default to '' in the inlineStr branch of parseCellValue.

The spec does expect <is> to be present, so openpyxl is arguably at fault. But the reading of a typed cell with no value is unambiguous, other readers (exceljs among them) accept it, and rejecting an entire user-uploaded workbook over it is a harsh outcome. This is the same shape as #68 (closed) (Invalid "shared" string index: undefined), where a single unresolvable string reference also fails the whole file.

If you'd rather keep the strict default, an opt-in such as { tolerateInvalidCells: true } would work just as well for us.

Current workaround

We pre-process the worksheet XML and give bare inlineStr cells an empty <is><t></t></is> before calling readSheet. It works, but it means unzipping and repacking the workbook, which we would like to drop.