Skip to content

Commit 185d0ae

Browse files
authored
refactor(svg): move FONT_MAP out of generator.ts into a dedicated lib/… (JhaSourav07#2237)
## Description Fixes JhaSourav07#292 ## Changes Made - Moved FONT_MAP and font-resolution logic into lib/svg/fonts.ts - re-exported it from generator.ts. - Added unit tests for resolveFont covering predefined, dynamic, and invalid inputs. ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support. closes(JhaSourav07#292)
2 parents 866a328 + 38b1f2d commit 185d0ae

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') {
@@ -632,15 +635,10 @@ export function generateSVG(
632635
const borderAttr = params.border ? `stroke="#${params.border}" stroke-width="2"` : '';
633636

634637
const sanitizedFont = sanitizeFont(params.font);
635-
const predefinedFont = sanitizedFont
636-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
637-
: null;
638-
const isPredefinedFont = Boolean(predefinedFont);
639-
const selectedFont = isPredefinedFont
640-
? predefinedFont
641-
: sanitizedFont
642-
? `"${sanitizedFont}", sans-serif`
643-
: null;
638+
const selectedFont = resolveFont(sanitizedFont);
639+
const isPredefinedFont = sanitizedFont
640+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
641+
: false;
644642
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
645643
const googleFontUrlPart =
646644
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -704,10 +702,7 @@ function generateAutoThemeSVG(
704702
const darkLabelOpacity = getLuminance(dark.bg) > 0.5 ? '0.8' : '0.7';
705703
const safeUser = escapeXML(params.user || 'GitHub User');
706704
const sanitizedFont = sanitizeFont(params.font);
707-
const selectedFont = sanitizedFont
708-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null) ||
709-
`"${sanitizedFont}", sans-serif`
710-
: null;
705+
const selectedFont = resolveFont(sanitizedFont);
711706
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
712707
const googleFontUrlPart = sanitizedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
713708
const googleFontsImport = googleFontUrlPart
@@ -807,16 +802,10 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st
807802
const text = `#${sanitizeHexColor(params.text, 'ffffff')}`;
808803

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

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

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

12031183
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
12041184
const googleFontUrlPart = sanitizedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -1482,12 +1462,10 @@ export function generateHeatmapSVG(
14821462
const predefinedFont = sanitizedFont
14831463
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
14841464
: null;
1485-
const isPredefinedFont = Boolean(predefinedFont);
1486-
const selectedFont = isPredefinedFont
1487-
? predefinedFont
1488-
: sanitizedFont
1489-
? `"${sanitizedFont}", sans-serif`
1490-
: null;
1465+
const selectedFont = resolveFont(sanitizedFont);
1466+
const isPredefinedFont = sanitizedFont
1467+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
1468+
: false;
14911469
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
14921470
const googleFontUrlPart =
14931471
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -1619,10 +1597,7 @@ function generateAutoThemeHeatmapSVG(
16191597
const safeUser = escapeXML(params.user || 'GitHub User');
16201598

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

19431918
const sanitizedFont = sanitizeFont(params.font);
1944-
const predefinedFont = sanitizedFont
1945-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null)
1946-
: null;
1947-
const isPredefinedFont = Boolean(predefinedFont);
1948-
const selectedFont = isPredefinedFont
1949-
? predefinedFont
1950-
: sanitizedFont
1951-
? `"${sanitizedFont}", sans-serif`
1952-
: null;
1919+
const selectedFont = resolveFont(sanitizedFont);
1920+
const isPredefinedFont = sanitizedFont
1921+
? Boolean(FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP])
1922+
: false;
19531923
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
19541924
const googleFontUrlPart =
19551925
sanitizedFont && !isPredefinedFont ? sanitizeGoogleFontUrl(sanitizedFont) : null;
@@ -2027,10 +1997,7 @@ function generateAutoThemeVersusSVG(
20271997
const safeUser1 = escapeXML(params.user || 'User 1');
20281998
const safeUser2 = escapeXML(params.versus || 'User 2');
20291999
const sanitizedFont = sanitizeFont(params.font);
2030-
const selectedFont = sanitizedFont
2031-
? (FONT_MAP[sanitizedFont.toLowerCase() as keyof typeof FONT_MAP] ?? null) ||
2032-
`"${sanitizedFont}", sans-serif`
2033-
: null;
2000+
const selectedFont = resolveFont(sanitizedFont);
20342001
const statsFont = selectedFont || '"Space Grotesk", sans-serif';
20352002
const sf = getSizeScale(params.size);
20362003
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)