From 342b9583c8b2b5e55f5a45fd93b43f8439e8728f Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Fri, 5 Jun 2026 11:50:00 -0300 Subject: [PATCH] fix(painter): clamp double borders to CSS-visible width CSS `double` only renders two distinct rules when the border is at least 3px wide (1px rule + 1px gap + 1px rule). The painter emitted the authored width verbatim, so a typical w:val="double" w:sz="12" border (~2px) was collapsed by the browser into a single solid-looking line, while Word always renders two parallel rules for double borders. Clamp the rendered width up to 3px when the resolved CSS style is double, keeping authored widths that are already 3px or wider. Every border paint path funnels through applyBorder, so cell borders, outer table edges, and continuation rows are all covered by the one change. The prior unit test asserted the collapsed 2px output; the expectation flip to 3px is deliberate. Verified against the SD-3308 mutation fixture (double_or_dotted_borders.docx): the double table now shows two rules on every outer and interior edge, dotted and dashed render unchanged. --- .../painters/dom/src/table/border-utils.test.ts | 13 +++++++++++-- .../painters/dom/src/table/border-utils.ts | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/layout-engine/painters/dom/src/table/border-utils.test.ts b/packages/layout-engine/painters/dom/src/table/border-utils.test.ts index ebadd10b2d..da9cdca1a8 100644 --- a/packages/layout-engine/painters/dom/src/table/border-utils.test.ts +++ b/packages/layout-engine/painters/dom/src/table/border-utils.test.ts @@ -39,10 +39,19 @@ describe('applyBorder', () => { expect(element.style.borderTop).toMatch(/2px solid (#FF0000|rgb\(255,\s*0,\s*0\))/i); }); - it('should apply border with double style', () => { + // SD-3308: CSS `double` only renders two distinct rules at >= 3px (1px rule + 1px gap + + // 1px rule); below that it collapses to a single solid-looking line, while Word always + // shows two rules for w:val="double". The painter clamps the width up, never down. + it('clamps a double border below 3px up so both rules render', () => { const border: BorderSpec = { style: 'double', width: 2, color: '#FF0000' }; applyBorder(element, 'Top', border); - expect(element.style.borderTop).toMatch(/2px double (#FF0000|rgb\(255,\s*0,\s*0\))/i); + expect(element.style.borderTop).toMatch(/3px double (#FF0000|rgb\(255,\s*0,\s*0\))/i); + }); + + it('keeps an authored double width that is already >= 3px', () => { + const border: BorderSpec = { style: 'double', width: 4, color: '#FF0000' }; + applyBorder(element, 'Top', border); + expect(element.style.borderTop).toMatch(/4px double (#FF0000|rgb\(255,\s*0,\s*0\))/i); }); it('should apply border with dashed style', () => { diff --git a/packages/layout-engine/painters/dom/src/table/border-utils.ts b/packages/layout-engine/painters/dom/src/table/border-utils.ts index 1285d0aae0..ccf9803796 100644 --- a/packages/layout-engine/painters/dom/src/table/border-utils.ts +++ b/packages/layout-engine/painters/dom/src/table/border-utils.ts @@ -83,7 +83,11 @@ export const applyBorder = ( const width = border.width ?? 1; const color = border.color ?? '#000000'; const safeColor = isValidHexColor(color) ? color : '#000000'; - const actualWidth = border.style === 'thick' ? Math.max(width * 2, 3) : width; + // CSS `double` only renders two distinct rules at >= 3px (1px rule + 1px gap + 1px rule); + // below that it collapses to a single solid-looking line. Word always shows two rules for + // w:val="double", so clamp the rendered width up (never shrink an authored width). (SD-3308) + const minStyleWidth = style === 'double' ? 3 : 0; + const actualWidth = border.style === 'thick' ? Math.max(width * 2, 3) : Math.max(width, minStyleWidth); element.style[`border${side}`] = `${actualWidth}px ${style} ${safeColor}`; };