diff --git a/coverage/emitted-schema-known-failures.json b/coverage/emitted-schema-known-failures.json index 7abd554..fe51488 100644 --- a/coverage/emitted-schema-known-failures.json +++ b/coverage/emitted-schema-known-failures.json @@ -1,8 +1 @@ -[ - { - "id": "synthetic-table-missing-tblgrid", - "issue": "#453", - "match": "}tr': This element is not expected.", - "reason": "synthetic table fixtures omit the required w:tblGrid, and the engine faithfully preserves the gap into emitted output" - } -] +[] diff --git a/packages/docx-core/src/integration/canonical-emission-regression.test.ts b/packages/docx-core/src/integration/canonical-emission-regression.test.ts index 5920af9..3ffffde 100644 --- a/packages/docx-core/src/integration/canonical-emission-regression.test.ts +++ b/packages/docx-core/src/integration/canonical-emission-regression.test.ts @@ -215,7 +215,7 @@ describe('Canonical emission catalog', () => { await given('a table row with a prior height definition', async () => { const { doc } = await loadIndexedDoc( await makeMinimalDocx( - 'Cell', + 'Cell', ), ); @@ -240,7 +240,7 @@ describe('Canonical emission catalog', () => { await given('a table cell with prior padding properties', async () => { const { doc } = await loadIndexedDoc( await makeMinimalDocx( - 'Cell', + 'Cell', ), ); diff --git a/packages/docx-core/src/primitives/layout.test.ts b/packages/docx-core/src/primitives/layout.test.ts index 3ce1146..cea8034 100644 --- a/packages/docx-core/src/primitives/layout.test.ts +++ b/packages/docx-core/src/primitives/layout.test.ts @@ -143,56 +143,61 @@ describe('layout tracked-change emission', () => { }); }); - test('setTableCellPadding emits tcPrChange with the prior cell properties snapshot', async ({ given, when, then }: AllureBddContext) => { - let doc: Document; - let tcMar: Element; - let left: Element; - let tcPrChange: Element; - let previousTcMar: Element; - - await given('a table cell that already has top padding', () => { - doc = makeDocument( - `Cell`, - ); - }); - - await when('tracked left padding is added', () => { - const result = setTableCellPadding( - doc, - { tableIndexes: [0], leftDxa: 240 }, - createRevisionContext({ - author: 'SafeDocX AI', - date: '2026-05-03T14:15:16Z', - idState: createRevisionIdState(), - }), - ); - expect(result).toEqual({ - affectedCells: 1, - missingTableIndexes: [], - missingRowIndexes: [], - missingCellIndexes: [], - }); - - const table = doc.getElementsByTagNameNS(W_NS, W.tbl).item(0) as Element; - const row = firstDirectChild(table, W.tr); - const cell = firstDirectChild(row, W.tc); - const tcPr = firstDirectChild(cell, W.tcPr); - tcMar = firstDirectChild(tcPr, W.tcMar); - left = firstDirectChild(tcMar, W.left); - tcPrChange = firstDirectChild(tcPr, 'tcPrChange'); - previousTcMar = firstDirectChild(firstDirectChild(tcPrChange, W.tcPr), W.tcMar); - }); - - await then('the outer cell properties are updated while the inner tcPr snapshot preserves the old padding', () => { - expect(wordAttr(tcPrChange, 'author')).toBe('SafeDocX AI'); - expect(wordAttr(tcPrChange, 'date')).toBe('2026-05-03T14:15:16Z'); - expect(revisionId(tcPrChange)).toBe(1); - expect(wordAttr(left, 'w')).toBe('240'); - expect(wordAttr(left, 'type')).toBe('dxa'); - expect(getDirectChildrenByName(previousTcMar, W.left)).toHaveLength(0); - expect(firstDirectChild(previousTcMar, W.top)).toBeDefined(); - }); - }); + test + .conformance({ spec: 'ECMA-376', edition: 5, part: 1, section: '17.4.68' })( + 'setTableCellPadding emits tcPrChange with the prior cell properties snapshot', + async ({ given, when, then }: AllureBddContext) => { + let doc: Document; + let tcMar: Element; + let left: Element; + let tcPrChange: Element; + let previousTcMar: Element; + + await given('a table cell that already has top padding', () => { + doc = makeDocument( + `Cell`, + ); + }); + + await when('tracked left padding is added', () => { + const result = setTableCellPadding( + doc, + { tableIndexes: [0], leftDxa: 240 }, + createRevisionContext({ + author: 'SafeDocX AI', + date: '2026-05-03T14:15:16Z', + idState: createRevisionIdState(), + }), + ); + expect(result).toEqual({ + affectedCells: 1, + missingTableIndexes: [], + missingRowIndexes: [], + missingCellIndexes: [], + }); + + const table = doc.getElementsByTagNameNS(W_NS, W.tbl).item(0) as Element; + const row = firstDirectChild(table, W.tr); + const cell = firstDirectChild(row, W.tc); + const tcPr = firstDirectChild(cell, W.tcPr); + tcMar = firstDirectChild(tcPr, W.tcMar); + left = firstDirectChild(tcMar, W.left); + tcPrChange = firstDirectChild(tcPr, 'tcPrChange'); + previousTcMar = firstDirectChild(firstDirectChild(tcPrChange, W.tcPr), W.tcMar); + }); + + await then('the outer cell properties are updated while the inner tcPr snapshot preserves the old padding', () => { + expect(wordAttr(tcPrChange, 'author')).toBe('SafeDocX AI'); + expect(wordAttr(tcPrChange, 'date')).toBe('2026-05-03T14:15:16Z'); + expect(revisionId(tcPrChange)).toBe(1); + expect(Array.from(tcMar.children).map((child) => child.localName)).toEqual([W.top, W.left]); + expect(wordAttr(left, 'w')).toBe('240'); + expect(wordAttr(left, 'type')).toBe('dxa'); + expect(getDirectChildrenByName(previousTcMar, W.left)).toHaveLength(0); + expect(firstDirectChild(previousTcMar, W.top)).toBeDefined(); + }); + }, + ); test('layout primitives preserve legacy mutation behavior when revision context is omitted', async ({ given, when, then }: AllureBddContext) => { let doc: Document; diff --git a/packages/docx-core/src/primitives/layout.ts b/packages/docx-core/src/primitives/layout.ts index 63bb952..e5d7e8d 100644 --- a/packages/docx-core/src/primitives/layout.ts +++ b/packages/docx-core/src/primitives/layout.ts @@ -92,6 +92,19 @@ function ensureChild(parent: Element, localName: string): Element { return created; } +/** + * Keep per-cell margins in the CT_TcMar child sequence required by the schema. + * + * @conformance ECMA-376 edition 5, Part 1 § 17.4.68 + */ +function reorderCellMarginEdges(tcMar: Element): void { + const orderedNames = [W.top, W.left, W.bottom, W.right]; + const ordered = orderedNames.flatMap((name) => getDirectChildrenByName(tcMar, name)); + for (const child of ordered) { + tcMar.appendChild(child); + } +} + function setWAttr(el: Element, localName: string, value: string): void { el.setAttributeNS(OOXML.W_NS, `w:${localName}`, value); } @@ -274,6 +287,7 @@ export function setTableCellPadding( setWAttr(right, W.w, String(mutation.rightDxa)); setWAttr(right, W.type, 'dxa'); } + reorderCellMarginEdges(tcMar); if (ctx) { // CT_TcPr permits at most one child. for (const stale of getDirectChildrenByName(tcPr, 'tcPrChange')) { diff --git a/packages/docx-core/src/primitives/minimal_save.test.ts b/packages/docx-core/src/primitives/minimal_save.test.ts index c3af726..a0d6c12 100644 --- a/packages/docx-core/src/primitives/minimal_save.test.ts +++ b/packages/docx-core/src/primitives/minimal_save.test.ts @@ -193,7 +193,7 @@ describe('minimal re-serialization on save (issue #408)', () => { `cell` + ` text`; const TBL = (cellP: string) => - `${cellP}`; + `${cellP}`; let untouched: { originalXml: string; savedXml: string }; let modified: { originalXml: string; savedXml: string }; @@ -240,10 +240,10 @@ describe('minimal re-serialization on save (issue #408)', () => { // proofErr/rsid-bearing paragraph in the table must keep its original // XML — issue #408's repro document is almost entirely tables. const cellP = (id: string, marker: string) => - `${marker}` + - ` tail`; + `${marker}` + + ` tail`; const tableBody = - `` + + `` + `${cellP('00000A01', 'AA')}${cellP('00000A02', 'AB')}${cellP('00000B01', 'BA')}` + `${cellP('00000C01', 'CA')}${cellP('00000D01', 'DA')}` + ``; diff --git a/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts b/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts index b6517af..b8da33c 100644 --- a/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts +++ b/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts @@ -268,6 +268,8 @@ describe('Tool integration through SessionManager: canonical revision emission', xml: makeDocXml( 'Spacing paragraph.' + '' + + '' + + '' + '' + 'A1' + 'B1' + diff --git a/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts b/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts index fab92e2..431a2fa 100644 --- a/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts +++ b/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts @@ -161,7 +161,7 @@ describe('Traceability: Accept Tracked Changes', () => { `` + `bold text` + `` + - `` + + `` + `` + `cell`; const filePath = await writeTestDocx(dir, 'prchange.docx', bodyXml); diff --git a/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts b/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts index c69989c..16d2e91 100644 --- a/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts +++ b/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts @@ -77,6 +77,8 @@ describe('Traceability: Layout Format Controls', () => { `` + `Table heading` + `` + + `` + + `` + `` + `A1` + `B1` + diff --git a/packages/docx-mcp/src/tools/extract_revisions.test.ts b/packages/docx-mcp/src/tools/extract_revisions.test.ts index 7150e12..a4be2b4 100644 --- a/packages/docx-mcp/src/tools/extract_revisions.test.ts +++ b/packages/docx-mcp/src/tools/extract_revisions.test.ts @@ -799,7 +799,7 @@ describe('extract_revisions tool', () => { `` + `` + `` + - `` + + `` + `Cell ` + `edited` + `` + diff --git a/packages/docx-mcp/src/tools/format_layout.validation.test.ts b/packages/docx-mcp/src/tools/format_layout.validation.test.ts index a524616..278d780 100644 --- a/packages/docx-mcp/src/tools/format_layout.validation.test.ts +++ b/packages/docx-mcp/src/tools/format_layout.validation.test.ts @@ -50,6 +50,8 @@ describe('format_layout validation + strictness', () => { `` + `Only paragraph` + `` + + `` + + `` + `A1` + `` + `` + @@ -109,6 +111,8 @@ describe('format_layout validation + strictness', () => { `` + `P` + `` + + `` + + `` + `A1` + `` + `` + diff --git a/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts b/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts index e516e76..6edd956 100644 --- a/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts +++ b/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts @@ -31,6 +31,8 @@ describe('format_layout: non-body part preservation', () => { `` + `Cover Terms` + `` + + `` + + `` + `` + `A1` + `B1` + diff --git a/packages/docx-mcp/src/tools/strict_transactionality.test.ts b/packages/docx-mcp/src/tools/strict_transactionality.test.ts index 304144a..297cc07 100644 --- a/packages/docx-mcp/src/tools/strict_transactionality.test.ts +++ b/packages/docx-mcp/src/tools/strict_transactionality.test.ts @@ -40,6 +40,8 @@ describe('format_layout: strict failure transactionality', () => { `` + `Alpha` + `` + + `` + + `` + `A1` + `A2` + `` +