Talk to Figma MCP
by sonnylazuardi
Verified
- cursor-talk-to-figma-mcp
- src
- cursor_mcp_plugin
function uniqBy(arr, predicate) {
const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
return [
...arr
.reduce((map, item) => {
const key = item === null || item === undefined ? item : cb(item);
map.has(key) || map.set(key, item);
return map;
}, new Map())
.values(),
];
}
export const setCharacters = async (node, characters, options) => {
const fallbackFont = options?.fallbackFont || {
family: "Roboto",
style: "Regular",
};
try {
if (node.fontName === figma.mixed) {
if (options?.smartStrategy === "prevail") {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const charFont = node.getRangeFontName(i - 1, i);
const key = `${charFont.family}::${charFont.style}`;
fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
}
const prevailedTreeItem = Object.entries(fontHashTree).sort(
(a, b) => b[1] - a[1]
)[0];
const [family, style] = prevailedTreeItem[0].split("::");
const prevailedFont = {
family,
style,
};
await figma.loadFontAsync(prevailedFont);
node.fontName = prevailedFont;
} else if (options?.smartStrategy === "strict") {
return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
} else if (options?.smartStrategy === "experimental") {
return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
} else {
const firstCharFont = node.getRangeFontName(0, 1);
await figma.loadFontAsync(firstCharFont);
node.fontName = firstCharFont;
}
} else {
await figma.loadFontAsync({
family: node.fontName.family,
style: node.fontName.style,
});
}
} catch (err) {
console.warn(
`Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
err
);
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
}
try {
node.characters = characters;
return true;
} catch (err) {
console.warn(`Failed to set characters. Skipped.`, err);
return false;
}
};
const setCharactersWithStrictMatchFont = async (
node,
characters,
fallbackFont
) => {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const startIdx = i - 1;
const startCharFont = node.getRangeFontName(startIdx, i);
const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
while (i < node.characters.length) {
i++;
const charFont = node.getRangeFontName(i - 1, i);
if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
break;
}
}
fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
}
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
node.characters = characters;
console.log(fontHashTree);
await Promise.all(
Object.keys(fontHashTree).map(async (range) => {
console.log(range, fontHashTree[range]);
const [start, end] = range.split("_");
const [family, style] = fontHashTree[range].split("::");
const matchedFont = {
family,
style,
};
await figma.loadFontAsync(matchedFont);
return node.setRangeFontName(Number(start), Number(end), matchedFont);
})
);
return true;
};
const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
const indices = [];
let temp = startIdx;
for (let i = startIdx; i < endIdx; i++) {
if (
str[i] === delimiter &&
i + startIdx !== endIdx &&
temp !== i + startIdx
) {
indices.push([temp, i + startIdx]);
temp = i + startIdx + 1;
}
}
temp !== endIdx && indices.push([temp, endIdx]);
return indices.filter(Boolean);
};
const buildLinearOrder = (node) => {
const fontTree = [];
const newLinesPos = getDelimiterPos(node.characters, "\n");
newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
const newLinesRangeFont = node.getRangeFontName(
newLinesRangeStart,
newLinesRangeEnd
);
if (newLinesRangeFont === figma.mixed) {
const spacesPos = getDelimiterPos(
node.characters,
" ",
newLinesRangeStart,
newLinesRangeEnd
);
spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeEnd
);
if (spacesRangeFont === figma.mixed) {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeStart[0]
);
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
} else {
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
}
});
} else {
fontTree.push({
start: newLinesRangeStart,
delimiter: "\n",
family: newLinesRangeFont.family,
style: newLinesRangeFont.style,
});
}
});
return fontTree
.sort((a, b) => +a.start - +b.start)
.map(({ family, style, delimiter }) => ({ family, style, delimiter }));
};
const setCharactersWithSmartMatchFont = async (
node,
characters,
fallbackFont
) => {
const rangeTree = buildLinearOrder(node);
const fontsToLoad = uniqBy(
rangeTree,
({ family, style }) => `${family}::${style}`
).map(({ family, style }) => ({
family,
style,
}));
await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
node.fontName = fallbackFont;
node.characters = characters;
let prevPos = 0;
rangeTree.forEach(({ family, style, delimiter }) => {
if (prevPos < node.characters.length) {
const delimeterPos = node.characters.indexOf(delimiter, prevPos);
const endPos =
delimeterPos > prevPos ? delimeterPos : node.characters.length;
const matchedFont = {
family,
style,
};
node.setRangeFontName(prevPos, endPos, matchedFont);
prevPos = endPos + 1;
}
});
return true;
};