tempelhtml / src /figma /font-resolver.js
jehian's picture
Upload 16 files
dc7ee79 verified
/**
* 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<FontMap>} 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<string, { family: string, style: string }>} FontMap
*/