Skip to content

fix(xlsx): paint merged ranges fully and write styled blank cells - #2

Merged
mitresthen merged 3 commits into
masterfrom
fix/merged-range-and-blank-cell-styles
Jul 2, 2026
Merged

fix(xlsx): paint merged ranges fully and write styled blank cells#2
mitresthen merged 3 commits into
masterfrom
fix/merged-range-and-blank-cell-styles

Conversation

@mitresthen

Copy link
Copy Markdown
Owner

Problem

Report headers built as title merge + styled blank spacer rows + label/value merges rendered with alternating white stripes, where the same code on exceljs produced a solid colour block (found migrating a consumer from exceljs; repro'd byte-level against exceljs output).

Two writer gaps:

  1. Merged ranges didn't propagate the master's style. Spreadsheet apps paint each underlying cell of a merge, so a fill carried only by the top-left cell rendered over one grid position — C3:F3 of a merged B3:F3 stayed white.
  2. Value-less styled cells were dropped. cell('A2').style.fill = … without a value emitted nothing, so styled spacer rows disappeared entirely (<row r="2"></row>).

Fix

  • Worksheet.merge() materializes every covered cell and shares the master's style object (exceljs semantics) — styling works before or after merging. Merges now also count toward rowCount, so addRow lands below a merged block exactly like exceljs.
  • The worksheet writer emits <c r="A2" s="3"/> for blank cells with a non-default style; default-styled blanks stay unwritten.
  • The worksheet reader keeps self-closing styled cells so those blanks round-trip through readXlsx; unstyled empties stay dropped.

Verification

  • 5 new tests in merged-styles.test.ts (exceljs-as-oracle + readXlsx round-trip + model semantics).
  • Full suite: 247 passed (242 existing — no regressions), typecheck/lint/format/build green.
  • The consumer's report-header pattern now serializes cell-for-cell identically to exceljs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JpaqFKiTLyBFcYZHuqL5G1

mitresthen and others added 3 commits July 2, 2026 13:18
Two writer gaps left branded report headers striped white where exceljs
rendered a solid block:

- merge() now materializes every covered cell and shares the master's
  style object (exceljs semantics), so a fill set on the master — before
  or after merging — paints the whole range. Spreadsheet apps render each
  underlying cell of a merge, so a master-only fill covered one position.
  Merges also count toward rowCount now, so addRow lands below a merged
  block exactly like exceljs.
- The worksheet writer emits value-less cells that carry a non-default
  style (`<c r="A2" s="3"/>`), instead of dropping them. Blank spacer
  rows styled via cell().style previously vanished from the file.
- The worksheet reader keeps self-closing styled cells so those blanks
  round-trip through readXlsx; unstyled empties stay dropped.

Verified against exceljs as oracle: the report-header pattern (title
merge + styled spacer rows + label/value merges) now serializes
cell-for-cell identically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpaqFKiTLyBFcYZHuqL5G1
<mergeCells> appears after <sheetData>, so the reader calling the new
style-propagating merge() overwrote covered-cell styles that were just
parsed — Excel writes distinct xfs per merge edge (borders), and those
must round-trip. Split out recordMerge() for the reader; public merge()
keeps the exceljs-matching propagation. Verified ExcelJS's own reader
preserves covered-cell styles the same way; regression test fails on
the old path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrxsnGbCRtq7VPskBRoSPr
@mitresthen

Copy link
Copy Markdown
Owner Author

Reviewed and hardened before merge:

  • Found one gap: the reader calls merge() when parsing <mergeCells>, which — with the new style propagation — clobbered covered cells' own parsed styles (Excel writes distinct xfs per merge edge, e.g. outline borders). Verified empirically that ExcelJS's reader preserves covered-cell styles on read. Fixed by splitting out recordMerge() (record-only) for the reader; public merge() keeps the propagation semantics this PR introduces. Regression test added — it fails on the pre-fix path.
  • Merged current master (1.0.0, browser tests, scanner) into the branch; full gate green: 259 tests, typecheck/lint/format/build/size.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JrxsnGbCRtq7VPskBRoSPr

@mitresthen
mitresthen merged commit 40cfa48 into master Jul 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant