diff --git a/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts b/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts index 6836c4319d..b8a44538dd 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts @@ -1454,7 +1454,7 @@ describe('renderTableCell', () => { const lineEl = paraWrapper.firstElementChild as HTMLElement; const markerEl = lineEl.querySelector('.superdoc-paragraph-marker') as HTMLElement; - expect(markerEl.style.fontFamily).toBe('"Times New Roman", sans-serif'); + expect(markerEl.style.fontFamily).toBe('"Times New Roman", serif'); expect(markerEl.style.fontSize).toBe('18px'); expect(markerEl.style.fontWeight).toBe('bold'); expect(markerEl.style.fontStyle).toBe('italic'); diff --git a/packages/layout-engine/pm-adapter/src/converters/paragraph.test.ts b/packages/layout-engine/pm-adapter/src/converters/paragraph.test.ts index c701136ccd..dfa44dbf3c 100644 --- a/packages/layout-engine/pm-adapter/src/converters/paragraph.test.ts +++ b/packages/layout-engine/pm-adapter/src/converters/paragraph.test.ts @@ -119,7 +119,7 @@ import { const DEFAULT_HYPERLINK_CONFIG: HyperlinkConfig = { enableRichHyperlinks: false }; const DEFAULT_TEST_FONT_FAMILY = 'Arial, sans-serif'; const DEFAULT_TEST_FONT_SIZE_PX = (16 * 96) / 72; -const FALLBACK_FONT_FAMILY = 'Times New Roman, sans-serif'; +const FALLBACK_FONT_FAMILY = 'Times New Roman, serif'; const FALLBACK_FONT_SIZE_PX = 12; let defaultConverterContext: ConverterContext = { translatedNumbering: {}, diff --git a/packages/layout-engine/pm-adapter/src/index.test.ts b/packages/layout-engine/pm-adapter/src/index.test.ts index 65d5456486..4f387dd52b 100644 --- a/packages/layout-engine/pm-adapter/src/index.test.ts +++ b/packages/layout-engine/pm-adapter/src/index.test.ts @@ -70,7 +70,7 @@ describe('toFlowBlocks', () => { runs: [ { text: 'Hello world', - fontFamily: 'Times New Roman, sans-serif', + fontFamily: 'Times New Roman, serif', }, ], }); @@ -114,7 +114,7 @@ describe('toFlowBlocks', () => { }); expect(blocks[0].runs[0]).toMatchObject({ - fontFamily: 'Times New Roman, sans-serif', + fontFamily: 'Times New Roman, serif', }); expect(blocks[0].runs[0]?.fontSize).toBeCloseTo(14, 5); }); diff --git a/shared/font-utils/index.js b/shared/font-utils/index.js index d2ebd3544b..36a73f10a3 100644 --- a/shared/font-utils/index.js +++ b/shared/font-utils/index.js @@ -44,6 +44,40 @@ export const FONT_FAMILY_FALLBACKS = Object.freeze({ */ export const DEFAULT_GENERIC_FALLBACK = 'sans-serif'; +/** + * Known serif-like font families used as a heuristic when OOXML `w:family` + * is unavailable. This keeps fallbacks closer to Word metrics for fonts like Cambria. + */ +const SERIF_LIKE_FONTS = new Set([ + 'cambria', + 'cambria math', + 'times', + 'times new roman', + 'georgia', + 'garamond', + 'palatino', + 'palatino linotype', + 'book antiqua', + 'baskerville', + 'cochin', + 'hoefler text', + 'minion pro', + 'didot', + 'bodoni mt', + 'constantia', +]); + +const normalizeFontNameForLookup = (fontName) => { + if (!fontName || typeof fontName !== 'string') return ''; + return fontName + .trim() + .replace(/^["']|["']$/g, '') + .toLowerCase(); +}; + +const inferGenericFallbackFromFontName = (fontName) => + SERIF_LIKE_FONTS.has(normalizeFontNameForLookup(fontName)) ? 'serif' : DEFAULT_GENERIC_FALLBACK; + /** * Normalizes a comma-separated font-family string into an array of trimmed, non-empty parts. * @@ -189,7 +223,7 @@ export function mapWordFamilyFallback(wordFamily) { * @example * // Basic usage with default fallback * toCssFontFamily('Arial'); // "Arial, sans-serif" - * toCssFontFamily('Times New Roman'); // "Times New Roman, sans-serif" + * toCssFontFamily('Times New Roman'); // "Times New Roman, serif" * * @example * // Custom explicit fallback @@ -244,7 +278,9 @@ export function toCssFontFamily(fontName, options = {}) { const { fallback, wordFamily } = options; const fallbackValue = - fallback ?? (wordFamily ? mapWordFamilyFallback(wordFamily) : undefined) ?? DEFAULT_GENERIC_FALLBACK; + fallback ?? + (wordFamily ? mapWordFamilyFallback(wordFamily) : undefined) ?? + inferGenericFallbackFromFontName(trimmed); const fallbackParts = normalizeParts(fallbackValue); if (fallbackParts.length === 0) { diff --git a/shared/font-utils/index.test.js b/shared/font-utils/index.test.js index 1810da76bd..42b70bbb87 100644 --- a/shared/font-utils/index.test.js +++ b/shared/font-utils/index.test.js @@ -178,8 +178,14 @@ describe('toCssFontFamily', () => { expect(toCssFontFamily('Arial')).toBe('Arial, sans-serif'); }); - it('should append default fallback to font with spaces', () => { - expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, sans-serif'); + it('should use serif fallback for known serif-like fonts', () => { + expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, serif'); + expect(toCssFontFamily('Cambria')).toBe('Cambria, serif'); + expect(toCssFontFamily('Times')).toBe('Times, serif'); + expect(toCssFontFamily('Cambria Math')).toBe('Cambria Math, serif'); + expect(toCssFontFamily('Cochin')).toBe('Cochin, serif'); + expect(toCssFontFamily('Hoefler Text')).toBe('Hoefler Text, serif'); + expect(toCssFontFamily('Minion Pro')).toBe('Minion Pro, serif'); }); it('should append default fallback when options is empty object', () => { @@ -201,7 +207,7 @@ describe('toCssFontFamily', () => { }); it('should preserve internal whitespace in font names', () => { - expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, sans-serif'); + expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, serif'); }); });