UNPKG

@lyubomir-popov/baseline-nudge-generator

Version:

Automatic font metrics reader that generates baseline grid nudges for CSS typography systems

349 lines (303 loc) 11.7 kB
/** * Robust Font Name Extraction Utility * Handles problematic font names with comprehensive validation and fallback strategies * @author Lyubomir Popov */ const path = require('path'); /** * OpenType Name Table ID Priority Order * Based on OpenType specification and common font naming practices */ const NAME_ID_PRIORITY = [ { id: 16, description: 'Typographic Family Name', priority: 1 }, { id: 1, description: 'Font Family Name', priority: 2 }, { id: 4, description: 'Full Font Name', priority: 3 }, { id: 6, description: 'PostScript Name', priority: 4 } ]; /** * Font-specific cleanup patterns * Some fonts need specific cleanup based on their naming patterns */ const FONT_SPECIFIC_CLEANUP = { 'Questa Regular': { patterns: [/\s*Webfont$/i, /\s*Regular$/i], replacement: '' }, 'Inter': { patterns: [/\s*Variable$/i, /\s*VF$/i], replacement: '' }, 'IBM Plex': { patterns: [/\s*Var$/i, /\s*Variable$/i], replacement: '' } }; /** * Invalid name patterns that should be rejected */ const INVALID_NAME_PATTERNS = [ /^\.+$/, // Only dots /^\s*$/, // Empty or whitespace only /^.{1}$/, // Single character /ilovetypography/i, // Common licensing text /for\s+use\s+only/i, // Licensing restriction /version\s+\d/i, // Version information /git-/i, // Git commit info /\d{4}-\d{2}-\d{2}/, // Date patterns /\.(ttf|otf|woff|woff2)$/i, // File extensions /test\s*font/i, // Test fonts /sample/i, // Sample fonts /demo/i, // Demo fonts /placeholder/i, // Placeholder names /untitled/i, // Untitled fonts /^font\s*$/i, // Generic "font" name /^regular\s*$/i, // Just "regular" /^bold\s*$/i, // Just "bold" /^italic\s*$/i, // Just "italic" /^\d+$/, // Just numbers /[^\x20-\x7E]/, // Non-ASCII characters (basic check) ]; /** * Common font name cleanup patterns * More conservative - preserves meaningful weight/style descriptors */ const CLEANUP_PATTERNS = [ // Remove technical suffixes first { pattern: /\s*-webfont$/i, replacement: '' }, { pattern: /-webfont$/i, replacement: '' }, { pattern: /\s*webfont$/i, replacement: '' }, { pattern: /\s*Web\s*Font$/i, replacement: '' }, { pattern: /\s*VF$/i, replacement: '' }, { pattern: /\s*Variable$/i, replacement: '' }, { pattern: /\s*Var$/i, replacement: '' }, // Remove version info { pattern: /\s*\d+\s*$/, replacement: '' }, { pattern: /\s*v\d+(\.\d+)*\s*$/i, replacement: '' }, { pattern: /\s*version\s*\d+\s*$/i, replacement: '' }, { pattern: /\s*git-[a-f0-9]+\s*$/i, replacement: '' }, // Remove brackets and parentheses content { pattern: /\s*\([^)]*\)\s*$/, replacement: '' }, { pattern: /\s*\[[^\]]*\]\s*$/, replacement: '' }, { pattern: /\s*\{[^}]*\}\s*$/, replacement: '' }, // Only remove "Regular" and "Normal" if they appear to be redundant // (i.e., only if there are no other weight/style descriptors) { pattern: /^(.+?)\s*-\s*Regular$/i, replacement: '$1', condition: 'noOtherWeightStyle' }, { pattern: /^(.+?)\s*Regular$/i, replacement: '$1', condition: 'noOtherWeightStyle' }, { pattern: /^(.+?)\s*-\s*Normal$/i, replacement: '$1', condition: 'noOtherWeightStyle' }, { pattern: /^(.+?)\s*Normal$/i, replacement: '$1', condition: 'noOtherWeightStyle' }, // Normalize spacing { pattern: /\s+/g, replacement: ' ' }, ]; /** * Validates if a font name is suitable for use * @param {string} name - The font name to validate * @returns {boolean} - True if the name is valid */ function isValidFontName(name) { if (!name || typeof name !== 'string') { return false; } const trimmed = name.trim(); // Check against invalid patterns for (const pattern of INVALID_NAME_PATTERNS) { if (pattern.test(trimmed)) { return false; } } // Must be at least 2 characters if (trimmed.length < 2) { return false; } // Must contain at least one letter if (!/[a-zA-Z]/.test(trimmed)) { return false; } return true; } /** * Cleans up a font name by removing common suffixes and patterns * More conservative approach that preserves meaningful descriptors * @param {string} name - The font name to clean * @returns {string} - The cleaned font name */ function cleanupFontName(name) { if (!name || typeof name !== 'string') { return ''; } let cleaned = name.trim(); // Apply font-specific cleanup first for (const [fontName, config] of Object.entries(FONT_SPECIFIC_CLEANUP)) { if (cleaned.toLowerCase().includes(fontName.toLowerCase())) { for (const pattern of config.patterns) { cleaned = cleaned.replace(pattern, config.replacement); } } } // Check if this has meaningful weight/style descriptors const hasWeightStyle = /\b(thin|light|regular|medium|semibold|bold|heavy|black|ultra|extra|italic|oblique)\b/i.test(cleaned); // Apply general cleanup patterns for (const { pattern, replacement, condition } of CLEANUP_PATTERNS) { if (condition === 'noOtherWeightStyle' && hasWeightStyle) { // Skip patterns that would remove weight/style info if other descriptors exist continue; } cleaned = cleaned.replace(pattern, replacement); } return cleaned.trim(); } /** * Extracts font family name from filename as fallback * Preserves meaningful weight/style descriptors * @param {string} fontPath - Path to the font file * @returns {string|null} - Extracted font name or null */ function extractFontNameFromPath(fontPath) { try { const filename = path.basename(fontPath, path.extname(fontPath)); // Remove technical prefixes (numbers, prefixes) and suffixes let name = filename .replace(/^\d+-/, '') // Remove leading numbers like "2-" or "5-" .replace(/[-_]/g, ' ') // Replace separators with spaces .replace(/\bwebfont\b/gi, '') // Remove "webfont" but preserve weight/style .trim(); // Clean up the result (this will preserve weight/style if meaningful) name = cleanupFontName(name); if (isValidFontName(name)) { return name; } // Try parent directory name const parentDir = path.basename(path.dirname(fontPath)); const dirName = parentDir .replace(/[-_]/g, ' ') .replace(/\bfonts?\b/gi, '') .trim(); const cleanedDirName = cleanupFontName(dirName); if (isValidFontName(cleanedDirName)) { return cleanedDirName; } return null; } catch (error) { return null; } } /** * Extracts font name from OpenType name table using priority order * @param {Object} nameTable - Font name table object * @returns {string|null} - Extracted font name or null */ function extractFontNameFromNameTable(nameTable) { if (!nameTable || typeof nameTable !== 'object') { return null; } // Try name table records in priority order const nameProperties = [ 'preferredFamily', // Name ID 16 'fontFamily', // Name ID 1 'fullName', // Name ID 4 'postScriptName' // Name ID 6 ]; for (const prop of nameProperties) { if (nameTable[prop]) { let name = null; // Try English first if (nameTable[prop].en) { name = nameTable[prop].en; } else if (typeof nameTable[prop] === 'string') { name = nameTable[prop]; } else if (typeof nameTable[prop] === 'object') { // Try other languages const values = Object.values(nameTable[prop]); if (values.length > 0) { name = values[0]; } } if (name && typeof name === 'string') { const cleaned = cleanupFontName(name); if (isValidFontName(cleaned)) { return cleaned; } } } } return null; } /** * Extracts font name from opentype.js font object * @param {Object} otFont - OpenType.js font object * @returns {string|null} - Extracted font name or null */ function extractFontNameFromOpenType(otFont) { if (!otFont || !otFont.names) { return null; } const nameProperties = [ 'preferredFamily', 'fontFamily', 'fullName', 'postScriptName' ]; for (const prop of nameProperties) { if (otFont.names[prop]) { let name = null; if (otFont.names[prop].en) { name = otFont.names[prop].en; } else if (typeof otFont.names[prop] === 'string') { name = otFont.names[prop]; } else if (typeof otFont.names[prop] === 'object') { const values = Object.values(otFont.names[prop]); if (values.length > 0) { name = values[0]; } } if (name && typeof name === 'string') { const cleaned = cleanupFontName(name); if (isValidFontName(cleaned)) { return cleaned; } } } } return null; } /** * Main font name extraction function with comprehensive fallback strategy * @param {string} fontPath - Path to the font file * @param {Object} nameTable - Font name table (fontkit) * @param {Object} otFont - OpenType.js font object (optional) * @returns {string} - Extracted font name or fallback */ function extractRobustFontName(fontPath, nameTable = null, otFont = null) { // Try name table first (most reliable) if (nameTable) { const nameFromTable = extractFontNameFromNameTable(nameTable); if (nameFromTable) { return nameFromTable; } } // Try opentype.js as fallback if (otFont) { const nameFromOpenType = extractFontNameFromOpenType(otFont); if (nameFromOpenType) { return nameFromOpenType; } } // Try extracting from file path const nameFromPath = extractFontNameFromPath(fontPath); if (nameFromPath) { return nameFromPath; } // Last resort: use filename without extension const filename = path.basename(fontPath, path.extname(fontPath)); return filename.replace(/[-_]/g, ' ').trim() || 'Unknown Font'; } module.exports = { extractRobustFontName, isValidFontName, cleanupFontName, extractFontNameFromPath, extractFontNameFromNameTable, extractFontNameFromOpenType, NAME_ID_PRIORITY, INVALID_NAME_PATTERNS, CLEANUP_PATTERNS };