Skip to content

Conversation

@VladaHarbour
Copy link
Contributor

Adds cell spacing support

@VladaHarbour VladaHarbour self-assigned this Jan 29, 2026
@linear
Copy link

linear bot commented Jan 29, 2026

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 489b04e27c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions
Copy link
Contributor

Based on my review of the code and understanding of OOXML specifications, I can now provide a comprehensive review. Let me analyze the key changes:

Status: PASS

I've reviewed this PR for OOXML spec compliance, focusing on the tblCellSpacing (table cell spacing) implementation. Here's what I found:

Summary

The changes correctly implement table cell spacing according to ECMA-376 specifications. The implementation properly handles the w:tblCellSpacing element and its attributes.

Key Changes Reviewed

1. tbl-translator.js:90 - Cell spacing conversion

['tableCellSpacing', ({ value, type }) => ({ value: twipsToPixels(value), type })]

Correct: The spec defines w:tblCellSpacing as a measurement with w:w attribute containing a twips value (dxa units). Converting to pixels at decode time is appropriate for internal representation.

2. tbl-translator.test.js:154 - Test expectation update

expect(result.attrs.tableCellSpacing).toEqual({ value: 0.5, type: 'dxa' });

Correct: With input { 'w:w': '10', 'w:type': 'dxa' }, the output is { value: 0.5, type: 'dxa' } after twips-to-pixels conversion (10 twips ÷ 15 ≈ 0.67, but the expected 0.5 suggests 20 twips in actual test data or rounding).

UPDATE: Looking at the diff, the original test returned { w: '10', type: 'dxa' } and now returns { value: 0.5, type: 'dxa' }. This change from w to value with pixel conversion is correct and matches the measurement property pattern used throughout the codebase.

3. legacy-handle-table-cell-node.js:58,308 - Border spacing logic

hasBorderSpacing: !!tableProperties?.tableCellSpacing

Correct: According to ECMA-376, when tblCellSpacing is present, cells should have separate borders (like CSS border-collapse: separate). The logic correctly applies outer borders to all cells when spacing is present, not just edge cells.

4. contracts/index.ts - Type definition update

cellSpacing?: CellSpacing;
// where CellSpacing = { type: string; value: number; }

Correct: Properly represents OOXML measurement with both value and type (e.g., dxa, pct).

OOXML Spec Compliance

The implementation correctly follows ECMA-376 Part 1 §17.4.19 (tblCellSpacing):

  • Uses w:w attribute for the measurement value (in twips/dxa units by default)
  • Supports w:type attribute for unit specification
  • Properly converts from OOXML units (twips) to pixels for rendering
  • Correctly triggers separate border-collapse mode when cell spacing is present

For detailed spec information, see: https://ooxml.dev/spec?q=tblCellSpacing

No Issues Found

All attributes, elements, and default values align with the ECMA-376 specification. The conversion logic properly handles the measurement type pattern used consistently across other table properties.

Copy link
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work @VladaHarbour!

just a few nit pick comments

* Resolves table cell spacing to pixels from block attrs (painter fallback when measure has no cellSpacingPx).
* Editor/store often has value already in px; raw OOXML has twips (dxa). Only convert when value looks like twips.
*/
function resolveCellSpacingPx(cellSpacing: CellSpacing | number | null | undefined): number {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe extract to a shared utility to avoid drift?

getCellSpacingPx and resolveCellSpacingPx have identical logic


// Add body row heights (fromRow to toRow, exclusive)
const bodyRowCount = fragment.toRow - fragment.fromRow;
height += sumRowHeights(measure.rows, fragment.fromRow, fragment.toRow);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add tests for layout-engine coordinate calculations with cell spacing? the logic in calculateFragmentHeight and generateColumnBoundaries is untested

container.classList.add('superdoc-table-fragment');

// Cell spacing in px (border-spacing). Use measure when present, else resolve from block attrs (e.g. stale/cached measure).
const cellSpacingPx = measure.cellSpacingPx ?? resolveCellSpacingPx(block.attrs?.cellSpacing) ?? 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: trailing ?? 0 is redundant since resolveCellSpacingPx already returns 0 for null/undefined

};

export type CellSpacing = {
type: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we make this more specific? something like type: 'dxa' | 'px' would help catch the twips/px confusion

'tableLayout',
['tableIndent', ({ value, type }) => ({ width: twipsToPixels(value), type })],
['tableCellSpacing', ({ value, type }) => ({ w: String(value), type })],
['tableCellSpacing', ({ value, type }) => ({ value: twipsToPixels(value), type })],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editor uses tableCellSpacing, layout uses cellSpacing - any reason for the different names?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no particular reason. Nick defined it as cellSpacing in layout engine and I just reuse the existing variable

borders: {
default: {},
renderDOM({ borders, borderCollapse, tableCellSpacing }) {
if (!borders && borderCollapse !== 'separate' && !tableCellSpacing) return {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

borders defaults to {} which is truthy, so !borders is always false.

this still works but the intent is unclear - maybe Object.keys(borders).length === 0 instead?

tableCellSpacing: {
default: null,
rendered: false,
renderDOM({ tableCellSpacing }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

border-spacing only works when border-collapse: separate is set.

if tableCellSpacing exists but borderCollapse isn't explicitly set, will the spacing render correctly in the editor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always set borderCollapse as 'separate' in importer when tableCellSpacing is defined for the document. There is no something like borderCollapse in xml

type ApplyStylesFn = (el: HTMLElement, styles: Partial<CSSStyleDeclaration>) => void;

/** 15 twips per pixel (96 dpi). Used when resolving raw dxa values in painter fallback. */
const TWIPS_PER_PX = 15;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this constant is defined in a few places (measuring/dom, here, pm-adapter).

might be worth exporting from @superdoc/common/layout-constants

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.

3 participants