Skip to content

feat(converter): preserve row-level content control wrappers in table round-trip (SD-3291)#3567

Open
luccas-harbour wants to merge 6 commits into
mainfrom
luccas/sd-3291-bug-content-controls-that-wrap-a-whole-table-row-do-not
Open

feat(converter): preserve row-level content control wrappers in table round-trip (SD-3291)#3567
luccas-harbour wants to merge 6 commits into
mainfrom
luccas/sd-3291-bug-content-controls-that-wrap-a-whole-table-row-do-not

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

Word lets a user wrap a whole table row in a content control (a <w:sdt> around a <w:tr>) — for example, a repeating section that controls a row. Before this change, the table importer read rows directly and ignored any row wrapped in <w:sdt>, so the wrapped row never entered the editable document data: it was missing from the editor, absent from text search, and dropped on export back to DOCX.

This is the row-level analog of the cell-level content control bug fixed in SD-3289 / IT-1119. This PR extends that fix to rows and factors the now-shared logic into common helpers so the two scopes stay in lockstep.

What changed

New shared helperssuper-converter/v3/handlers/w/sdt/helpers/sdt-envelope.js

  • getSdtEnvelopeParts — pulls sdtPr / sdtEndPr / sdtContent out of a <w:sdt> (replaces inline find(w:sdtContent) across five SDT handlers).
  • normalizeSdtContentChildren — on import, unwraps a single child (w:tr or w:tc) from its <w:sdt> and preserves the envelope (sdtPr, sdtEndPr, and any contentBefore / contentAfter siblings) as opaque metadata.
  • wrapSdtContentChildren — on export, reconstructs the <w:sdt> envelope around the exported child from that metadata.

Row-level wiring

  • tbl-translator.js attaches rowSdt metadata on import and re-wraps rows on export; row enumeration (incl. the first-row width helper) now goes through the normalizer so wrapped rows are included.
  • table-row.js registers a rowSdt schema attribute (default: null, rendered: false).
  • legacy-handle-table-cell-node.js now finds vMerge continuations inside row-level SDT wrappers.

Cell-level path now routes through the same shared helpers (tr-translator.js, row-cell-children.js), and additionally gained contentBefore / contentAfter sibling preservation as a side effect of the extraction.

Typesnode-attributes.ts introduces a shared SdtMetadata<Scope> with CellSdtMetadata / RowSdtMetadata aliases; adds rowSdt to TableRowAttrs.

Tests

  • table-row-children.test.js — normalizer unit tests (direct rows, single-row unwrap + metadata, content siblings, multi-row wrappers, empty SDTs).
  • tbl-translator.row-sdt.integration.test.js — full encode → export round-trip with no mocks; wrapped row renders, text is findable, <w:sdt> reconstructed.
  • tbl-translator.test.js, tr-translator.cell-sdt.test.js — encode/decode coverage for both scopes.
  • legacy-handle-table-cell-node.test.js — vMerge across cell- and row-level SDT wrappers.

Known limitations

  • Multi-row wrappers: a single <w:sdt> wrapping more than one <w:tr> imports defensively — the rows survive but the wrapper metadata is dropped (SuperDoc doesn't yet model a single control spanning rows). Documented and tested.
  • Repeating sections: the fix handles a <w:sdt> directly wrapping one <w:tr>. If a given Word version emits the nested repeatingSection > repeatingSectionItem > w:tr shape, that row is not yet unwrapped — worth confirming against a real fixture.

Content controls that wrap a whole table row (<w:sdt> containing a
<w:tr>, ECMA-376 §17.5.2.30 CT_SdtRow) were dropped on import, breaking
round-trip. Add normalizeTableRowChildren to unwrap single-row SDT
wrappers and attach their w:sdtPr/w:sdtEndPr as rowSdt metadata on the
tableRow node, then rebuild the <w:sdt> envelope on export. Multi-row
wrappers are imported defensively by emitting inner rows in order
without wrapper metadata.

Extract the shared getSdtEnvelopeParts helper and reuse it from the
cell-level path. Route table-row enumeration through the normalizer in
the table translator and legacy cell handler so rowspan and width
calculations see SDT-wrapped rows. Generalize CellSdtMetadata into a
scoped SdtMetadata<Scope> type and add RowSdtMetadata.
@luccas-harbour luccas-harbour requested a review from a team as a code owner May 29, 2026 15:08
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 29, 2026

SD-3291

@github-actions
Copy link
Copy Markdown
Contributor

The calls keep returning "not granted yet," so I'll pause here.

To run the spec verification I need you to approve the mcp__ecma-spec__* tools (all read-only ECMA-376 lookups). Once you approve, I'll verify these claims from the PR:

  • w:sdt as a direct child of w:tbl (the CT_SdtRow / row-level SDT path the PR adds) — §17.5.2.30
  • w:sdt as a direct child of w:tr (the existing CT_SdtCell cell-level path it refactors) — §17.5.2.32
  • w:sdtPr / w:sdtEndPr / w:sdtContent envelope structure and ordering (the new sdt-envelope.js re-wrapping logic)
  • w:docPartObj / w:docPartGallery and its w:val attribute
  • w:sdtPr children referenced (w:id, w:tag, w:alias, w:date, w:rPr, lock) — that they're real and the w:val attributes are correct

Go ahead and grant access (or tell me to proceed using my own knowledge of the spec without the tools), and I'll produce the PASS/FAIL review.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 20 files

Tip: cubic could auto-approve low-risk PRs like this, if it thinks it's safe to merge. Learn more

Re-trigger cubic

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants