2026-01-06 14:38:55 +03:00
|
|
|
/**
|
|
|
|
|
* 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,
|
refactor(font): consolidate all types into single types.ts file
- Created unified model/types.ts with all type definitions
- Consolidated domain types (FontCategory, FontProvider, FontSubset)
- Consolidated Google Fonts API types (FontItem, GoogleFontsApiModel, etc.)
- Consolidated Fontshare API types (FontshareFont, FontshareStyle, etc.)
- Consolidated normalization types (UnifiedFont, FontStyleUrls, etc.)
- Consolidated store types (FontCollectionStore, FontCollectionFilters, etc.)
- Removed duplicate type files (font.ts, google_fonts.ts, fontshare_fonts.ts)
- Updated all imports to use consolidated types
- Updated normalize module to import from /Font
- Updated API clients to re-export types for backward compatibility
- Updated store to use centralized types
- Updated Font index.ts to export all types
Benefits:
- Centralized type definitions in single location
- Cleaner imports (single import from /Font)
- Better code organization with clear sections
- Follows FSD principles (types in model layer)
- No duplicate type definitions
2026-01-06 15:06:38 +03:00
|
|
|
FontStyleUrls,
|
2026-01-06 14:38:55 +03:00
|
|
|
FontSubset,
|
2026-01-06 21:31:25 +03:00
|
|
|
FontshareFont,
|
|
|
|
|
GoogleFontItem,
|
|
|
|
|
UnifiedFont,
|
|
|
|
|
} from '../../model/types';
|
2026-01-06 14:38:55 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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)) {
|
2026-01-06 15:11:16 +03:00
|
|
|
const urlString = url as string; // Type assertion for Record<string, string>
|
2026-01-06 14:38:55 +03:00
|
|
|
if (variant === 'regular' || variant === '400') {
|
2026-01-06 15:11:16 +03:00
|
|
|
styles.regular = urlString;
|
2026-01-06 14:38:55 +03:00
|
|
|
} else if (variant === 'italic' || variant === '400italic') {
|
2026-01-06 15:11:16 +03:00
|
|
|
styles.italic = urlString;
|
2026-01-06 14:38:55 +03:00
|
|
|
} else if (variant === 'bold' || variant === '700') {
|
2026-01-06 15:11:16 +03:00
|
|
|
styles.bold = urlString;
|
2026-01-06 14:38:55 +03:00
|
|
|
} else if (variant === 'bolditalic' || variant === '700italic') {
|
2026-01-06 15:11:16 +03:00
|
|
|
styles.boldItalic = urlString;
|
2026-01-06 14:38:55 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
2026-01-06 15:23:08 +03:00
|
|
|
|
|
|
|
|
// Re-export UnifiedFont for backward compatibility
|
2026-01-06 21:31:25 +03:00
|
|
|
export type { UnifiedFont } from '../../model/types/normalize';
|