Cursor Talk To Figma MCP
by toiletmadarchut
Verified
- cursor-talk-to-figma-mcp
- src
- cursor_mcp_plugin
// This is the main code file for the Cursor MCP Figma plugin
// It handles Figma API commands
// Plugin state
const state = {
serverPort: 3055, // Default port
};
// Show UI
figma.showUI(__html__, { width: 350, height: 450 });
// Plugin commands from UI
figma.ui.onmessage = async (msg) => {
switch (msg.type) {
case "update-settings":
updateSettings(msg);
break;
case "notify":
figma.notify(msg.message);
break;
case "close-plugin":
figma.closePlugin();
break;
case "execute-command":
// Execute commands received from UI (which gets them from WebSocket)
try {
const result = await handleCommand(msg.command, msg.params);
// Send result back to UI
figma.ui.postMessage({
type: "command-result",
id: msg.id,
result,
});
} catch (error) {
figma.ui.postMessage({
type: "command-error",
id: msg.id,
error: error.message || "Error executing command",
});
}
break;
}
};
// Listen for plugin commands from menu
figma.on("run", ({ command }) => {
figma.ui.postMessage({ type: "auto-connect" });
});
// Update plugin settings
function updateSettings(settings) {
if (settings.serverPort) {
state.serverPort = settings.serverPort;
}
figma.clientStorage.setAsync("settings", {
serverPort: state.serverPort,
});
}
// Handle commands from UI
async function handleCommand(command, params) {
switch (command) {
case "get_document_info":
return await getDocumentInfo();
case "get_selection":
return await getSelection();
case "get_node_info":
if (!params || !params.nodeId) {
throw new Error("Missing nodeId parameter");
}
return await getNodeInfo(params.nodeId);
case "create_rectangle":
return await createRectangle(params);
case "create_frame":
return await createFrame(params);
case "create_text":
return await createText(params);
case "set_fill_color":
return await setFillColor(params);
case "set_stroke_color":
return await setStrokeColor(params);
case "move_node":
return await moveNode(params);
case "resize_node":
return await resizeNode(params);
case "delete_node":
return await deleteNode(params);
case "get_styles":
return await getStyles();
case "get_local_components":
return await getLocalComponents();
// case "get_team_components":
// return await getTeamComponents();
case "create_component_instance":
return await createComponentInstance(params);
case "export_node_as_image":
return await exportNodeAsImage(params);
case "execute_code":
return await executeCode(params);
case "set_corner_radius":
return await setCornerRadius(params);
default:
throw new Error(`Unknown command: ${command}`);
}
}
// Command implementations
async function getDocumentInfo() {
await figma.currentPage.loadAsync();
const page = figma.currentPage;
return {
name: page.name,
id: page.id,
type: page.type,
children: page.children.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
})),
currentPage: {
id: page.id,
name: page.name,
childCount: page.children.length,
},
pages: [
{
id: page.id,
name: page.name,
childCount: page.children.length,
},
],
};
}
async function getSelection() {
return {
selectionCount: figma.currentPage.selection.length,
selection: figma.currentPage.selection.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
visible: node.visible,
})),
};
}
async function getNodeInfo(nodeId) {
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Base node information
const nodeInfo = {
id: node.id,
name: node.name,
type: node.type,
visible: node.visible,
};
// Add position and size for SceneNode
if ("x" in node && "y" in node) {
nodeInfo.x = node.x;
nodeInfo.y = node.y;
}
if ("width" in node && "height" in node) {
nodeInfo.width = node.width;
nodeInfo.height = node.height;
}
// Add fills for nodes with fills
if ("fills" in node) {
nodeInfo.fills = node.fills;
}
// Add strokes for nodes with strokes
if ("strokes" in node) {
nodeInfo.strokes = node.strokes;
if ("strokeWeight" in node) {
nodeInfo.strokeWeight = node.strokeWeight;
}
}
// Add children for parent nodes
if ("children" in node) {
nodeInfo.children = node.children.map((child) => ({
id: child.id,
name: child.name,
type: child.type,
}));
}
// Add text-specific properties
if (node.type === "TEXT") {
nodeInfo.characters = node.characters;
nodeInfo.fontSize = node.fontSize;
nodeInfo.fontName = node.fontName;
}
return nodeInfo;
}
async function createRectangle(params) {
const {
x = 0,
y = 0,
width = 100,
height = 100,
name = "Rectangle",
parentId,
} = params || {};
const rect = figma.createRectangle();
rect.x = x;
rect.y = y;
rect.resize(width, height);
rect.name = name;
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(rect);
} else {
figma.currentPage.appendChild(rect);
}
return {
id: rect.id,
name: rect.name,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
parentId: rect.parent ? rect.parent.id : undefined,
};
}
async function createFrame(params) {
const {
x = 0,
y = 0,
width = 100,
height = 100,
name = "Frame",
parentId,
fillColor,
strokeColor,
strokeWeight,
} = params || {};
const frame = figma.createFrame();
frame.x = x;
frame.y = y;
frame.resize(width, height);
frame.name = name;
// Set fill color if provided
if (fillColor) {
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(fillColor.r) || 0,
g: parseFloat(fillColor.g) || 0,
b: parseFloat(fillColor.b) || 0,
},
opacity: parseFloat(fillColor.a) || 1,
};
frame.fills = [paintStyle];
}
// Set stroke color and weight if provided
if (strokeColor) {
const strokeStyle = {
type: "SOLID",
color: {
r: parseFloat(strokeColor.r) || 0,
g: parseFloat(strokeColor.g) || 0,
b: parseFloat(strokeColor.b) || 0,
},
opacity: parseFloat(strokeColor.a) || 1,
};
frame.strokes = [strokeStyle];
}
// Set stroke weight if provided
if (strokeWeight !== undefined) {
frame.strokeWeight = strokeWeight;
}
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(frame);
} else {
figma.currentPage.appendChild(frame);
}
return {
id: frame.id,
name: frame.name,
x: frame.x,
y: frame.y,
width: frame.width,
height: frame.height,
fills: frame.fills,
strokes: frame.strokes,
strokeWeight: frame.strokeWeight,
parentId: frame.parent ? frame.parent.id : undefined,
};
}
async function createText(params) {
const {
x = 0,
y = 0,
text = "Text",
fontSize = 14,
fontWeight = 400,
fontColor = { r: 0, g: 0, b: 0, a: 1 }, // Default to black
name = "Text",
parentId,
} = params || {};
// Map common font weights to Figma font styles
const getFontStyle = (weight) => {
switch (weight) {
case 100:
return "Thin";
case 200:
return "Extra Light";
case 300:
return "Light";
case 400:
return "Regular";
case 500:
return "Medium";
case 600:
return "Semi Bold";
case 700:
return "Bold";
case 800:
return "Extra Bold";
case 900:
return "Black";
default:
return "Regular";
}
};
const textNode = figma.createText();
textNode.x = x;
textNode.y = y;
textNode.name = name;
try {
await figma.loadFontAsync({
family: "Inter",
style: getFontStyle(fontWeight),
});
textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) };
textNode.fontSize = parseInt(fontSize);
} catch (error) {
console.error("Error setting font size", error);
}
setCharacters(textNode, text);
// Set text color
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(fontColor.r) || 0,
g: parseFloat(fontColor.g) || 0,
b: parseFloat(fontColor.b) || 0,
},
opacity: parseFloat(fontColor.a) || 1,
};
textNode.fills = [paintStyle];
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(textNode);
} else {
figma.currentPage.appendChild(textNode);
}
return {
id: textNode.id,
name: textNode.name,
x: textNode.x,
y: textNode.y,
width: textNode.width,
height: textNode.height,
characters: textNode.characters,
fontSize: textNode.fontSize,
fontWeight: fontWeight,
fontColor: fontColor,
fontName: textNode.fontName,
fills: textNode.fills,
parentId: textNode.parent ? textNode.parent.id : undefined,
};
}
async function setFillColor(params) {
console.log("setFillColor", params);
const {
nodeId,
color: { r, g, b, a },
} = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("fills" in node)) {
throw new Error(`Node does not support fills: ${nodeId}`);
}
// Create RGBA color
const rgbColor = {
r: parseFloat(r) || 0,
g: parseFloat(g) || 0,
b: parseFloat(b) || 0,
a: parseFloat(a) || 1,
};
// Set fill
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(rgbColor.r),
g: parseFloat(rgbColor.g),
b: parseFloat(rgbColor.b),
},
opacity: parseFloat(rgbColor.a),
};
console.log("paintStyle", paintStyle);
node.fills = [paintStyle];
return {
id: node.id,
name: node.name,
fills: [paintStyle],
};
}
async function setStrokeColor(params) {
const {
nodeId,
color: { r, g, b, a },
weight = 1,
} = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("strokes" in node)) {
throw new Error(`Node does not support strokes: ${nodeId}`);
}
// Create RGBA color
const rgbColor = {
r: r !== undefined ? r : 0,
g: g !== undefined ? g : 0,
b: b !== undefined ? b : 0,
a: a !== undefined ? a : 1,
};
// Set stroke
const paintStyle = {
type: "SOLID",
color: {
r: rgbColor.r,
g: rgbColor.g,
b: rgbColor.b,
},
opacity: rgbColor.a,
};
node.strokes = [paintStyle];
// Set stroke weight if available
if ("strokeWeight" in node) {
node.strokeWeight = weight;
}
return {
id: node.id,
name: node.name,
strokes: node.strokes,
strokeWeight: "strokeWeight" in node ? node.strokeWeight : undefined,
};
}
async function moveNode(params) {
const { nodeId, x, y } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (x === undefined || y === undefined) {
throw new Error("Missing x or y parameters");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("x" in node) || !("y" in node)) {
throw new Error(`Node does not support position: ${nodeId}`);
}
node.x = x;
node.y = y;
return {
id: node.id,
name: node.name,
x: node.x,
y: node.y,
};
}
async function resizeNode(params) {
const { nodeId, width, height } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (width === undefined || height === undefined) {
throw new Error("Missing width or height parameters");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("resize" in node)) {
throw new Error(`Node does not support resizing: ${nodeId}`);
}
node.resize(width, height);
return {
id: node.id,
name: node.name,
width: node.width,
height: node.height,
};
}
async function deleteNode(params) {
const { nodeId } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Save node info before deleting
const nodeInfo = {
id: node.id,
name: node.name,
type: node.type,
};
node.remove();
return nodeInfo;
}
async function getStyles() {
const styles = {
colors: await figma.getLocalPaintStylesAsync(),
texts: await figma.getLocalTextStylesAsync(),
effects: await figma.getLocalEffectStylesAsync(),
grids: await figma.getLocalGridStylesAsync(),
};
return {
colors: styles.colors.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
paint: style.paints[0],
})),
texts: styles.texts.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
fontSize: style.fontSize,
fontName: style.fontName,
})),
effects: styles.effects.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
})),
grids: styles.grids.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
})),
};
}
async function getLocalComponents() {
await figma.loadAllPagesAsync();
const components = figma.root.findAllWithCriteria({
types: ["COMPONENT"],
});
return {
count: components.length,
components: components.map((component) => ({
id: component.id,
name: component.name,
key: "key" in component ? component.key : null,
})),
};
}
// async function getTeamComponents() {
// try {
// const teamComponents =
// await figma.teamLibrary.getAvailableComponentsAsync();
// return {
// count: teamComponents.length,
// components: teamComponents.map((component) => ({
// key: component.key,
// name: component.name,
// description: component.description,
// libraryName: component.libraryName,
// })),
// };
// } catch (error) {
// throw new Error(`Error getting team components: ${error.message}`);
// }
// }
async function createComponentInstance(params) {
const { componentKey, x = 0, y = 0 } = params || {};
if (!componentKey) {
throw new Error("Missing componentKey parameter");
}
try {
const component = await figma.importComponentByKeyAsync(componentKey);
const instance = component.createInstance();
instance.x = x;
instance.y = y;
figma.currentPage.appendChild(instance);
return {
id: instance.id,
name: instance.name,
x: instance.x,
y: instance.y,
width: instance.width,
height: instance.height,
componentId: instance.componentId,
};
} catch (error) {
throw new Error(`Error creating component instance: ${error.message}`);
}
}
async function exportNodeAsImage(params) {
const { nodeId, format = "PNG", scale = 1 } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("exportAsync" in node)) {
throw new Error(`Node does not support exporting: ${nodeId}`);
}
try {
const settings = {
format: format,
constraint: { type: "SCALE", value: scale },
};
const bytes = await node.exportAsync(settings);
let mimeType;
switch (format) {
case "PNG":
mimeType = "image/png";
break;
case "JPG":
mimeType = "image/jpeg";
break;
case "SVG":
mimeType = "image/svg+xml";
break;
case "PDF":
mimeType = "application/pdf";
break;
default:
mimeType = "application/octet-stream";
}
// Convert to base64
const uint8Array = new Uint8Array(bytes);
let binary = "";
for (let i = 0; i < uint8Array.length; i++) {
binary += String.fromCharCode(uint8Array[i]);
}
const base64 = btoa(binary);
const imageData = `data:${mimeType};base64,${base64}`;
return {
nodeId,
format,
scale,
mimeType,
imageData,
};
} catch (error) {
throw new Error(`Error exporting node as image: ${error.message}`);
}
}
async function executeCode(params) {
const { code } = params || {};
if (!code) {
throw new Error("Missing code parameter");
}
try {
// Execute the provided code
// Note: This is potentially unsafe, but matches the Blender MCP functionality
const executeFn = new Function(
"figma",
"selection",
`
try {
const result = (async () => {
${code}
})();
return result;
} catch (error) {
throw new Error('Error executing code: ' + error.message);
}
`
);
const result = await executeFn(figma, figma.currentPage.selection);
return { result };
} catch (error) {
throw new Error(`Error executing code: ${error.message}`);
}
}
async function setCornerRadius(params) {
const { nodeId, radius, corners } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (radius === undefined) {
throw new Error("Missing radius parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Check if node supports corner radius
if (!("cornerRadius" in node)) {
throw new Error(`Node does not support corner radius: ${nodeId}`);
}
// If corners array is provided, set individual corner radii
if (corners && Array.isArray(corners) && corners.length === 4) {
if ("topLeftRadius" in node) {
// Node supports individual corner radii
if (corners[0]) node.topLeftRadius = radius;
if (corners[1]) node.topRightRadius = radius;
if (corners[2]) node.bottomRightRadius = radius;
if (corners[3]) node.bottomLeftRadius = radius;
} else {
// Node only supports uniform corner radius
node.cornerRadius = radius;
}
} else {
// Set uniform corner radius
node.cornerRadius = radius;
}
return {
id: node.id,
name: node.name,
cornerRadius: "cornerRadius" in node ? node.cornerRadius : undefined,
topLeftRadius: "topLeftRadius" in node ? node.topLeftRadius : undefined,
topRightRadius: "topRightRadius" in node ? node.topRightRadius : undefined,
bottomRightRadius:
"bottomRightRadius" in node ? node.bottomRightRadius : undefined,
bottomLeftRadius:
"bottomLeftRadius" in node ? node.bottomLeftRadius : undefined,
};
}
// Initialize settings on load
(async function initializePlugin() {
try {
const savedSettings = await figma.clientStorage.getAsync("settings");
if (savedSettings) {
if (savedSettings.serverPort) {
state.serverPort = savedSettings.serverPort;
}
}
// Send initial settings to UI
figma.ui.postMessage({
type: "init-settings",
settings: {
serverPort: state.serverPort,
},
});
} catch (error) {
console.error("Error loading settings:", error);
}
})();
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(),
];
}
const setCharacters = async (node, characters, options) => {
const fallbackFont = (options && options.fallbackFont) || {
family: "Inter",
style: "Regular",
};
try {
if (node.fontName === figma.mixed) {
if (options && 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 && options.smartStrategy === "strict") {
return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
} else if (options && 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;
};