Skip to content

Commit ebe6eb3

Browse files
authored
Support custom monospace fonts (#3430)
1 parent 3bfe347 commit ebe6eb3

File tree

7 files changed

+189
-46
lines changed

7 files changed

+189
-46
lines changed

packages/gitbook/e2e/internal.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
CustomizationBackground,
33
CustomizationCorners,
4+
CustomizationDefaultMonospaceFont,
45
CustomizationDepth,
56
CustomizationHeaderPreset,
67
CustomizationIconsStyle,
@@ -798,6 +799,15 @@ const testCases: TestsCase[] = [
798799
}),
799800
run: waitForCookiesDialog,
800801
},
802+
{
803+
name: `With custom monospace font - Theme mode ${themeMode}`,
804+
url: `blocks/code${getCustomizationURL({
805+
styling: {
806+
monospaceFont: CustomizationDefaultMonospaceFont.SpaceMono,
807+
},
808+
})}`,
809+
run: waitForCookiesDialog,
810+
},
801811
// New site themes
802812
...allThemes.flatMap((theme) => [
803813
...allTintColors.flatMap((tint) => [

packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
import { IconStyle, IconsProvider } from '@gitbook/icons';
2323
import * as ReactDOM from 'react-dom';
2424

25-
import { getFontData } from '@/fonts';
26-
import { fontNotoColorEmoji, ibmPlexMono } from '@/fonts/default';
25+
import { type FontData, getFontData } from '@/fonts';
26+
import { fontNotoColorEmoji, fonts } from '@/fonts/default';
2727
import { getSpaceLanguage } from '@/intl/server';
2828
import { getAssetURL } from '@/lib/assets';
2929
import { tcls } from '@/lib/tailwind';
@@ -35,6 +35,22 @@ import './globals.css';
3535
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@/lib/env';
3636
import { AnnouncementDismissedScript } from '../Announcement';
3737

38+
function preloadFont(fontData: FontData) {
39+
if (fontData.type === 'custom') {
40+
ReactDOM.preconnect(GITBOOK_FONTS_URL);
41+
fontData.preloadSources
42+
.flatMap((face) => face.sources)
43+
.forEach(({ url, format }) => {
44+
ReactDOM.preload(url, {
45+
as: 'font',
46+
crossOrigin: 'anonymous',
47+
fetchPriority: 'high',
48+
type: format ? `font/${format}` : undefined,
49+
});
50+
});
51+
}
52+
}
53+
3854
/**
3955
* Layout shared between the content and the PDF renderer.
4056
* It takes care of setting the theme and the language.
@@ -51,22 +67,19 @@ export async function CustomizationRootLayout(props: {
5167
const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
5268
const sidebarStyles = getSidebarStyles(customization);
5369
const { infoColor, successColor, warningColor, dangerColor } = getSemanticColors(customization);
54-
const fontData = getFontData(customization.styling.font);
70+
const fontData = getFontData(customization.styling.font, 'content');
71+
// Temporarily add a if here while the cache is being warmed up.
72+
// We can remove the condition after 14-07-2025.
73+
const monospaceFontData = customization.styling.monospaceFont
74+
? getFontData(customization.styling.monospaceFont, 'mono')
75+
: {
76+
type: 'default' as const,
77+
variable: fonts.IBMPlexMono.variable,
78+
};
5579

5680
// Preconnect and preload custom fonts if needed
57-
if (fontData.type === 'custom') {
58-
ReactDOM.preconnect(GITBOOK_FONTS_URL);
59-
fontData.preloadSources
60-
.flatMap((face) => face.sources)
61-
.forEach(({ url, format }) => {
62-
ReactDOM.preload(url, {
63-
as: 'font',
64-
crossOrigin: 'anonymous',
65-
fetchPriority: 'high',
66-
type: format ? `font/${format}` : undefined,
67-
});
68-
});
69-
}
81+
preloadFont(fontData);
82+
preloadFont(monospaceFontData);
7083

7184
return (
7285
<html
@@ -86,8 +99,8 @@ export async function CustomizationRootLayout(props: {
8699
'links' in customization.styling && `links-${customization.styling.links}`,
87100
'depth' in customization.styling && `depth-${customization.styling.depth}`,
88101
fontNotoColorEmoji.variable,
89-
ibmPlexMono.variable,
90-
fontData.type === 'default' ? fontData.variable : 'font-custom',
102+
monospaceFontData.type === 'default' ? monospaceFontData.variable : null,
103+
fontData.type === 'default' ? fontData.variable : null,
91104

92105
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
93106
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
@@ -102,6 +115,9 @@ export async function CustomizationRootLayout(props: {
102115

103116
{/* Inject custom font @font-face rules */}
104117
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
118+
{monospaceFontData.type === 'custom' ? (
119+
<style>{monospaceFontData.fontFaceRules}</style>
120+
) : null}
105121

106122
{/* Inject a script to detect if the announcmeent banner has been dismissed */}
107123
{'announcement' in customization && customization.announcement?.enabled ? (

packages/gitbook/src/fonts/custom.test.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ async function isCSSValid(css: string): Promise<boolean> {
229229

230230
describe('generateFontFacesCSS', () => {
231231
test('basic case with regular and bold weights', async () => {
232-
const css = generateFontFacesCSS(TEST_FONTS.basic);
232+
const css = generateFontFacesCSS(TEST_FONTS.basic, 'content');
233233

234234
const isValid = await isCSSValid(css);
235235
expect(isValid).toBe(true);
@@ -240,11 +240,26 @@ describe('generateFontFacesCSS', () => {
240240
"url(https://example.com/fonts/opensans-regular.woff2) format('woff2')"
241241
);
242242
expect(css).toContain("url(https://example.com/fonts/opensans-bold.woff2) format('woff2')");
243-
expect(css).toContain('--font-custom: CustomFont');
243+
expect(css).toContain('--font-content: CustomFont_content');
244+
});
245+
246+
test('mono type', async () => {
247+
const css = generateFontFacesCSS(TEST_FONTS.basic, 'mono');
248+
249+
const isValid = await isCSSValid(css);
250+
expect(isValid).toBe(true);
251+
252+
expect(css).toContain('font-weight: 400');
253+
expect(css).toContain('font-weight: 700');
254+
expect(css).toContain(
255+
"url(https://example.com/fonts/opensans-regular.woff2) format('woff2')"
256+
);
257+
expect(css).toContain("url(https://example.com/fonts/opensans-bold.woff2) format('woff2')");
258+
expect(css).toContain('--font-mono: CustomFont_mono');
244259
});
245260

246261
test('multiple font weights', async () => {
247-
const css = generateFontFacesCSS(TEST_FONTS.multiWeight);
262+
const css = generateFontFacesCSS(TEST_FONTS.multiWeight, 'content');
248263

249264
const isValid = await isCSSValid(css);
250265
expect(isValid).toBe(true);
@@ -255,7 +270,7 @@ describe('generateFontFacesCSS', () => {
255270
});
256271

257272
test('multiple sources for a single weight', async () => {
258-
const css = generateFontFacesCSS(TEST_FONTS.multiSource);
273+
const css = generateFontFacesCSS(TEST_FONTS.multiSource, 'content');
259274

260275
const isValid = await isCSSValid(css);
261276
expect(isValid).toBe(true);
@@ -266,7 +281,7 @@ describe('generateFontFacesCSS', () => {
266281
});
267282

268283
test('missing format property', async () => {
269-
const css = generateFontFacesCSS(TEST_FONTS.missingFormat);
284+
const css = generateFontFacesCSS(TEST_FONTS.missingFormat, 'content');
270285

271286
const isValid = await isCSSValid(css);
272287
expect(isValid).toBe(true);
@@ -277,13 +292,13 @@ describe('generateFontFacesCSS', () => {
277292
});
278293

279294
test('empty font faces array', async () => {
280-
const css = generateFontFacesCSS(TEST_FONTS.empty);
295+
const css = generateFontFacesCSS(TEST_FONTS.empty, 'content');
281296

282297
expect(css).toBe('');
283298
});
284299

285300
test('font with special characters in name', async () => {
286-
const css = generateFontFacesCSS(TEST_FONTS.specialChars);
301+
const css = generateFontFacesCSS(TEST_FONTS.specialChars, 'content');
287302

288303
// Validate CSS syntax
289304
const isValid = await isCSSValid(css);

packages/gitbook/src/fonts/custom.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import type { CustomizationFontDefinitionInput } from '@gitbook/api';
22

33
/**
4-
* Define the custom font faces and set the --font-custom to the custom font name
4+
* Define the custom font faces and set the --font-content or --font-mono variable
5+
* to the custom font name.
56
*/
6-
export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInput): string {
7+
export function generateFontFacesCSS(
8+
customFont: CustomizationFontDefinitionInput,
9+
type: 'content' | 'mono'
10+
): string {
711
const { fontFaces } = customFont;
12+
const fontFamilyName = `CustomFont_${type}`;
13+
const fontVariableName = `--font-${type}`;
14+
const fallbackFont = type === 'content' ? 'sans-serif' : 'monospace';
815

916
// Generate font face declarations for all weights
1017
const fontFaceDeclarations = fontFaces
@@ -24,7 +31,7 @@ export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInpu
2431
// We could use the customFont.fontFamily name here, but to avoid extra normalization we're using 'CustomFont'
2532
return `
2633
@font-face {
27-
font-family: CustomFont;
34+
font-family: ${fontFamilyName};
2835
font-style: normal;
2936
font-weight: ${face.weight};
3037
font-display: swap;
@@ -37,7 +44,7 @@ export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInpu
3744
return fontFaceDeclarations
3845
? `${fontFaceDeclarations}
3946
:root {
40-
--font-custom: CustomFont;
47+
${fontVariableName}: ${fontFamilyName}, ${fallbackFont};
4148
}`
4249
: '';
4350
}

packages/gitbook/src/fonts/default.ts

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { CustomizationDefaultFont } from '@gitbook/api';
1+
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
22
import {
3+
DM_Mono,
4+
Fira_Code,
35
Fira_Sans_Extra_Condensed,
46
IBM_Plex_Mono,
57
IBM_Plex_Serif,
8+
Inconsolata,
69
Inter,
10+
JetBrains_Mono,
711
Lato,
812
Merriweather,
913
Noto_Color_Emoji,
@@ -13,8 +17,11 @@ import {
1317
Poppins,
1418
Raleway,
1519
Roboto,
20+
Roboto_Mono,
1621
Roboto_Slab,
22+
Source_Code_Pro,
1723
Source_Sans_3,
24+
Space_Mono,
1825
Ubuntu,
1926
} from 'next/font/google';
2027
import localFont from 'next/font/local';
@@ -41,16 +48,6 @@ const inter = Inter({
4148
fallback: ['system-ui', 'arial'],
4249
});
4350

44-
export const ibmPlexMono = IBM_Plex_Mono({
45-
weight: ['400', '500', '600', '700'],
46-
variable: '--font-mono',
47-
style: 'normal',
48-
display: 'swap',
49-
preload: false,
50-
fallback: ['monospace'],
51-
adjustFontFallback: false,
52-
});
53-
5451
const firaSans = Fira_Sans_Extra_Condensed({
5552
weight: ['400', '500', '600', '700'],
5653
variable: '--font-content',
@@ -185,10 +182,94 @@ const abcFavorit = localFont({
185182
declarations: [{ prop: 'ascent-override', value: '100%' }],
186183
});
187184

185+
const ibmPlexMono = IBM_Plex_Mono({
186+
weight: ['400', '500', '600', '700'],
187+
variable: '--font-mono',
188+
style: 'normal',
189+
display: 'swap',
190+
preload: false,
191+
fallback: ['monospace'],
192+
adjustFontFallback: false,
193+
});
194+
195+
const dmMono = DM_Mono({
196+
weight: ['400', '500'],
197+
variable: '--font-mono',
198+
style: 'normal',
199+
display: 'swap',
200+
preload: false,
201+
fallback: ['monospace'],
202+
adjustFontFallback: false,
203+
});
204+
205+
const firaCode = Fira_Code({
206+
weight: ['400', '500', '600', '700'],
207+
variable: '--font-mono',
208+
style: 'normal',
209+
display: 'swap',
210+
preload: false,
211+
fallback: ['monospace'],
212+
adjustFontFallback: false,
213+
});
214+
215+
const inconsolata = Inconsolata({
216+
weight: ['400', '500', '600', '700'],
217+
variable: '--font-mono',
218+
style: 'normal',
219+
display: 'swap',
220+
preload: false,
221+
fallback: ['monospace'],
222+
adjustFontFallback: false,
223+
});
224+
225+
const jetBrainsMono = JetBrains_Mono({
226+
weight: ['400', '500', '600', '700'],
227+
variable: '--font-mono',
228+
style: 'normal',
229+
display: 'swap',
230+
preload: false,
231+
fallback: ['monospace'],
232+
adjustFontFallback: false,
233+
});
234+
235+
const robotoMono = Roboto_Mono({
236+
weight: ['400', '500', '600', '700'],
237+
variable: '--font-mono',
238+
style: 'normal',
239+
display: 'swap',
240+
preload: false,
241+
fallback: ['monospace'],
242+
adjustFontFallback: false,
243+
});
244+
245+
const sourceCodePro = Source_Code_Pro({
246+
weight: ['400', '500', '600', '700'],
247+
variable: '--font-mono',
248+
style: 'normal',
249+
display: 'swap',
250+
preload: false,
251+
fallback: ['monospace'],
252+
adjustFontFallback: false,
253+
});
254+
255+
const spaceMono = Space_Mono({
256+
weight: ['400', '700'],
257+
variable: '--font-mono',
258+
style: 'normal',
259+
display: 'swap',
260+
preload: false,
261+
fallback: ['monospace'],
262+
adjustFontFallback: false,
263+
});
264+
188265
/**
189266
* Font definitions.
190267
*/
191-
export const fonts: { [fontName in CustomizationDefaultFont]: { variable: string } } = {
268+
export const fonts: {
269+
[fontName in CustomizationDefaultFont | CustomizationDefaultMonospaceFont]: {
270+
variable: string;
271+
};
272+
} = {
192273
[CustomizationDefaultFont.Inter]: inter,
193274
[CustomizationDefaultFont.FiraSans]: firaSans,
194275
[CustomizationDefaultFont.IBMPlexSerif]: ibmPlexSerif,
@@ -204,4 +285,12 @@ export const fonts: { [fontName in CustomizationDefaultFont]: { variable: string
204285
[CustomizationDefaultFont.SourceSansPro]: sourceSansPro,
205286
[CustomizationDefaultFont.Ubuntu]: ubuntu,
206287
[CustomizationDefaultFont.ABCFavorit]: abcFavorit,
288+
[CustomizationDefaultMonospaceFont.IBMPlexMono]: ibmPlexMono,
289+
[CustomizationDefaultMonospaceFont.DMMono]: dmMono,
290+
[CustomizationDefaultMonospaceFont.FiraCode]: firaCode,
291+
[CustomizationDefaultMonospaceFont.Inconsolata]: inconsolata,
292+
[CustomizationDefaultMonospaceFont.JetBrainsMono]: jetBrainsMono,
293+
[CustomizationDefaultMonospaceFont.RobotoMono]: robotoMono,
294+
[CustomizationDefaultMonospaceFont.SourceCodePro]: sourceCodePro,
295+
[CustomizationDefaultMonospaceFont.SpaceMono]: spaceMono,
207296
};

0 commit comments

Comments
 (0)