From e56997752c2e50795cb93315cf3f07621b8a90ec Mon Sep 17 00:00:00 2001 From: Artem Nistuley Date: Mon, 11 May 2026 19:45:28 +0300 Subject: [PATCH 1/2] fix: mirror explicit left/right paragraph alignment for rtl --- .../src/attributes/spacing-indent.test.ts | 6 ++-- .../src/attributes/spacing-indent.ts | 6 ++-- .../pm-adapter/src/index.test.ts | 33 ++++++++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts b/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts index 0ad57727a7..beaa68c1bf 100644 --- a/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts +++ b/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts @@ -114,9 +114,9 @@ describe('normalizeAlignment', () => { expect(normalizeAlignment('end', true)).toBe('left'); }); - it('does not flip explicit left/right/center/justify in RTL', () => { - expect(normalizeAlignment('left', true)).toBe('left'); - expect(normalizeAlignment('right', true)).toBe('right'); + it('maps explicit left/right to logical start/end in RTL', () => { + expect(normalizeAlignment('left', true)).toBe('right'); + expect(normalizeAlignment('right', true)).toBe('left'); expect(normalizeAlignment('center', true)).toBe('center'); expect(normalizeAlignment('justify', true)).toBe('justify'); }); diff --git a/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.ts b/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.ts index 426e10ad03..5d488af698 100644 --- a/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.ts +++ b/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.ts @@ -43,10 +43,12 @@ const AUTO_SPACING_LINE_DEFAULT = 240; // Default OOXML auto line spacing in twi export const normalizeAlignment = (value: unknown, isRtl = false): ParagraphAttrs['alignment'] => { switch (value) { case 'center': - case 'right': case 'justify': - case 'left': return value; + case 'left': + return isRtl ? 'right' : 'left'; + case 'right': + return isRtl ? 'left' : 'right'; case 'both': case 'distribute': case 'numTab': diff --git a/packages/layout-engine/pm-adapter/src/index.test.ts b/packages/layout-engine/pm-adapter/src/index.test.ts index 510578a7e4..a113896ff3 100644 --- a/packages/layout-engine/pm-adapter/src/index.test.ts +++ b/packages/layout-engine/pm-adapter/src/index.test.ts @@ -4655,7 +4655,7 @@ describe('toFlowBlocks', () => { }); }); - it('preserves explicit left alignment on RTL paragraphs', () => { + it('maps explicit left alignment to right on RTL paragraphs', () => { const pmDoc = { type: 'doc', content: [ @@ -4680,6 +4680,37 @@ describe('toFlowBlocks', () => { const { blocks } = toFlowBlocks(pmDoc); + expect(blocks).toHaveLength(1); + expect(blocks[0].attrs?.direction).toBe('rtl'); + expect(blocks[0].attrs).toMatchObject({ + alignment: 'right', + }); + }); + + it('maps explicit right alignment to left on RTL paragraphs', () => { + const pmDoc = { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + paragraphProperties: { + rightToLeft: true, + justification: 'right', + }, + }, + content: [ + { + type: 'text', + text: 'مرحبا بالعالم', + }, + ], + }, + ], + }; + + const { blocks } = toFlowBlocks(pmDoc); + expect(blocks).toHaveLength(1); expect(blocks[0].attrs?.direction).toBe('rtl'); expect(blocks[0].attrs).toMatchObject({ From 656ae5092b33642e9cd8b2f01371561373aafdee Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 12 May 2026 17:07:17 -0300 Subject: [PATCH 2/2] test(rtl-paragraph-alignment): cover both/distribute + Word-fixture import path --- .../src/attributes/spacing-indent.test.ts | 16 +++++++ .../pm-adapter/src/index.test.ts | 23 +++++++++ .../fixtures/rtl-paragraph-alignment.docx | Bin 0 -> 12958 bytes .../rtl-paragraph-alignment-import.spec.ts | 44 ++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 tests/behavior/tests/formatting/fixtures/rtl-paragraph-alignment.docx create mode 100644 tests/behavior/tests/formatting/rtl-paragraph-alignment-import.spec.ts diff --git a/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts b/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts index beaa68c1bf..05e919856f 100644 --- a/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts +++ b/packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts @@ -127,6 +127,22 @@ describe('normalizeAlignment', () => { expect(normalizeAlignment('highKashida')).toBe('justify'); }); + // SD-3093: both/distribute/numTab/thaiDistribute collapse to justify regardless + // of direction. They must not flip under RTL like `left`/`right` do. + it('maps both/distribute/numTab/thaiDistribute to justify in LTR', () => { + expect(normalizeAlignment('both', false)).toBe('justify'); + expect(normalizeAlignment('distribute', false)).toBe('justify'); + expect(normalizeAlignment('numTab', false)).toBe('justify'); + expect(normalizeAlignment('thaiDistribute', false)).toBe('justify'); + }); + + it('maps both/distribute/numTab/thaiDistribute to justify in RTL (no flip)', () => { + expect(normalizeAlignment('both', true)).toBe('justify'); + expect(normalizeAlignment('distribute', true)).toBe('justify'); + expect(normalizeAlignment('numTab', true)).toBe('justify'); + expect(normalizeAlignment('thaiDistribute', true)).toBe('justify'); + }); + it('returns undefined for invalid values', () => { expect(normalizeAlignment('unknown')).toBeUndefined(); expect(normalizeAlignment(123)).toBeUndefined(); diff --git a/packages/layout-engine/pm-adapter/src/index.test.ts b/packages/layout-engine/pm-adapter/src/index.test.ts index a113896ff3..af949c4f0c 100644 --- a/packages/layout-engine/pm-adapter/src/index.test.ts +++ b/packages/layout-engine/pm-adapter/src/index.test.ts @@ -4757,6 +4757,29 @@ describe('toFlowBlocks', () => { expect(blocksStart[0].attrs?.alignment).toBe('right'); expect(blocksEnd[0].attrs?.alignment).toBe('left'); }); + + // SD-3093: justify-family values must collapse to 'justify' without flipping + // in RTL. Regression guard against accidentally extending the mirror logic. + it('maps both/distribute/numTab/thaiDistribute to justify on RTL paragraphs', () => { + const makeDoc = (jc: string) => ({ + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + paragraphProperties: { rightToLeft: true, justification: jc }, + }, + content: [{ type: 'text', text: 'مرحبا' }], + }, + ], + }); + + for (const jc of ['both', 'distribute', 'numTab', 'thaiDistribute']) { + const { blocks } = toFlowBlocks(makeDoc(jc)); + expect(blocks[0].attrs?.direction).toBe('rtl'); + expect(blocks[0].attrs).toMatchObject({ alignment: 'justify' }); + } + }); }); describe('documentSection SDT metadata propagation', () => { diff --git a/tests/behavior/tests/formatting/fixtures/rtl-paragraph-alignment.docx b/tests/behavior/tests/formatting/fixtures/rtl-paragraph-alignment.docx new file mode 100644 index 0000000000000000000000000000000000000000..b3f138066f96bcd70dd33bd3bcff863bdf75fec7 GIT binary patch literal 12958 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 literal 0 HcmV?d00001 diff --git a/tests/behavior/tests/formatting/rtl-paragraph-alignment-import.spec.ts b/tests/behavior/tests/formatting/rtl-paragraph-alignment-import.spec.ts new file mode 100644 index 0000000000..84fbf5546e --- /dev/null +++ b/tests/behavior/tests/formatting/rtl-paragraph-alignment-import.spec.ts @@ -0,0 +1,44 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { test, expect } from '../../fixtures/superdoc.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DOC_PATH = path.resolve(__dirname, 'fixtures/rtl-paragraph-alignment.docx'); + +// SD-3093: When a Word doc has `w:bidi` + explicit `w:jc="left"`/"right"/"center", +// ECMA-376 §17.3.1.13 says left = leading edge, right = trailing edge. In an RTL +// paragraph that resolves to visual right / visual left / center respectively. +// This spec loads a Word-authored fixture exercising all three to guard the +// import + render path that PR #3235 fixes. +test('RTL paragraph w:jc=left renders text-align: right', async ({ superdoc }) => { + await superdoc.loadDocument(DOC_PATH); + await superdoc.waitForStable(); + + const lines = superdoc.page.locator('.superdoc-page .superdoc-fragment .superdoc-line'); + const jcLeftLine = lines.filter({ hasText: 'jc=left' }).first(); + await expect(jcLeftLine).toBeVisible(); + const textAlign = await jcLeftLine.evaluate((el) => window.getComputedStyle(el).textAlign); + expect(textAlign).toBe('right'); +}); + +test('RTL paragraph w:jc=right renders text-align: left', async ({ superdoc }) => { + await superdoc.loadDocument(DOC_PATH); + await superdoc.waitForStable(); + + const lines = superdoc.page.locator('.superdoc-page .superdoc-fragment .superdoc-line'); + const jcRightLine = lines.filter({ hasText: 'jc=right' }).first(); + await expect(jcRightLine).toBeVisible(); + const textAlign = await jcRightLine.evaluate((el) => window.getComputedStyle(el).textAlign); + expect(textAlign).toBe('left'); +}); + +test('RTL paragraph w:jc=center renders text-align: center', async ({ superdoc }) => { + await superdoc.loadDocument(DOC_PATH); + await superdoc.waitForStable(); + + const lines = superdoc.page.locator('.superdoc-page .superdoc-fragment .superdoc-line'); + const jcCenterLine = lines.filter({ hasText: 'jc=center' }).first(); + await expect(jcCenterLine).toBeVisible(); + const textAlign = await jcCenterLine.evaluate((el) => window.getComputedStyle(el).textAlign); + expect(textAlign).toBe('center'); +});