348 lines
9.1 KiB
TypeScript
348 lines
9.1 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,
|
||
|
|
FontProvider,
|
||
|
|
FontSubset,
|
||
|
|
} from '$entities/Font';
|
||
|
|
import type { FontshareFont } from '$entities/Font';
|
||
|
|
import type { GoogleFontItem } from './googleFonts';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Font variant types (standardized)
|
||
|
|
*/
|
||
|
|
export type UnifiedFontVariant = string;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Font style URLs
|
||
|
|
*/
|
||
|
|
export interface FontStyleUrls {
|
||
|
|
/** Regular weight URL */
|
||
|
|
regular?: string;
|
||
|
|
/** Italic URL */
|
||
|
|
italic?: string;
|
||
|
|
/** Bold weight URL */
|
||
|
|
bold?: string;
|
||
|
|
/** Bold italic URL */
|
||
|
|
boldItalic?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Font metadata
|
||
|
|
*/
|
||
|
|
export interface FontMetadata {
|
||
|
|
/** Timestamp when font was cached */
|
||
|
|
cachedAt: number;
|
||
|
|
/** Font version from provider */
|
||
|
|
version?: string;
|
||
|
|
/** Last modified date from provider */
|
||
|
|
lastModified?: string;
|
||
|
|
/** Popularity rank (if available from provider) */
|
||
|
|
popularity?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Font features (variable fonts, axes, tags)
|
||
|
|
*/
|
||
|
|
export interface FontFeatures {
|
||
|
|
/** Whether this is a variable font */
|
||
|
|
isVariable?: boolean;
|
||
|
|
/** Variable font axes (for Fontshare) */
|
||
|
|
axes?: Array<{
|
||
|
|
name: string;
|
||
|
|
property: string;
|
||
|
|
default: number;
|
||
|
|
min: number;
|
||
|
|
max: number;
|
||
|
|
}>;
|
||
|
|
/** Usage tags (for Fontshare) */
|
||
|
|
tags?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Unified font model
|
||
|
|
*
|
||
|
|
* Combines Google Fonts and Fontshare data into a common interface
|
||
|
|
* for consistent font handling across the application.
|
||
|
|
*/
|
||
|
|
export interface UnifiedFont {
|
||
|
|
/** Unique identifier (Google: family name, Fontshare: slug) */
|
||
|
|
id: string;
|
||
|
|
/** Font display name */
|
||
|
|
name: string;
|
||
|
|
/** Font provider (google | fontshare) */
|
||
|
|
provider: FontProvider;
|
||
|
|
/** Font category classification */
|
||
|
|
category: FontCategory;
|
||
|
|
/** Supported character subsets */
|
||
|
|
subsets: FontSubset[];
|
||
|
|
/** Available font variants (weights, styles) */
|
||
|
|
variants: UnifiedFontVariant[];
|
||
|
|
/** URL mapping for font file downloads */
|
||
|
|
styles: FontStyleUrls;
|
||
|
|
/** Additional metadata */
|
||
|
|
metadata: FontMetadata;
|
||
|
|
/** Advanced font features */
|
||
|
|
features: FontFeatures;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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)) {
|
||
|
|
if (variant === 'regular' || variant === '400') {
|
||
|
|
styles.regular = url;
|
||
|
|
} else if (variant === 'italic' || variant === '400italic') {
|
||
|
|
styles.italic = url;
|
||
|
|
} else if (variant === 'bold' || variant === '700') {
|
||
|
|
styles.bold = url;
|
||
|
|
} else if (variant === 'bolditalic' || variant === '700italic') {
|
||
|
|
styles.boldItalic = url;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|