Skip to content

Commit fa0cfc5

Browse files
artem-harbourArtem Nistuleycaio-pizzol
authored
fix: mirror explicit left/right paragraph alignment for rtl (#3235)
* fix: mirror explicit left/right paragraph alignment for rtl * test(rtl-paragraph-alignment): cover both/distribute + Word-fixture import path --------- Co-authored-by: Artem Nistuley <artem@superdoc.dev> Co-authored-by: Caio Pizzol <caio@superdoc.dev>
1 parent 71bf8d7 commit fa0cfc5

5 files changed

Lines changed: 122 additions & 6 deletions

File tree

packages/layout-engine/pm-adapter/src/attributes/spacing-indent.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ describe('normalizeAlignment', () => {
114114
expect(normalizeAlignment('end', true)).toBe('left');
115115
});
116116

117-
it('does not flip explicit left/right/center/justify in RTL', () => {
118-
expect(normalizeAlignment('left', true)).toBe('left');
119-
expect(normalizeAlignment('right', true)).toBe('right');
117+
it('maps explicit left/right to logical start/end in RTL', () => {
118+
expect(normalizeAlignment('left', true)).toBe('right');
119+
expect(normalizeAlignment('right', true)).toBe('left');
120120
expect(normalizeAlignment('center', true)).toBe('center');
121121
expect(normalizeAlignment('justify', true)).toBe('justify');
122122
});
@@ -127,6 +127,22 @@ describe('normalizeAlignment', () => {
127127
expect(normalizeAlignment('highKashida')).toBe('justify');
128128
});
129129

130+
// SD-3093: both/distribute/numTab/thaiDistribute collapse to justify regardless
131+
// of direction. They must not flip under RTL like `left`/`right` do.
132+
it('maps both/distribute/numTab/thaiDistribute to justify in LTR', () => {
133+
expect(normalizeAlignment('both', false)).toBe('justify');
134+
expect(normalizeAlignment('distribute', false)).toBe('justify');
135+
expect(normalizeAlignment('numTab', false)).toBe('justify');
136+
expect(normalizeAlignment('thaiDistribute', false)).toBe('justify');
137+
});
138+
139+
it('maps both/distribute/numTab/thaiDistribute to justify in RTL (no flip)', () => {
140+
expect(normalizeAlignment('both', true)).toBe('justify');
141+
expect(normalizeAlignment('distribute', true)).toBe('justify');
142+
expect(normalizeAlignment('numTab', true)).toBe('justify');
143+
expect(normalizeAlignment('thaiDistribute', true)).toBe('justify');
144+
});
145+
130146
it('returns undefined for invalid values', () => {
131147
expect(normalizeAlignment('unknown')).toBeUndefined();
132148
expect(normalizeAlignment(123)).toBeUndefined();

packages/layout-engine/pm-adapter/src/attributes/spacing-indent.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ const AUTO_SPACING_LINE_DEFAULT = 240; // Default OOXML auto line spacing in twi
4343
export const normalizeAlignment = (value: unknown, isRtl = false): ParagraphAttrs['alignment'] => {
4444
switch (value) {
4545
case 'center':
46-
case 'right':
4746
case 'justify':
48-
case 'left':
4947
return value;
48+
case 'left':
49+
return isRtl ? 'right' : 'left';
50+
case 'right':
51+
return isRtl ? 'left' : 'right';
5052
case 'both':
5153
case 'distribute':
5254
case 'numTab':

packages/layout-engine/pm-adapter/src/index.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4655,7 +4655,7 @@ describe('toFlowBlocks', () => {
46554655
});
46564656
});
46574657

4658-
it('preserves explicit left alignment on RTL paragraphs', () => {
4658+
it('maps explicit left alignment to right on RTL paragraphs', () => {
46594659
const pmDoc = {
46604660
type: 'doc',
46614661
content: [
@@ -4680,6 +4680,37 @@ describe('toFlowBlocks', () => {
46804680

46814681
const { blocks } = toFlowBlocks(pmDoc);
46824682

4683+
expect(blocks).toHaveLength(1);
4684+
expect(blocks[0].attrs?.direction).toBe('rtl');
4685+
expect(blocks[0].attrs).toMatchObject({
4686+
alignment: 'right',
4687+
});
4688+
});
4689+
4690+
it('maps explicit right alignment to left on RTL paragraphs', () => {
4691+
const pmDoc = {
4692+
type: 'doc',
4693+
content: [
4694+
{
4695+
type: 'paragraph',
4696+
attrs: {
4697+
paragraphProperties: {
4698+
rightToLeft: true,
4699+
justification: 'right',
4700+
},
4701+
},
4702+
content: [
4703+
{
4704+
type: 'text',
4705+
text: 'مرحبا بالعالم',
4706+
},
4707+
],
4708+
},
4709+
],
4710+
};
4711+
4712+
const { blocks } = toFlowBlocks(pmDoc);
4713+
46834714
expect(blocks).toHaveLength(1);
46844715
expect(blocks[0].attrs?.direction).toBe('rtl');
46854716
expect(blocks[0].attrs).toMatchObject({
@@ -4726,6 +4757,29 @@ describe('toFlowBlocks', () => {
47264757
expect(blocksStart[0].attrs?.alignment).toBe('right');
47274758
expect(blocksEnd[0].attrs?.alignment).toBe('left');
47284759
});
4760+
4761+
// SD-3093: justify-family values must collapse to 'justify' without flipping
4762+
// in RTL. Regression guard against accidentally extending the mirror logic.
4763+
it('maps both/distribute/numTab/thaiDistribute to justify on RTL paragraphs', () => {
4764+
const makeDoc = (jc: string) => ({
4765+
type: 'doc',
4766+
content: [
4767+
{
4768+
type: 'paragraph',
4769+
attrs: {
4770+
paragraphProperties: { rightToLeft: true, justification: jc },
4771+
},
4772+
content: [{ type: 'text', text: 'مرحبا' }],
4773+
},
4774+
],
4775+
});
4776+
4777+
for (const jc of ['both', 'distribute', 'numTab', 'thaiDistribute']) {
4778+
const { blocks } = toFlowBlocks(makeDoc(jc));
4779+
expect(blocks[0].attrs?.direction).toBe('rtl');
4780+
expect(blocks[0].attrs).toMatchObject({ alignment: 'justify' });
4781+
}
4782+
});
47294783
});
47304784

47314785
describe('documentSection SDT metadata propagation', () => {
Binary file not shown.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import path from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
import { test, expect } from '../../fixtures/superdoc.js';
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
const DOC_PATH = path.resolve(__dirname, 'fixtures/rtl-paragraph-alignment.docx');
7+
8+
// SD-3093: When a Word doc has `w:bidi` + explicit `w:jc="left"`/"right"/"center",
9+
// ECMA-376 §17.3.1.13 says left = leading edge, right = trailing edge. In an RTL
10+
// paragraph that resolves to visual right / visual left / center respectively.
11+
// This spec loads a Word-authored fixture exercising all three to guard the
12+
// import + render path that PR #3235 fixes.
13+
test('RTL paragraph w:jc=left renders text-align: right', async ({ superdoc }) => {
14+
await superdoc.loadDocument(DOC_PATH);
15+
await superdoc.waitForStable();
16+
17+
const lines = superdoc.page.locator('.superdoc-page .superdoc-fragment .superdoc-line');
18+
const jcLeftLine = lines.filter({ hasText: 'jc=left' }).first();
19+
await expect(jcLeftLine).toBeVisible();
20+
const textAlign = await jcLeftLine.evaluate((el) => window.getComputedStyle(el).textAlign);
21+
expect(textAlign).toBe('right');
22+
});
23+
24+
test('RTL paragraph w:jc=right renders text-align: left', async ({ superdoc }) => {
25+
await superdoc.loadDocument(DOC_PATH);
26+
await superdoc.waitForStable();
27+
28+
const lines = superdoc.page.locator('.superdoc-page .superdoc-fragment .superdoc-line');
29+
const jcRightLine = lines.filter({ hasText: 'jc=right' }).first();
30+
await expect(jcRightLine).toBeVisible();
31+
const textAlign = await jcRightLine.evaluate((el) => window.getComputedStyle(el).textAlign);
32+
expect(textAlign).toBe('left');
33+
});
34+
35+
test('RTL paragraph w:jc=center renders text-align: center', async ({ superdoc }) => {
36+
await superdoc.loadDocument(DOC_PATH);
37+
await superdoc.waitForStable();
38+
39+
const lines = superdoc.page.locator('.superdoc-page .superdoc-fragment .superdoc-line');
40+
const jcCenterLine = lines.filter({ hasText: 'jc=center' }).first();
41+
await expect(jcCenterLine).toBeVisible();
42+
const textAlign = await jcCenterLine.evaluate((el) => window.getComputedStyle(el).textAlign);
43+
expect(textAlign).toBe('center');
44+
});

0 commit comments

Comments
 (0)