/** * src/figma/font-resolver.js * Resolves CSS font families and weights to Figma font names. * Builds a font map for the entire DOM tree before node creation. * * NOTE: This module runs in Node.js (not inside Figma). * It outputs a fontMap that the Figma plugin reads and pre-loads. */ import { walk } from '../core/dom-tree.js'; import { WEIGHT_MAP } from '../utils/units.js'; // Known Google Fonts available in Figma + their available styles const FIGMA_FONT_STYLES = { 'Playfair Display': ['Thin', 'ExtraLight', 'Light', 'Regular', 'Medium', 'SemiBold', 'Bold', 'ExtraBold', 'Black', 'Thin Italic', 'ExtraLight Italic', 'Light Italic', 'Italic', 'Medium Italic', 'SemiBold Italic', 'Bold Italic', 'ExtraBold Italic', 'Black Italic'], 'DM Sans': ['Thin', 'ExtraLight', 'Light', 'Regular', 'Medium', 'SemiBold', 'Bold', 'ExtraBold', 'Black', 'Thin Italic', 'ExtraLight Italic', 'Light Italic', 'Italic', 'Medium Italic', 'SemiBold Italic', 'Bold Italic', 'ExtraBold Italic', 'Black Italic'], 'Bebas Neue': ['Regular'], 'Inter': ['Thin', 'ExtraLight', 'Light', 'Regular', 'Medium', 'SemiBold', 'Bold', 'ExtraBold', 'Black', 'Thin Italic', 'ExtraLight Italic', 'Light Italic', 'Italic', 'Medium Italic', 'SemiBold Italic', 'Bold Italic', 'ExtraBold Italic', 'Black Italic'], Georgia: ['Regular', 'Italic', 'Bold', 'Bold Italic'], 'Courier New': ['Regular', 'Italic', 'Bold', 'Bold Italic'], }; /** * @param {object} domTree * @returns {Promise} Map of "family|weight|italic" → { family, style } */ export async function resolveFonts(domTree) { const needed = new Set(); walk(domTree, (node) => { const { fontFamily, fontWeight, fontStyle } = node.computed ?? {}; if (fontFamily) { const key = `${fontFamily}|${fontWeight ?? '400'}|${fontStyle ?? 'normal'}`; needed.add(key); } for (const run of node.textRuns ?? []) { const runFamily = run.computed?.fontFamily; if (!runFamily) continue; const key = `${runFamily}|${run.computed?.fontWeight ?? '400'}|${run.computed?.fontStyle ?? 'normal'}`; needed.add(key); } for (const pseudo of [node.pseudo?.before, node.pseudo?.after]) { const pseudoFamily = pseudo?.computed?.fontFamily; if (!pseudoFamily) continue; const key = `${pseudoFamily}|${pseudo.computed?.fontWeight ?? '400'}|${pseudo.computed?.fontStyle ?? 'normal'}`; needed.add(key); } }); const fontMap = {}; for (const key of needed) { const [family, weight, style] = key.split('|'); const resolved = resolveFont(family, weight, style === 'italic'); fontMap[key] = resolved; } return fontMap; } /** * Strip quotes from CSS font-family string. * e.g. "'Playfair Display', serif" → "Playfair Display" */ function cleanFamilyName(css) { return getFontFamilyStack(css)[0] ?? ''; } /** * Resolve to a specific Figma font, with fallback chain. */ function resolveFont(cssFamily, weightStr, isItalic) { const stack = getFontFamilyStack(cssFamily); const family = cleanFamilyName(cssFamily); const weight = parseInt(weightStr) || 400; const styleName = WEIGHT_MAP[weight] ?? 'Regular'; const italicSuffix = isItalic ? ' Italic' : ''; const targetStyle = styleName === 'Regular' && isItalic ? 'Italic' : `${styleName}${italicSuffix}`; const requestedNamedFamily = getRequestedNamedFamily(stack); const candidates = []; const availableStackFamily = stack.find((name) => FIGMA_FONT_STYLES[name]); if (availableStackFamily) { candidates.push(availableStackFamily); } else if (requestedNamedFamily) { candidates.push(requestedNamedFamily); } else { const generic = getGenericFontFamily(stack); if (generic === 'serif') { candidates.push('Georgia'); } else if (generic === 'monospace') { candidates.push('Courier New'); } else { candidates.push('Inter'); } } if (family && family !== candidates[0] && FIGMA_FONT_STYLES[family]) { candidates.push(family); } if (!candidates.includes('Inter')) { candidates.push('Inter'); } for (const candidate of candidates) { const styles = FIGMA_FONT_STYLES[candidate]; if (!styles) { return { family: candidate, style: targetStyle }; } if (styles.includes(targetStyle)) { return { family: candidate, style: targetStyle }; } if (styles.includes('Regular')) { return { family: candidate, style: 'Regular' }; } } return { family: 'Inter', style: 'Regular' }; } function getFontFamilyStack(cssFamily) { return String(cssFamily || '') .split(',') .map((part) => part.trim().replace(/['"]/g, '')) .filter(Boolean); } function getGenericFontFamily(stack) { if (!Array.isArray(stack) || stack.length === 0) { return null; } const last = stack[stack.length - 1].toLowerCase(); if (last === 'serif' || last === 'sans-serif' || last === 'monospace') { return last; } return null; } function getRequestedNamedFamily(stack) { if (!Array.isArray(stack)) { return null; } return stack.find((name) => !isGenericFontFamily(name)) || null; } function isGenericFontFamily(name) { const normalized = String(name || '').toLowerCase(); return normalized === 'serif' || normalized === 'sans-serif' || normalized === 'monospace' || normalized === 'cursive' || normalized === 'fantasy' || normalized === 'system-ui' || normalized === 'ui-serif' || normalized === 'ui-sans-serif' || normalized === 'ui-monospace' || normalized === 'ui-rounded'; } /** * @typedef {Record} FontMap */