From ff43a4eeea952f2a2f4747b28d582462b1915c7c Mon Sep 17 00:00:00 2001 From: 0xez <110299556+0xez@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:03:16 +0800 Subject: [PATCH 1/2] fix(renderer): preserve paragraph text color over fontRef --- CHANGELOG.md | 2 + src/renderer/TextRenderer.ts | 13 ++-- .../renderer/TextRenderer.branches.test.ts | 76 +++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2389d..1755128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ### Fixed +- Preserved explicit paragraph `defRPr` text colors when a shape also provides a theme + `fontRef` color, preventing light theme text from overriding authored paragraph colors. - Sized tables from their column/row grid (Σ column widths × Σ row heights) instead of the graphicFrame ``, so tables authored in Google Slides — which export a stale placeholder ext — no longer render squished with clipped cell text. diff --git a/src/renderer/TextRenderer.ts b/src/renderer/TextRenderer.ts index 308533f..e6b9be1 100644 --- a/src/renderer/TextRenderer.ts +++ b/src/renderer/TextRenderer.ts @@ -802,7 +802,7 @@ interface RenderTextBodyOptions { cellTextItalic?: boolean; /** When set, applies font family from table style tcTxStyle (overrides inherited, yields to explicit run rPr). */ cellTextFontFamily?: string | string[]; - /** fontRef color from shape style (e.g. SmartArt). Overrides inherited styles but yields to explicit run rPr color. */ + /** fontRef color from shape style (e.g. SmartArt). Overrides inherited styles but yields to explicit paragraph/run colors. */ fontRefColor?: string; /** True when the text container uses vertical writing mode. */ isVerticalText?: boolean; @@ -1279,15 +1279,18 @@ export function renderTextBody( element.style.backgroundColor = runStyle.highlightColor; } - // Color priority: explicit run rPr > hlink theme color > cellTextColor (table style tcTxStyle) > fontRef (shape style) > inherited styles > black default + // Color priority: explicit paragraph/run color > hlink theme color > cellTextColor (table style tcTxStyle) > fontRef (shape style) > inherited styles > black default // cellTextColor from table style overrides inherited cascade colors but yields to explicit run/paragraph solidFill/gradFill. - // fontRefColor overrides inherited styles but yields to explicit run solidFill/gradFill. + // fontRefColor overrides inherited styles but yields to explicit paragraph/run solidFill/gradFill. const runColorKind = getRunColorKind(run.properties); const hasExplicitRunColor = runColorKind !== 'none'; + const paragraphColorKind = getRunColorKind(paragraph.properties?.child('defRPr')); + const hasExplicitParagraphColor = paragraphColorKind !== 'none'; + const hasExplicitTextColor = hasExplicitRunColor || hasExplicitParagraphColor; let effectiveColor: string | undefined; if (options?.fontRefColor) { - effectiveColor = hasExplicitRunColor ? runStyle.color : options.fontRefColor; - } else if (options?.cellTextColor && !hasExplicitRunColor) { + effectiveColor = hasExplicitTextColor ? runStyle.color : options.fontRefColor; + } else if (options?.cellTextColor && !hasExplicitTextColor) { effectiveColor = options.cellTextColor; } else { effectiveColor = runStyle.color; diff --git a/test/unit/renderer/TextRenderer.branches.test.ts b/test/unit/renderer/TextRenderer.branches.test.ts index bf1b04d..05ad262 100644 --- a/test/unit/renderer/TextRenderer.branches.test.ts +++ b/test/unit/renderer/TextRenderer.branches.test.ts @@ -1546,6 +1546,82 @@ describe('TextRenderer — branch coverage (uncovered paths)', () => { expect(span!.style.color).toBe('rgb(255, 0, 0)'); }); + it('prefers explicit paragraph defRPr color over fontRefColor', () => { + const body = makeTextBody({ + paragraphs: [ + { + properties: xmlNode( + '', + ), + runs: [{ text: 'Paragraph color wins' }], + level: 0, + }, + ], + }); + const ctx = createMockRenderContext(); + const container = document.createElement('div'); + renderTextBody(body, undefined, ctx, container, { + fontRefColor: '#FFFFFF', + }); + const span = container.querySelector('span'); + expect(span!.style.color).toBe('rgb(36, 59, 83)'); + }); + + it('uses fontRefColor when paragraph defRPr has no color', () => { + const body = makeTextBody({ + paragraphs: [ + { + properties: xmlNode(''), + runs: [{ text: 'Shape style color wins' }], + level: 0, + }, + ], + }); + const ctx = createMockRenderContext(); + const container = document.createElement('div'); + renderTextBody(body, undefined, ctx, container, { + fontRefColor: '#FFFFFF', + }); + const span = container.querySelector('span'); + expect(span!.style.color).toBe('rgb(255, 255, 255)'); + }); + + it('keeps fontRefColor precedence over inherited list style colors', () => { + const body = makeTextBody({ + listStyle: + '', + paragraphs: [{ runs: [{ text: 'Shape style color wins' }], level: 0 }], + }); + const ctx = createMockRenderContext(); + const container = document.createElement('div'); + renderTextBody(body, undefined, ctx, container, { + fontRefColor: '#FFFFFF', + }); + const span = container.querySelector('span'); + expect(span!.style.color).toBe('rgb(255, 255, 255)'); + }); + + it('prefers explicit paragraph defRPr color over cellTextColor', () => { + const body = makeTextBody({ + paragraphs: [ + { + properties: xmlNode( + '', + ), + runs: [{ text: 'Paragraph color wins' }], + level: 0, + }, + ], + }); + const ctx = createMockRenderContext(); + const container = document.createElement('div'); + renderTextBody(body, undefined, ctx, container, { + cellTextColor: '#FF0000', + }); + const span = container.querySelector('span'); + expect(span!.style.color).toBe('rgb(36, 59, 83)'); + }); + it('uses cellTextColor as fallback when no other color set', () => { const body = makeTextBody({ paragraphs: [ From 8e9ec9d97688b9d9871f5324fdf52c1c6bdd15aa Mon Sep 17 00:00:00 2001 From: 0xez <110299556+0xez@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:04:26 +0800 Subject: [PATCH 2/2] test(renderer): cover paragraph color through shape rendering --- src/renderer/TextRenderer.ts | 3 +- test/unit/renderer/ShapeRenderer.test.ts | 44 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/renderer/TextRenderer.ts b/src/renderer/TextRenderer.ts index e6b9be1..f524e42 100644 --- a/src/renderer/TextRenderer.ts +++ b/src/renderer/TextRenderer.ts @@ -1279,7 +1279,8 @@ export function renderTextBody( element.style.backgroundColor = runStyle.highlightColor; } - // Color priority: explicit paragraph/run color > hlink theme color > cellTextColor (table style tcTxStyle) > fontRef (shape style) > inherited styles > black default + // Base color priority before hyperlink post-processing: explicit paragraph/run color > cellTextColor (table style tcTxStyle) > fontRef (shape style) > inherited styles > black default. + // Hyperlink theme color may override this base color below. // cellTextColor from table style overrides inherited cascade colors but yields to explicit run/paragraph solidFill/gradFill. // fontRefColor overrides inherited styles but yields to explicit paragraph/run solidFill/gradFill. const runColorKind = getRunColorKind(run.properties); diff --git a/test/unit/renderer/ShapeRenderer.test.ts b/test/unit/renderer/ShapeRenderer.test.ts index 0a6d994..537cc69 100644 --- a/test/unit/renderer/ShapeRenderer.test.ts +++ b/test/unit/renderer/ShapeRenderer.test.ts @@ -5843,6 +5843,50 @@ describe('ShapeRenderer', () => { expect(hasWhiteText).toBe(true); }); + it('keeps paragraph defRPr color over shape fontRef color', () => { + const xml = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + Paragraph Color + + + + `; + + const el = renderShape(parseShapeNode(parseXml(xml)), createMockRenderContext()); + const textSpan = Array.from(el.querySelectorAll('span')).find( + (span) => span.textContent === 'Paragraph Color', + ); + + expect(textSpan).toBeDefined(); + expect(textSpan!.style.color).toBe('rgb(36, 59, 83)'); + }); + // --------------------------------------------------------------------------- // Dynamic normAutofit scaling (lines ~1142-1162 in ShapeRenderer.ts) // In jsdom, scrollHeight often equals clientHeight, so the scale branch may not