Skip to content

Commit 781b8cd

Browse files
artem-harbourArtem Nistuley
andauthored
fix: serif fallback for serif-like fonts (#2870)
Co-authored-by: Artem Nistuley <artem@superdoc.dev>
1 parent 43bb2e6 commit 781b8cd

5 files changed

Lines changed: 51 additions & 9 deletions

File tree

packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,7 @@ describe('renderTableCell', () => {
14541454
const lineEl = paraWrapper.firstElementChild as HTMLElement;
14551455
const markerEl = lineEl.querySelector('.superdoc-paragraph-marker') as HTMLElement;
14561456

1457-
expect(markerEl.style.fontFamily).toBe('"Times New Roman", sans-serif');
1457+
expect(markerEl.style.fontFamily).toBe('"Times New Roman", serif');
14581458
expect(markerEl.style.fontSize).toBe('18px');
14591459
expect(markerEl.style.fontWeight).toBe('bold');
14601460
expect(markerEl.style.fontStyle).toBe('italic');

packages/layout-engine/pm-adapter/src/converters/paragraph.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ import {
119119
const DEFAULT_HYPERLINK_CONFIG: HyperlinkConfig = { enableRichHyperlinks: false };
120120
const DEFAULT_TEST_FONT_FAMILY = 'Arial, sans-serif';
121121
const DEFAULT_TEST_FONT_SIZE_PX = (16 * 96) / 72;
122-
const FALLBACK_FONT_FAMILY = 'Times New Roman, sans-serif';
122+
const FALLBACK_FONT_FAMILY = 'Times New Roman, serif';
123123
const FALLBACK_FONT_SIZE_PX = 12;
124124
let defaultConverterContext: ConverterContext = {
125125
translatedNumbering: {},

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('toFlowBlocks', () => {
7070
runs: [
7171
{
7272
text: 'Hello world',
73-
fontFamily: 'Times New Roman, sans-serif',
73+
fontFamily: 'Times New Roman, serif',
7474
},
7575
],
7676
});
@@ -114,7 +114,7 @@ describe('toFlowBlocks', () => {
114114
});
115115

116116
expect(blocks[0].runs[0]).toMatchObject({
117-
fontFamily: 'Times New Roman, sans-serif',
117+
fontFamily: 'Times New Roman, serif',
118118
});
119119
expect(blocks[0].runs[0]?.fontSize).toBeCloseTo(14, 5);
120120
});

shared/font-utils/index.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,40 @@ export const FONT_FAMILY_FALLBACKS = Object.freeze({
4444
*/
4545
export const DEFAULT_GENERIC_FALLBACK = 'sans-serif';
4646

47+
/**
48+
* Known serif-like font families used as a heuristic when OOXML `w:family`
49+
* is unavailable. This keeps fallbacks closer to Word metrics for fonts like Cambria.
50+
*/
51+
const SERIF_LIKE_FONTS = new Set([
52+
'cambria',
53+
'cambria math',
54+
'times',
55+
'times new roman',
56+
'georgia',
57+
'garamond',
58+
'palatino',
59+
'palatino linotype',
60+
'book antiqua',
61+
'baskerville',
62+
'cochin',
63+
'hoefler text',
64+
'minion pro',
65+
'didot',
66+
'bodoni mt',
67+
'constantia',
68+
]);
69+
70+
const normalizeFontNameForLookup = (fontName) => {
71+
if (!fontName || typeof fontName !== 'string') return '';
72+
return fontName
73+
.trim()
74+
.replace(/^["']|["']$/g, '')
75+
.toLowerCase();
76+
};
77+
78+
const inferGenericFallbackFromFontName = (fontName) =>
79+
SERIF_LIKE_FONTS.has(normalizeFontNameForLookup(fontName)) ? 'serif' : DEFAULT_GENERIC_FALLBACK;
80+
4781
/**
4882
* Normalizes a comma-separated font-family string into an array of trimmed, non-empty parts.
4983
*
@@ -189,7 +223,7 @@ export function mapWordFamilyFallback(wordFamily) {
189223
* @example
190224
* // Basic usage with default fallback
191225
* toCssFontFamily('Arial'); // "Arial, sans-serif"
192-
* toCssFontFamily('Times New Roman'); // "Times New Roman, sans-serif"
226+
* toCssFontFamily('Times New Roman'); // "Times New Roman, serif"
193227
*
194228
* @example
195229
* // Custom explicit fallback
@@ -244,7 +278,9 @@ export function toCssFontFamily(fontName, options = {}) {
244278

245279
const { fallback, wordFamily } = options;
246280
const fallbackValue =
247-
fallback ?? (wordFamily ? mapWordFamilyFallback(wordFamily) : undefined) ?? DEFAULT_GENERIC_FALLBACK;
281+
fallback ??
282+
(wordFamily ? mapWordFamilyFallback(wordFamily) : undefined) ??
283+
inferGenericFallbackFromFontName(trimmed);
248284

249285
const fallbackParts = normalizeParts(fallbackValue);
250286
if (fallbackParts.length === 0) {

shared/font-utils/index.test.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,14 @@ describe('toCssFontFamily', () => {
178178
expect(toCssFontFamily('Arial')).toBe('Arial, sans-serif');
179179
});
180180

181-
it('should append default fallback to font with spaces', () => {
182-
expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, sans-serif');
181+
it('should use serif fallback for known serif-like fonts', () => {
182+
expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, serif');
183+
expect(toCssFontFamily('Cambria')).toBe('Cambria, serif');
184+
expect(toCssFontFamily('Times')).toBe('Times, serif');
185+
expect(toCssFontFamily('Cambria Math')).toBe('Cambria Math, serif');
186+
expect(toCssFontFamily('Cochin')).toBe('Cochin, serif');
187+
expect(toCssFontFamily('Hoefler Text')).toBe('Hoefler Text, serif');
188+
expect(toCssFontFamily('Minion Pro')).toBe('Minion Pro, serif');
183189
});
184190

185191
it('should append default fallback when options is empty object', () => {
@@ -201,7 +207,7 @@ describe('toCssFontFamily', () => {
201207
});
202208

203209
it('should preserve internal whitespace in font names', () => {
204-
expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, sans-serif');
210+
expect(toCssFontFamily('Times New Roman')).toBe('Times New Roman, serif');
205211
});
206212
});
207213

0 commit comments

Comments
 (0)