import { getFontStyle, loadInterFont } from "../utils";
interface CreateTextParams {
x?: number;
y?: number;
text?: string;
fontSize?: number;
fontWeight?: number;
fontColor?: { r: number; g: number; b: number; a?: number };
name?: string;
parentId?: string;
}
interface SetTextContentParams {
nodeId: string;
text: string;
}
interface SetMultipleTextContentsParams {
nodeId: string;
text: Array<{ nodeId: string; text: string }>;
}
interface ScanTextNodesParams {
nodeId: string;
useChunking?: boolean;
chunkSize?: number;
}
/**
* Create a text node
*/
export async function createText(params: CreateTextParams): Promise<{
id: string;
name: string;
x: number;
y: number;
characters: string;
parentId?: string;
}> {
const {
x = 0,
y = 0,
text = "Text",
fontSize = 14,
fontWeight = 400,
fontColor = { r: 0, g: 0, b: 0, a: 1 },
name = "",
parentId,
} = params;
const textNode = figma.createText();
textNode.x = x;
textNode.y = y;
textNode.name = name || text;
try {
await loadInterFont(fontWeight);
textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) };
textNode.fontSize = fontSize;
} catch (error) {
console.error("Error setting font", error);
// Fallback to default font
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
textNode.fontName = { family: "Inter", style: "Regular" };
textNode.fontSize = fontSize;
}
textNode.characters = text;
// Set text color
textNode.fills = [
{
type: "SOLID",
color: { r: fontColor.r, g: fontColor.g, b: fontColor.b },
opacity: fontColor.a ?? 1,
},
];
// If parentId is provided, append to that node
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 as ChildrenMixin).appendChild(textNode);
} else {
figma.currentPage.appendChild(textNode);
}
return {
id: textNode.id,
name: textNode.name,
x: textNode.x,
y: textNode.y,
characters: textNode.characters,
parentId: textNode.parent?.id,
};
}
/**
* Set text content on a text node
*/
export async function setTextContent(params: SetTextContentParams): Promise<{
id: string;
name: string;
characters: string;
}> {
const { nodeId, text } = params;
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (node.type !== "TEXT") {
throw new Error(`Node is not a text node: ${nodeId}`);
}
const textNode = node as TextNode;
// Load the font before changing text
await figma.loadFontAsync(textNode.fontName as FontName);
textNode.characters = text;
return {
id: textNode.id,
name: textNode.name,
characters: textNode.characters,
};
}
/**
* Set multiple text contents in parallel
*/
export async function setMultipleTextContents(
params: SetMultipleTextContentsParams,
): Promise<{
success: boolean;
nodeId: string;
replacementsApplied: number;
replacementsFailed: number;
totalReplacements: number;
completedInChunks: number;
results: Array<{
success: boolean;
nodeId: string;
error?: string;
}>;
}> {
const { nodeId, text } = params;
const chunkSize = 5;
const results: Array<{ success: boolean; nodeId: string; error?: string }> =
[];
let completedChunks = 0;
// Process in chunks
for (let i = 0; i < text.length; i += chunkSize) {
const chunk = text.slice(i, i + chunkSize);
await Promise.all(
chunk.map(async ({ nodeId: textNodeId, text: newText }) => {
try {
const node = await figma.getNodeByIdAsync(textNodeId);
if (!node) {
results.push({
success: false,
nodeId: textNodeId,
error: "Node not found",
});
return;
}
if (node.type !== "TEXT") {
results.push({
success: false,
nodeId: textNodeId,
error: "Not a text node",
});
return;
}
const textNode = node as TextNode;
await figma.loadFontAsync(textNode.fontName as FontName);
textNode.characters = newText;
results.push({ success: true, nodeId: textNodeId });
} catch (error) {
results.push({
success: false,
nodeId: textNodeId,
error: error instanceof Error ? error.message : String(error),
});
}
}),
);
completedChunks++;
}
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
return {
success: successful > 0,
nodeId,
replacementsApplied: successful,
replacementsFailed: failed,
totalReplacements: text.length,
completedInChunks: completedChunks,
results,
};
}
/**
* Scan text nodes in a tree
*/
export async function scanTextNodes(params: ScanTextNodesParams): Promise<{
success: boolean;
totalNodes: number;
processedNodes: number;
chunks?: number;
textNodes: Array<{
id: string;
name: string;
characters: string;
path: string;
}>;
}> {
const { nodeId, useChunking = false, chunkSize = 50 } = params;
const rootNode = await figma.getNodeByIdAsync(nodeId);
if (!rootNode) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
const textNodes: Array<{
id: string;
name: string;
characters: string;
path: string;
}> = [];
function getNodePath(node: BaseNode): string {
const path: string[] = [];
let current: BaseNode | null = node;
while (current?.parent) {
path.unshift(current.name);
current = current.parent;
}
return path.join(" > ");
}
function scanNode(node: BaseNode): void {
if (node.type === "TEXT") {
const textNode = node as TextNode;
textNodes.push({
id: textNode.id,
name: textNode.name,
characters: textNode.characters,
path: getNodePath(textNode),
});
}
if ("children" in node) {
const containerNode = node as ChildrenMixin;
for (const child of containerNode.children) {
scanNode(child);
}
}
}
scanNode(rootNode);
if (useChunking) {
return {
success: true,
totalNodes: textNodes.length,
processedNodes: textNodes.length,
chunks: Math.ceil(textNodes.length / chunkSize),
textNodes,
};
}
return {
success: true,
totalNodes: textNodes.length,
processedNodes: textNodes.length,
textNodes,
};
}