Skip to content

Commit d88ee11

Browse files
committed
efactor(svg): move FONT_MAP out of generator.ts into a dedicated lib/svg/fonts.ts module
1 parent e6b5723 commit d88ee11

5 files changed

Lines changed: 83 additions & 67 deletions

File tree

lib/svg/fonts.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, it, expect } from 'vitest';
2+
import FONT_MAP, { resolveFont } from './fonts';
3+
4+
describe('fonts.resolveFont', () => {
5+
it('returns predefined stack for known keys (case-insensitive)', () => {
6+
expect(resolveFont('jetbrains')).toBe(FONT_MAP.jetbrains);
7+
expect(resolveFont('JetBrains')).toBe(FONT_MAP.jetbrains);
8+
expect(resolveFont('FIRA')).toBe(FONT_MAP.fira);
9+
});
10+
11+
it('returns a dynamic font-family for custom font names', () => {
12+
expect(resolveFont('Inter')).toBe('"Inter", sans-serif');
13+
expect(resolveFont('Space Mono')).toBe('"Space Mono", sans-serif');
14+
});
15+
16+
it('returns null for invalid or empty inputs', () => {
17+
expect(resolveFont(undefined)).toBeNull();
18+
expect(resolveFont(null)).toBeNull();
19+
expect(resolveFont('')).toBeNull();
20+
expect(resolveFont(' ')).toBeNull();
21+
// strings made only of disallowed characters should sanitize to null
22+
expect(resolveFont(';;;')).toBeNull();
23+
});
24+
});

lib/svg/fonts.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { sanitizeFont } from './sanitizer';
2+
3+
export const FONT_MAP = {
4+
jetbrains: '"JetBrains Mono", monospace',
5+
fira: '"Fira Code", monospace',
6+
roboto: '"Roboto", sans-serif',
7+
} as const;
8+
9+
/**
10+
* Resolve a font name to a CSS font-family string.
11+
* - If `font` matches a predefined key in `FONT_MAP` (case-insensitive), returns that stack.
12+
* - If `font` is a valid custom font name, returns `"<Font>", sans-serif`.
13+
* - Otherwise returns `null`.
14+
*/
15+
export function resolveFont(font?: string | null): string | null {
16+
const sanitized = sanitizeFont(font ?? undefined);
17+
if (!sanitized) return null;
18+
19+
const key = sanitized.toLowerCase();
20+
const predefined = (FONT_MAP as Record<string, string>)[key];
21+
if (predefined) return predefined;
22+
23+
return `"${sanitized}", sans-serif`;
24+
}
25+
26+
export type FontKey = keyof typeof FONT_MAP;
27+
28+
export default FONT_MAP;

lib/svg/generator.ts

Lines changed: 28 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import {
1515
getGradientCoordinates,
1616
} from './sanitizer';
1717

18-
import { SVG_WIDTH, SVG_HEIGHT, FONT_MAP } from './generatorConstants';
18+
import { SVG_WIDTH, SVG_HEIGHT } from './generatorConstants';
19+
import { FONT_MAP, resolveFont } from './fonts';
20+
21+
export { FONT_MAP, resolveFont } from './fonts';
1922

2023
// helpers
2124
export function getSizeScale(size?: 'small' | 'medium' | 'large') {
@@ -619,15 +622,10 @@ export function generateSVG(
619622
const borderAttr = params.border ? `stroke="#${params.border}" stroke-width="2"` : '';
620623

621624
const sanitizedFont = sanitizeFont(params.font);
622-
const predefinedFont = sanitizedFont
623-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
624-
: null;
625-
const isPredefinedFont = Boolean(predefinedFont);
626-
const selectedFont = isPredefinedFont
627-
? predefinedFont
628-
: sanitizedFont
629-
? `"${sanitizedFont}", sans-serif`
630-
: null;
625+
const selectedFont = resolveFont(sanitizedFont);
626+
const isPredefinedFont = sanitizedFont
627+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
628+
: false;
631629
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
632630
const googleFontUrlPart =
633631
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -688,10 +686,7 @@ function generateAutoThemeSVG(
688686
const darkLabelOpacity = getLuminance(dark.bg) > 0.5 ? '0.8' : '0.7';
689687
const safeUser = escapeXML(params.user || 'GitHub User');
690688
const sanitizedFont = sanitizeFont(params.font);
691-
const selectedFont = sanitizedFont
692-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null) ||
693-
`"${sanitizedFont}", sans-serif`
694-
: null;
689+
const selectedFont = resolveFont(sanitizedFont);
695690
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
696691
const googleFontUrlPart = sanitizedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
697692
const googleFontsImport = googleFontUrlPart
@@ -806,16 +801,10 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st
806801
const text = `#${sanitizeHexColor(params.text, 'ffffff')}`;
807802

808803
const sanitizedFont = sanitizeFont(params.font);
809-
const predefinedFont = sanitizedFont
810-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
811-
: null;
812-
const isPredefinedFont = Boolean(predefinedFont);
813-
const selectedFont = isPredefinedFont
814-
? predefinedFont
815-
: sanitizedFont
816-
? `"${sanitizedFont}", sans-serif`
817-
: null;
818-
804+
const selectedFont = resolveFont(sanitizedFont);
805+
const isPredefinedFont = sanitizedFont
806+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
807+
: false;
819808
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
820809
const radius = sanitizeRadius(params.radius, 8);
821810
const labels = getLabels(params.lang);
@@ -935,16 +924,10 @@ export function generateWrappedSVG(
935924
const text = `#${sanitizeHexColor(params.text, 'ffffff')}`;
936925

937926
const sanitizedFont = sanitizeFont(params.font);
938-
const predefinedFont = sanitizedFont
939-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
940-
: null;
941-
const isPredefinedFont = Boolean(predefinedFont);
942-
const selectedFont = isPredefinedFont
943-
? predefinedFont
944-
: sanitizedFont
945-
? `"${sanitizedFont}", sans-serif`
946-
: null;
947-
927+
const selectedFont = resolveFont(sanitizedFont);
928+
const isPredefinedFont = sanitizedFont
929+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
930+
: false;
948931
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
949932
const radius = sanitizeRadius(params.radius, 8);
950933

@@ -1194,10 +1177,7 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams):
11941177
const dark = AUTO_THEME_DARK;
11951178
const safeUser = escapeXML(params.user || 'GitHub User');
11961179
const sanitizedFont = sanitizeFont(params.font);
1197-
const selectedFont = sanitizedFont
1198-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null) ||
1199-
`"${sanitizedFont}", sans-serif`
1200-
: null;
1180+
const selectedFont = resolveFont(sanitizedFont);
12011181

12021182
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
12031183
const googleFontUrlPart = sanitizedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -1481,12 +1461,10 @@ export function generateHeatmapSVG(
14811461
const predefinedFont = sanitizedFont
14821462
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
14831463
: null;
1484-
const isPredefinedFont = Boolean(predefinedFont);
1485-
const selectedFont = isPredefinedFont
1486-
? predefinedFont
1487-
: sanitizedFont
1488-
? `"${sanitizedFont}", sans-serif`
1489-
: null;
1464+
const selectedFont = resolveFont(sanitizedFont);
1465+
const isPredefinedFont = sanitizedFont
1466+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
1467+
: false;
14901468
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
14911469
const googleFontUrlPart =
14921470
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -1618,10 +1596,7 @@ function generateAutoThemeHeatmapSVG(
16181596
const safeUser = escapeXML(params.user || 'GitHub User');
16191597

16201598
const sanitizedFont = sanitizeFont(params.font);
1621-
const selectedFont = sanitizedFont
1622-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null) ||
1623-
`"${sanitizedFont}", sans-serif`
1624-
: null;
1599+
const selectedFont = resolveFont(sanitizedFont);
16251600
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
16261601
const googleFontUrlPart = sanitizedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
16271602
const googleFontsImport = googleFontUrlPart
@@ -1940,15 +1915,10 @@ export function generateVersusSVG(
19401915
const text = `#${sanitizeHexColor(params.text, 'ffffff')}`;
19411916

19421917
const sanitizedFont = sanitizeFont(params.font);
1943-
const predefinedFont = sanitizedFont
1944-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
1945-
: null;
1946-
const isPredefinedFont = Boolean(predefinedFont);
1947-
const selectedFont = isPredefinedFont
1948-
? predefinedFont
1949-
: sanitizedFont
1950-
? `"${sanitizedFont}", sans-serif`
1951-
: null;
1918+
const selectedFont = resolveFont(sanitizedFont);
1919+
const isPredefinedFont = sanitizedFont
1920+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
1921+
: false;
19521922
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
19531923
const googleFontUrlPart =
19541924
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -2026,10 +1996,7 @@ function generateAutoThemeVersusSVG(
20261996
const safeUser1 = escapeXML(params.user || 'User 1');
20271997
const safeUser2 = escapeXML(params.versus || 'User 2');
20281998
const sanitizedFont = sanitizeFont(params.font);
2029-
const selectedFont = sanitizedFont
2030-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null) ||
2031-
`"${sanitizedFont}", sans-serif`
2032-
: null;
1999+
const selectedFont = resolveFont(sanitizedFont);
20332000
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
20342001
const sf = getSizeScale(params.size);
20352002
const radius = sanitizeRadius(params.radius, 8) * sf;

lib/svg/generatorConstants.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from 'vitest';
2-
import { SVG_WIDTH, SVG_HEIGHT, FONT_MAP, isFontKey } from './generatorConstants';
2+
import { SVG_WIDTH, SVG_HEIGHT, isFontKey } from './generatorConstants';
3+
import { FONT_MAP } from './fonts';
34

45
describe('generatorConstants', () => {
56
it('SVG_WIDTH equals 600', () => {

lib/svg/generatorConstants.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
export const SVG_WIDTH = 600;
22
export const SVG_HEIGHT = 420;
33

4-
export const FONT_MAP = {
5-
jetbrains: '"JetBrains Mono", monospace',
6-
fira: '"Fira Code", monospace',
7-
roboto: '"Roboto", sans-serif',
8-
} as const;
4+
import { FONT_MAP } from './fonts';
95

106
export type FontKey = keyof typeof FONT_MAP;
117

0 commit comments

Comments
 (0)