From 904edb391d0bbd8510c01927a21c1b6ca159aa7e Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Thu, 29 Jan 2026 22:21:48 -0800 Subject: [PATCH] fix: run color overwrite in applyInlineRunProperties (SD-1585) --- .../inline-converters/common.test.ts | 82 +++++++++++++++++++ .../converters/inline-converters/common.ts | 8 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/layout-engine/pm-adapter/src/converters/inline-converters/common.test.ts diff --git a/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.test.ts b/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.test.ts new file mode 100644 index 0000000000..9683832940 --- /dev/null +++ b/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from 'vitest'; +import type { TextRun } from '@superdoc/contracts'; +import type { RunProperties } from '@superdoc/style-engine/ooxml'; +import { applyInlineRunProperties } from './common.js'; + +vi.mock('../../attributes/paragraph.js', () => ({ + computeRunAttrs: vi.fn((runProps: RunProperties) => ({ + fontFamily: 'Arial', + fontSize: 12, + bold: runProps.bold, + italic: runProps.italic, + color: runProps.color?.val ? `#${runProps.color.val.toUpperCase()}` : undefined, + })), +})); + +describe('applyInlineRunProperties', () => { + const baseRun: TextRun = { + text: 'Hello', + fontFamily: 'Times New Roman', + fontSize: 16, + }; + + it('returns unchanged run when runProperties is undefined', () => { + const result = applyInlineRunProperties(baseRun, undefined); + + expect(result).toBe(baseRun); + }); + + it('merges computed attributes from runProperties onto the run', () => { + const runProperties: RunProperties = { bold: true }; + + const result = applyInlineRunProperties(baseRun, runProperties); + + expect(result.bold).toBe(true); + expect(result.fontFamily).toBe('Arial'); + expect(result.fontSize).toBe(12); + expect(result.text).toBe('Hello'); + }); + + it('preserves run.color when runProperties does not specify a color', () => { + const runWithColor: TextRun = { + ...baseRun, + color: '#FF0000', + }; + const runProperties: RunProperties = { bold: true }; + + const result = applyInlineRunProperties(runWithColor, runProperties); + + expect(result.color).toBe('#FF0000'); + }); + + it('overwrites run.color when runProperties specifies a color', () => { + const runWithColor: TextRun = { + ...baseRun, + color: '#FF0000', + }; + const runProperties: RunProperties = { + color: { val: '00FF00' }, + }; + + const result = applyInlineRunProperties(runWithColor, runProperties); + + expect(result.color).toBe('#00FF00'); + }); + + it('does not set color when both run and runProperties have no color', () => { + const runProperties: RunProperties = { bold: true }; + + const result = applyInlineRunProperties(baseRun, runProperties); + + expect(result.color).toBeUndefined(); + }); + + it('returns a new object instead of mutating the original run', () => { + const runProperties: RunProperties = { italic: true }; + + const result = applyInlineRunProperties(baseRun, runProperties); + + expect(result).not.toBe(baseRun); + expect(baseRun.italic).toBeUndefined(); + }); +}); diff --git a/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.ts b/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.ts index f602838eff..87864791b5 100644 --- a/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.ts +++ b/packages/layout-engine/pm-adapter/src/converters/inline-converters/common.ts @@ -80,5 +80,11 @@ export const applyInlineRunProperties = ( return run; } const runAttrs = computeRunAttrs(runProperties, converterContext); - return { ...run, ...runAttrs }; + const merged = { ...run, ...runAttrs }; + // Preserve existing run color when runProperties doesn't specify one. + // Object spread with undefined values overwrites the original, so we restore it. + if (runAttrs.color === undefined && run.color !== undefined) { + merged.color = run.color; + } + return merged; };