276 lines
7.4 KiB
TypeScript
276 lines
7.4 KiB
TypeScript
/**
|
|
* Normalize fonts from Google Fonts and Fontshare to unified model
|
|
*
|
|
* Transforms provider-specific font data into a common interface
|
|
* for consistent handling across the application.
|
|
*/
|
|
|
|
import type {
|
|
FontCategory,
|
|
FontStyleUrls,
|
|
FontSubset,
|
|
FontshareFont,
|
|
GoogleFontItem,
|
|
UnifiedFont,
|
|
UnifiedFontVariant,
|
|
} from '../../model/types';
|
|
|
|
/**
|
|
* Map Google Fonts category to unified FontCategory
|
|
*/
|
|
function mapGoogleCategory(category: string): FontCategory {
|
|
const normalized = category.toLowerCase();
|
|
if (normalized.includes('sans-serif')) {
|
|
return 'sans-serif';
|
|
}
|
|
if (normalized.includes('serif')) {
|
|
return 'serif';
|
|
}
|
|
if (normalized.includes('display')) {
|
|
return 'display';
|
|
}
|
|
if (normalized.includes('handwriting') || normalized.includes('cursive')) {
|
|
return 'handwriting';
|
|
}
|
|
if (normalized.includes('monospace')) {
|
|
return 'monospace';
|
|
}
|
|
// Default fallback
|
|
return 'sans-serif';
|
|
}
|
|
|
|
/**
|
|
* Map Fontshare category to unified FontCategory
|
|
*/
|
|
function mapFontshareCategory(category: string): FontCategory {
|
|
const normalized = category.toLowerCase();
|
|
if (normalized === 'sans' || normalized === 'sans-serif') {
|
|
return 'sans-serif';
|
|
}
|
|
if (normalized === 'serif') {
|
|
return 'serif';
|
|
}
|
|
if (normalized === 'display') {
|
|
return 'display';
|
|
}
|
|
if (normalized === 'script') {
|
|
return 'handwriting';
|
|
}
|
|
if (normalized === 'mono' || normalized === 'monospace') {
|
|
return 'monospace';
|
|
}
|
|
// Default fallback
|
|
return 'sans-serif';
|
|
}
|
|
|
|
/**
|
|
* Map Google subset to unified FontSubset
|
|
*/
|
|
function mapGoogleSubset(subset: string): FontSubset | null {
|
|
const validSubsets: FontSubset[] = [
|
|
'latin',
|
|
'latin-ext',
|
|
'cyrillic',
|
|
'greek',
|
|
'arabic',
|
|
'devanagari',
|
|
];
|
|
return validSubsets.includes(subset as FontSubset)
|
|
? (subset as FontSubset)
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Map Fontshare script to unified FontSubset
|
|
*/
|
|
function mapFontshareScript(script: string): FontSubset | null {
|
|
const normalized = script.toLowerCase();
|
|
const mapping: Record<string, FontSubset | null> = {
|
|
latin: 'latin',
|
|
'latin-ext': 'latin-ext',
|
|
cyrillic: 'cyrillic',
|
|
greek: 'greek',
|
|
arabic: 'arabic',
|
|
devanagari: 'devanagari',
|
|
};
|
|
return mapping[normalized] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Normalize Google Font to unified model
|
|
*
|
|
* @param apiFont - Font item from Google Fonts API
|
|
* @returns Unified font model
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const roboto = normalizeGoogleFont({
|
|
* family: 'Roboto',
|
|
* category: 'sans-serif',
|
|
* variants: ['regular', '700'],
|
|
* subsets: ['latin', 'latin-ext'],
|
|
* files: { regular: '...', '700': '...' }
|
|
* });
|
|
*
|
|
* console.log(roboto.id); // 'Roboto'
|
|
* console.log(roboto.provider); // 'google'
|
|
* ```
|
|
*/
|
|
export function normalizeGoogleFont(apiFont: GoogleFontItem): UnifiedFont {
|
|
const category = mapGoogleCategory(apiFont.category);
|
|
const subsets = apiFont.subsets
|
|
.map(mapGoogleSubset)
|
|
.filter((subset): subset is FontSubset => subset !== null);
|
|
|
|
// Map variant files to style URLs
|
|
const styles: FontStyleUrls = {};
|
|
for (const [variant, url] of Object.entries(apiFont.files)) {
|
|
const urlString = url as string; // Type assertion for Record<string, string>
|
|
if (variant === 'regular' || variant === '400') {
|
|
styles.regular = urlString;
|
|
} else if (variant === 'italic' || variant === '400italic') {
|
|
styles.italic = urlString;
|
|
} else if (variant === 'bold' || variant === '700') {
|
|
styles.bold = urlString;
|
|
} else if (variant === 'bolditalic' || variant === '700italic') {
|
|
styles.boldItalic = urlString;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: apiFont.family,
|
|
name: apiFont.family,
|
|
provider: 'google',
|
|
category,
|
|
subsets,
|
|
variants: apiFont.variants,
|
|
styles,
|
|
metadata: {
|
|
cachedAt: Date.now(),
|
|
version: apiFont.version,
|
|
lastModified: apiFont.lastModified,
|
|
},
|
|
features: {
|
|
isVariable: false, // Google Fonts doesn't expose variable font info
|
|
tags: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Normalize Fontshare font to unified model
|
|
*
|
|
* @param apiFont - Font item from Fontshare API
|
|
* @returns Unified font model
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const satoshi = normalizeFontshareFont({
|
|
* id: 'uuid',
|
|
* name: 'Satoshi',
|
|
* slug: 'satoshi',
|
|
* category: 'Sans',
|
|
* script: 'latin',
|
|
* styles: [ ... ]
|
|
* });
|
|
*
|
|
* console.log(satoshi.id); // 'satoshi'
|
|
* console.log(satoshi.provider); // 'fontshare'
|
|
* ```
|
|
*/
|
|
export function normalizeFontshareFont(apiFont: FontshareFont): UnifiedFont {
|
|
const category = mapFontshareCategory(apiFont.category);
|
|
const subset = mapFontshareScript(apiFont.script);
|
|
const subsets = subset ? [subset] : [];
|
|
|
|
// Extract variant names from styles
|
|
const variants = apiFont.styles.map(style => {
|
|
const weightLabel = style.weight.label;
|
|
const isItalic = style.is_italic;
|
|
return (isItalic ? `${weightLabel}italic` : weightLabel) as UnifiedFontVariant;
|
|
});
|
|
|
|
// Map styles to URLs
|
|
const styles: FontStyleUrls = {};
|
|
for (const style of apiFont.styles) {
|
|
if (style.is_variable) {
|
|
// Variable font - store as primary variant
|
|
styles.regular = style.file;
|
|
break;
|
|
}
|
|
|
|
const weight = style.weight.number;
|
|
const isItalic = style.is_italic;
|
|
|
|
if (weight === 400 && !isItalic) {
|
|
styles.regular = style.file;
|
|
} else if (weight === 400 && isItalic) {
|
|
styles.italic = style.file;
|
|
} else if (weight >= 700 && !isItalic) {
|
|
styles.bold = style.file;
|
|
} else if (weight >= 700 && isItalic) {
|
|
styles.boldItalic = style.file;
|
|
}
|
|
}
|
|
|
|
// Extract variable font axes
|
|
const axes = apiFont.axes.map(axis => ({
|
|
name: axis.name,
|
|
property: axis.property,
|
|
default: axis.range_default,
|
|
min: axis.range_left,
|
|
max: axis.range_right,
|
|
}));
|
|
|
|
// Extract tags
|
|
const tags = apiFont.font_tags.map(tag => tag.name);
|
|
|
|
return {
|
|
id: apiFont.slug,
|
|
name: apiFont.name,
|
|
provider: 'fontshare',
|
|
category,
|
|
subsets,
|
|
variants,
|
|
styles,
|
|
metadata: {
|
|
cachedAt: Date.now(),
|
|
version: apiFont.version,
|
|
lastModified: apiFont.inserted_at,
|
|
popularity: apiFont.views,
|
|
},
|
|
features: {
|
|
isVariable: apiFont.axes.length > 0,
|
|
axes: axes.length > 0 ? axes : undefined,
|
|
tags: tags.length > 0 ? tags : undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Normalize multiple Google Fonts to unified model
|
|
*
|
|
* @param apiFonts - Array of Google Font items
|
|
* @returns Array of unified fonts
|
|
*/
|
|
export function normalizeGoogleFonts(
|
|
apiFonts: GoogleFontItem[],
|
|
): UnifiedFont[] {
|
|
return apiFonts.map(normalizeGoogleFont);
|
|
}
|
|
|
|
/**
|
|
* Normalize multiple Fontshare fonts to unified model
|
|
*
|
|
* @param apiFonts - Array of Fontshare font items
|
|
* @returns Array of unified fonts
|
|
*/
|
|
export function normalizeFontshareFonts(
|
|
apiFonts: FontshareFont[],
|
|
): UnifiedFont[] {
|
|
return apiFonts.map(normalizeFontshareFont);
|
|
}
|
|
|
|
// Re-export UnifiedFont for backward compatibility
|
|
export type { UnifiedFont } from '../../model/types/normalize';
|