slint/tools/figma-inspector/backend/utils/property-parsing.ts
2025-06-19 21:38:30 +03:00

1141 lines
44 KiB
TypeScript

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import {
formatStructName,
extractHierarchy,
sanitizePropertyName,
} from "./export-variables";
export const indentation = " ";
const rectangleProperties = [
"x",
"y",
"width",
"height",
"fill",
"opacity",
"border-radius",
"border-width",
"border-color",
];
const textProperties = [
"x",
"y",
"text",
"fill",
"font-family",
"font-size",
"font-weight",
"horizontal-alignment",
];
const pathProperties = [
"width",
"height",
"x",
"y",
"commands",
"fill",
"stroke",
"stroke-width",
];
const unsupportedNodeProperties = ["x", "y", "width", "height", "opacity"];
export function rgbToHex(rgba: RGB | RGBA): string {
const red = Math.round(rgba.r * 255);
const green = Math.round(rgba.g * 255);
const blue = Math.round(rgba.b * 255);
const alphaF = "a" in rgba ? rgba.a : 1;
const alpha = Math.round(alphaF * 255);
const values = alphaF < 1 ? [red, green, blue, alpha] : [red, green, blue];
return "#" + values.map((x) => x.toString(16).padStart(2, "0")).join("");
}
export function generateRadialGradient(fill: {
opacity: number;
gradientStops: ReadonlyArray<{
color: { r: number; g: number; b: number; a: number };
position: number;
}>;
gradientTransform: number[][];
}): string {
if (!fill.gradientStops || fill.gradientStops.length < 2) {
return "";
}
const stops = fill.gradientStops
.map((stop) => {
const { r, g, b, a } = stop.color;
const hexColor = rgbToHex({ r, g, b, a });
const position = Math.round(stop.position * 100);
return `${hexColor} ${position}%`;
})
.join(", ");
return `@radial-gradient(circle, ${stops})`;
}
export function generateLinearGradient(fill: {
opacity: number;
gradientStops: ReadonlyArray<{ color: RGBA; position: number }>;
gradientTransform: number[][];
}): string {
if (!fill.gradientStops || fill.gradientStops.length < 2) {
return "";
}
const [a, b] = fill.gradientTransform[0];
const angle = (90 + Math.round(Math.atan2(b, a) * (180 / Math.PI))) % 360;
const stops = fill.gradientStops
.map((stop) => {
const { r, g, b, a } = stop.color;
const hexColor = rgbToHex({ r, g, b, a });
const position = Math.round(stop.position * 100);
return `${hexColor} ${position}%`;
})
.join(", ");
return `@linear-gradient(${angle}deg, ${stops})`;
}
function roundNumber(value: number): number | null {
if (value === 0) {
return null;
}
return Number(value.toFixed(3));
}
export async function getBorderRadius(
node: SceneNode,
useVariables: boolean,
): Promise<string | null> {
if ("boundVariables" in node) {
const boundVars = (node as any).boundVariables;
const boundCornerRadiusId = boundVars?.cornerRadius?.id;
if (boundCornerRadiusId && useVariables) {
const path = await getVariablePathString(boundCornerRadiusId);
if (path) {
return `${indentation}border-radius: ${path};`;
}
}
const cornerBindings = [
{
prop: "topLeftRadius",
slint: "border-top-left-radius",
id: boundVars?.topLeftRadius?.id,
},
{
prop: "topRightRadius",
slint: "border-top-right-radius",
id: boundVars?.topRightRadius?.id,
},
{
prop: "bottomLeftRadius",
slint: "border-bottom-left-radius",
id: boundVars?.bottomLeftRadius?.id,
},
{
prop: "bottomRightRadius",
slint: "border-bottom-right-radius",
id: boundVars?.bottomRightRadius?.id,
},
] as const;
const boundIndividualCorners = cornerBindings.filter((c) => c.id);
if (boundIndividualCorners.length > 0 && useVariables) {
// --- Check if all bound corners use the SAME variable ID ---
const allSameId = boundIndividualCorners.every(
(c) => c.id === boundIndividualCorners[0].id,
);
if (allSameId && boundIndividualCorners.length === 4) {
// All 4 corners bound to the same variable -> use shorthand border-radius
const path = await getVariablePathString(
boundIndividualCorners[0].id,
);
if (path) {
return `${indentation}border-radius: ${path};`;
}
} else {
const radiusStrings: string[] = [];
for (const corner of boundIndividualCorners) {
const path = await getVariablePathString(corner.id);
if (path) {
radiusStrings.push(
`${indentation}${corner.slint}: ${path};`,
);
}
}
if (radiusStrings.length > 0) {
return radiusStrings.join("\n");
}
}
}
}
// check if node has cornerRadius property
if (node === null || !("cornerRadius" in node) || node.cornerRadius === 0) {
return null;
}
const roundRadius = (value: number) => {
return Number(value.toFixed(3));
};
const cornerRadius = node.cornerRadius;
if (typeof cornerRadius === "number") {
return `${indentation}border-radius: ${roundRadius(cornerRadius)}px;`;
}
// Create type guard for corner properties
type NodeWithCorners = {
topLeftRadius?: number | symbol;
topRightRadius?: number | symbol;
bottomLeftRadius?: number | symbol;
bottomRightRadius?: number | symbol;
};
// Check if node has the corner properties
const hasCornerProperties = (
node: SceneNode,
): node is SceneNode & NodeWithCorners => {
return (
"topLeftRadius" in node ||
"topRightRadius" in node ||
"bottomLeftRadius" in node ||
"bottomRightRadius" in node
);
};
if (!hasCornerProperties(node)) {
return null;
}
const corners = [
{ prop: "topLeftRadius", slint: "border-top-left-radius" },
{ prop: "topRightRadius", slint: "border-top-right-radius" },
{ prop: "bottomLeftRadius", slint: "border-bottom-left-radius" },
{ prop: "bottomRightRadius", slint: "border-bottom-right-radius" },
] as const;
const validCorners = corners.filter((corner) => {
const value = node[corner.prop as keyof typeof node];
return typeof value === "number" && value > 0;
});
const radiusStrings = validCorners.map((corner) => {
const value = node[corner.prop as keyof typeof node] as number;
return `${indentation}${corner.slint}: ${roundRadius(value)}px;`;
});
return radiusStrings.length > 0 ? radiusStrings.join("\n") : null;
}
export async function getBorderWidthAndColor(
sceneNode: SceneNode,
useVariables: boolean,
): Promise<string[] | null> {
const properties: string[] = [];
if (
!("strokes" in sceneNode) ||
!Array.isArray(sceneNode.strokes) ||
sceneNode.strokes.length === 0
) {
return null;
}
const firstStroke = sceneNode.strokes[0];
// Border Width (check variable binding)
const boundWidthVarId = firstStroke.boundVariables?.strokeWeight?.id;
let borderWidthValue: string | null = null;
if (boundWidthVarId && useVariables) {
borderWidthValue = await getVariablePathString(boundWidthVarId);
}
// Fallback or if not bound
if (
!borderWidthValue &&
"strokeWeight" in sceneNode &&
typeof sceneNode.strokeWeight === "number"
) {
const width = roundNumber(sceneNode.strokeWeight);
if (width) {
borderWidthValue = `${width}px`;
}
}
if (borderWidthValue) {
properties.push(`${indentation}border-width: ${borderWidthValue};`);
}
// Border Color (check variable binding)
const boundColorVarId = firstStroke.boundVariables?.color?.id;
let borderColorValue: string | null = null;
if (boundColorVarId && useVariables) {
borderColorValue = await getVariablePathString(boundColorVarId);
}
// Fallback or if not bound
if (!borderColorValue) {
borderColorValue = getBrush(firstStroke); // Use existing function for resolved color
}
if (borderColorValue) {
properties.push(`${indentation}border-color: ${borderColorValue};`);
}
return properties.length > 0 ? properties : null;
}
export function getBrush(fill: {
type: string;
opacity?: number; // Allow opacity to be optional
color?: { r: number; g: number; b: number };
gradientStops?: ReadonlyArray<{
color: { r: number; g: number; b: number; a: number };
position: number;
}>;
gradientTransform?: number[][];
}): string | null {
const opacity = fill.opacity ?? 1; // Default to 1 if opacity is undefined
switch (fill.type) {
case "SOLID": {
if (!fill.color) {
console.warn("Missing fill colors for solid color value");
return "";
}
return rgbToHex({ ...fill.color, a: opacity });
}
case "GRADIENT_LINEAR": {
if (!fill.gradientStops || !fill.gradientTransform) {
console.warn("Missing gradient stops for linear gradient");
return "";
}
return generateLinearGradient({
opacity: opacity,
gradientStops: fill.gradientStops,
gradientTransform: fill.gradientTransform,
});
}
case "GRADIENT_RADIAL": {
if (!fill.gradientStops || !fill.gradientTransform) {
return "";
}
return generateRadialGradient({
opacity: opacity,
gradientStops: fill.gradientStops,
gradientTransform: fill.gradientTransform,
});
}
default: {
console.warn("Unknown fill type:", fill.type);
return null;
}
}
}
async function getVariablePathString(
variableId: string,
): Promise<string | null> {
const variable = await figma.variables.getVariableByIdAsync(variableId);
if (variable) {
const collection = await figma.variables.getVariableCollectionByIdAsync(
variable.variableCollectionId,
);
if (collection) {
const globalName = formatStructName(collection.name);
const pathParts = extractHierarchy(variable.name);
const slintPath = pathParts.map(sanitizePropertyName).join(".");
let resultPath = "";
if (collection.modes.length > 1) {
resultPath = `${globalName}.current.${slintPath}`;
} else {
resultPath = `${globalName}.${slintPath}`;
}
return resultPath;
}
console.warn(
`[getVariablePathString] Collection not found for variable ID: ${variableId}`,
);
}
return null;
}
export async function generateSlintSnippet(
sceneNode: SceneNode,
useVariables: boolean,
): Promise<string> {
const nodeType = sceneNode.type;
switch (nodeType) {
case "FRAME":
return await generateRectangleSnippet(sceneNode, useVariables);
case "RECTANGLE":
case "GROUP":
return await generateRectangleSnippet(sceneNode, useVariables);
case "COMPONENT":
case "INSTANCE":
return await generateRectangleSnippet(sceneNode, useVariables);
case "TEXT":
return await generateTextSnippet(sceneNode, useVariables);
case "VECTOR":
return await generatePathNodeSnippet(sceneNode, useVariables);
default:
return generateUnsupportedNodeSnippet(sceneNode);
}
}
export function generateUnsupportedNodeSnippet(sceneNode: SceneNode): string {
const properties: string[] = [];
const nodeType = sceneNode.type;
unsupportedNodeProperties.forEach((property) => {
switch (property) {
case "x":
if ("x" in sceneNode && typeof sceneNode.x === "number") {
const x = roundNumber(sceneNode.x);
if (x) {
properties.push(`${indentation}x: ${x}px;`);
}
}
break;
case "y":
if ("y" in sceneNode && typeof sceneNode.y === "number") {
const y = roundNumber(sceneNode.y);
if (y) {
properties.push(`${indentation}y: ${y}px;`);
}
}
break;
case "width":
if (
"width" in sceneNode &&
typeof sceneNode.width === "number"
) {
const width = roundNumber(sceneNode.width);
if (width) {
properties.push(`${indentation}width: ${width}px;`);
}
}
break;
case "height":
if (
"height" in sceneNode &&
typeof sceneNode.height === "number"
) {
const height = roundNumber(sceneNode.height);
if (height) {
properties.push(`${indentation}height: ${height}px;`);
}
}
break;
case "opacity":
if (
"opacity" in sceneNode &&
typeof sceneNode.opacity === "number"
) {
const opacity = sceneNode.opacity;
if (opacity !== 1) {
properties.push(
`${indentation}opacity: ${Math.round(opacity * 100)}%;`,
);
}
}
break;
}
});
return `//Unsupported type: ${nodeType}\nRectangle {\n${properties.join("\n")}\n}`;
}
export async function generateRectangleSnippet(
sceneNode: SceneNode,
useVariables: boolean,
): Promise<string> {
const properties: string[] = [];
const nodeId = sanitizePropertyName(sceneNode.name);
for (const property of rectangleProperties) {
try {
switch (property) {
case "x":
const boundXVarId = (sceneNode as any).boundVariables?.x
?.id;
let xValue: string | null = null;
if (boundXVarId && useVariables) {
xValue = await getVariablePathString(boundXVarId);
}
// use number value
if (
!xValue &&
"x" in sceneNode &&
typeof sceneNode.x === "number"
) {
const x = sceneNode.x; // Get raw value
if (x === 0) {
// Explicitly handle 0
xValue = "0px";
} else {
const roundedX = roundNumber(x); // Use roundNumber for non-zero
if (roundedX !== null) {
xValue = `${roundedX}px`;
}
}
}
if (xValue && sceneNode.parent?.type !== "PAGE") {
properties.push(`${indentation}x: ${xValue};`);
}
break;
case "y":
const boundYVarId = (sceneNode as any).boundVariables?.y
?.id;
let yValue: string | null = null;
if (boundYVarId && useVariables) {
yValue = await getVariablePathString(boundYVarId);
}
// use number value
if (
!yValue &&
"y" in sceneNode &&
typeof sceneNode.y === "number"
) {
const y = sceneNode.y; // Get raw value
if (y === 0) {
// Explicitly handle 0
yValue = "0px";
} else {
const roundedY = roundNumber(y); // Use roundNumber for non-zero
if (roundedY !== null) {
yValue = `${roundedY}px`;
}
}
}
// --- End modification ---
if (yValue && sceneNode.parent?.type !== "PAGE") {
// Keep parent check
properties.push(`${indentation}y: ${yValue};`);
}
break;
case "width":
const boundWidthVarId = (sceneNode as any).boundVariables
?.width?.id;
let widthValue: string | null = null;
if (boundWidthVarId && useVariables) {
widthValue =
await getVariablePathString(boundWidthVarId);
}
if (!widthValue && "width" in sceneNode) {
const normalizedWidth = roundNumber(sceneNode.width);
if (normalizedWidth) {
widthValue = `${normalizedWidth}px`;
}
}
if (widthValue) {
properties.push(`${indentation}width: ${widthValue};`);
}
break;
case "height":
const boundHeightVarId = (sceneNode as any).boundVariables
?.height?.id;
let heightValue: string | null = null;
if (boundHeightVarId && useVariables) {
heightValue =
await getVariablePathString(boundHeightVarId);
}
if (!heightValue && "height" in sceneNode) {
const normalizedHeight = roundNumber(sceneNode.height);
if (normalizedHeight) {
heightValue = `${normalizedHeight}px`;
}
}
if (heightValue) {
properties.push(
`${indentation}height: ${heightValue};`,
);
}
break;
case "fill":
if (
"fills" in sceneNode &&
Array.isArray(sceneNode.fills) &&
sceneNode.fills.length > 0
) {
const firstFill = sceneNode.fills[0];
if (firstFill.type === "SOLID") {
const boundVarId =
firstFill.boundVariables?.color?.id;
let fillValue: string | null = null;
if (boundVarId && useVariables) {
fillValue =
await getVariablePathString(boundVarId);
}
if (!fillValue) {
fillValue = getBrush(firstFill);
}
if (fillValue) {
properties.push(
`${indentation}background: ${fillValue};`,
);
}
} else {
const brush = getBrush(firstFill);
if (brush) {
properties.push(
`${indentation}background: ${brush};`,
);
}
}
}
break;
case "opacity":
if ("opacity" in sceneNode && sceneNode.opacity !== 1) {
properties.push(
`${indentation}opacity: ${Math.round(sceneNode.opacity * 100)}%;`,
);
}
break;
case "border-radius":
const borderRadiusProp = await getBorderRadius(
sceneNode,
useVariables,
);
if (borderRadiusProp !== null) {
properties.push(borderRadiusProp);
}
break;
case "border-width":
break;
case "border-color":
const borderWidthAndColor = await getBorderWidthAndColor(
sceneNode,
useVariables,
);
if (borderWidthAndColor !== null) {
properties.push(...borderWidthAndColor);
}
break;
}
} catch (err) {
console.error(
`[generateRectangleSnippet] Error processing property "${property}":`,
err,
);
properties.push(
`${indentation}// Error processing ${property}: ${err instanceof Error ? err.message : err}`,
);
}
}
return `${nodeId} := Rectangle {\n${properties.join("\n")}\n}`;
}
export async function generatePathNodeSnippet(
sceneNode: SceneNode,
useVariables: boolean,
): Promise<string> {
const properties: string[] = [];
const nodeId = sanitizePropertyName(sceneNode.name);
for (const property of pathProperties) {
try {
switch (property) {
case "x":
const boundPathXVarId = (sceneNode as any).boundVariables?.x
?.id;
let xPathValue: string | null = null;
if (boundPathXVarId && useVariables) {
xPathValue =
await getVariablePathString(boundPathXVarId);
}
if (
!xPathValue &&
"x" in sceneNode &&
typeof sceneNode.x === "number"
) {
const x = sceneNode.x;
if (x === 0) {
xPathValue = "0px";
} else {
const roundedX = roundNumber(x);
if (roundedX !== null) {
xPathValue = `${roundedX}px`;
}
}
}
if (xPathValue && sceneNode.parent?.type !== "PAGE") {
properties.push(`${indentation}x: ${xPathValue};`);
}
break;
case "y":
const boundPathYVarId = (sceneNode as any).boundVariables?.y
?.id;
let yPathValue: string | null = null;
if (boundPathYVarId && useVariables) {
yPathValue =
await getVariablePathString(boundPathYVarId);
}
if (
!yPathValue &&
"y" in sceneNode &&
typeof sceneNode.y === "number"
) {
const y = sceneNode.y;
if (y === 0) {
yPathValue = "0px";
} else {
const roundedY = roundNumber(y);
if (roundedY !== null) {
yPathValue = `${roundedY}px`;
}
}
}
if (yPathValue && sceneNode.parent?.type !== "PAGE") {
properties.push(`${indentation}y: ${yPathValue};`);
}
break;
case "width":
const boundPathWidthVarId = (sceneNode as any)
.boundVariables?.width?.id;
let widthPathValue: string | null = null;
if (boundPathWidthVarId && useVariables) {
widthPathValue =
await getVariablePathString(boundPathWidthVarId);
}
if (
!widthPathValue &&
"width" in sceneNode &&
typeof sceneNode.width === "number"
) {
const w = sceneNode.width;
if (w === 0) {
widthPathValue = "0px";
} else {
const roundedW = roundNumber(w);
if (roundedW !== null) {
widthPathValue = `${roundedW}px`;
}
}
}
if (widthPathValue) {
properties.push(
`${indentation}width: ${widthPathValue};`,
);
}
break;
case "height":
const boundPathHeightVarId = (sceneNode as any)
.boundVariables?.height?.id;
let heightPathValue: string | null = null;
if (boundPathHeightVarId && useVariables) {
heightPathValue =
await getVariablePathString(boundPathHeightVarId);
}
if (
!heightPathValue &&
"height" in sceneNode &&
typeof sceneNode.height === "number"
) {
const h = sceneNode.height;
if (h === 0) {
heightPathValue = "0px";
} else {
const roundedH = roundNumber(h);
if (roundedH !== null) {
heightPathValue = `${roundedH}px`;
}
}
}
if (heightPathValue) {
properties.push(
`${indentation}height: ${heightPathValue};`,
);
}
break;
case "commands":
if (sceneNode.type === "VECTOR") {
try {
const svgString = await sceneNode.exportAsync({
format: "SVG_STRING",
});
const match = svgString.match(
/<path[^>]*d=(["'])(.*?)\1/,
);
if (match && match[2]) {
const pathCommands = match[2];
properties.push(
`${indentation}commands: "${pathCommands}";`,
);
} else {
console.warn(
"[generatePathNodeSnippet] Could not extract path commands from SVG for node:",
sceneNode.id,
);
properties.push(
`${indentation}// Could not extract path commands from SVG`,
);
}
} catch (e) {
console.error(
"[generatePathNodeSnippet] Error exporting SVG for node:",
sceneNode.id,
e,
);
properties.push(
`${indentation}// Error exporting SVG: ${e instanceof Error ? e.message : e}`,
);
}
}
break;
case "fill":
if (
"fills" in sceneNode &&
Array.isArray(sceneNode.fills) &&
sceneNode.fills.length > 0 &&
sceneNode.fills[0].visible !== false
) {
const firstFill = sceneNode.fills[0] as Paint;
if (firstFill.type === "SOLID") {
const boundVarId = (firstFill as any).boundVariables
?.color?.id;
let fillValue: string | null = null;
if (boundVarId && useVariables) {
fillValue =
await getVariablePathString(boundVarId);
}
if (!fillValue) {
fillValue = getBrush(firstFill);
}
if (fillValue) {
properties.push(
`${indentation}fill: ${fillValue};`,
);
}
} else {
const brush = getBrush(firstFill);
if (brush) {
properties.push(
`${indentation}fill: ${brush};`,
);
}
}
}
break;
case "stroke":
if (
"strokes" in sceneNode &&
Array.isArray(sceneNode.strokes) &&
sceneNode.strokes.length > 0 &&
sceneNode.strokes[0].visible !== false
) {
const firstStroke = sceneNode.strokes[0] as Paint;
const boundColorVarId = (firstStroke as any)
.boundVariables?.color?.id;
let strokeValue: string | null = null;
if (boundColorVarId && useVariables) {
strokeValue =
await getVariablePathString(boundColorVarId);
}
if (!strokeValue) {
strokeValue = getBrush(firstStroke);
}
if (strokeValue) {
properties.push(
`${indentation}stroke: ${strokeValue};`,
);
}
}
break;
case "stroke-width":
const boundSWVarId = (sceneNode as any).boundVariables
?.strokeWeight?.id;
let strokeWValue: string | null = null;
if (boundSWVarId && useVariables) {
strokeWValue =
await getVariablePathString(boundSWVarId);
}
if (
!strokeWValue &&
"strokeWeight" in sceneNode &&
typeof sceneNode.strokeWeight === "number"
) {
const sw = sceneNode.strokeWeight;
if (sw === 0) {
strokeWValue = "0px";
} else {
const roundedSw = roundNumber(sw);
if (roundedSw !== null) {
strokeWValue = `${roundedSw}px`;
}
}
}
if (strokeWValue) {
if (strokeWValue !== "0px") {
if (
"strokes" in sceneNode &&
Array.isArray(sceneNode.strokes) &&
sceneNode.strokes.some(
(s) => s.visible !== false,
)
) {
properties.push(
`${indentation}stroke-width: ${strokeWValue};`,
);
}
} else {
if (
"strokes" in sceneNode &&
Array.isArray(sceneNode.strokes) &&
sceneNode.strokes.some(
(s) => s.visible !== false,
)
) {
properties.push(
`${indentation}stroke-width: ${strokeWValue};`,
);
}
}
}
break;
}
} catch (err) {
console.error(
`[generatePathNodeSnippet] Error processing property "${property}":`,
err,
);
properties.push(
`${indentation}// Error processing ${property}: ${err instanceof Error ? err.message : err}`,
);
}
}
return `${nodeId} := Path {\n${properties.join("\n")}\n}`;
}
export async function generateTextSnippet(
sceneNode: SceneNode,
useVariables: boolean,
): Promise<string> {
const properties: string[] = [];
const nodeId = sanitizePropertyName(sceneNode.name);
for (const property of textProperties) {
try {
switch (property) {
case "x":
const boundXVarId = (sceneNode as any).boundVariables?.x
?.id; // Assume direct object binding
let xValue: string | null = null;
if (boundXVarId && useVariables) {
xValue = await getVariablePathString(boundXVarId);
}
if (
!xValue &&
"x" in sceneNode &&
typeof sceneNode.x === "number"
) {
const x = roundNumber(sceneNode.x);
if (x !== null) {
// roundNumber returns null for 0
xValue = `${x}px`;
}
}
if (xValue) {
properties.push(`${indentation}x: ${xValue};`);
}
break;
case "y":
const boundYVarId = (sceneNode as any).boundVariables?.y
?.id;
let yValue: string | null = null;
if (boundYVarId && useVariables) {
yValue = await getVariablePathString(boundYVarId);
}
if (
!yValue &&
"y" in sceneNode &&
typeof sceneNode.y === "number"
) {
const y = roundNumber(sceneNode.y);
if (y !== null) {
// roundNumber returns null for 0
yValue = `${y}px`;
}
}
if (yValue) {
properties.push(`${indentation}y: ${yValue};`);
}
break;
case "text":
// Assuming 'characters' binding is also an array if it exists
const boundCharsVarId = (sceneNode as any).boundVariables
?.characters?.[0]?.id;
let textValue: string | null = null;
if (boundCharsVarId && useVariables) {
textValue =
await getVariablePathString(boundCharsVarId);
}
if (!textValue && "characters" in sceneNode) {
textValue = `"${sceneNode.characters}"`;
}
if (textValue) {
properties.push(`${indentation}text: ${textValue};`);
}
break;
case "fill":
if (
"fills" in sceneNode &&
Array.isArray(sceneNode.fills) &&
sceneNode.fills.length > 0
) {
const firstFill = sceneNode.fills[0];
if (firstFill.type === "SOLID") {
// Access ID via array index [0]
const boundVarId = (sceneNode as any).boundVariables
?.fills?.[0]?.id;
let fillValue: string | null = null;
if (boundVarId && useVariables) {
fillValue =
await getVariablePathString(boundVarId);
}
if (!fillValue) {
fillValue = getBrush(firstFill);
}
if (fillValue) {
properties.push(
`${indentation}color: ${fillValue};`,
);
}
} else {
const brush = getBrush(firstFill);
if (brush) {
properties.push(
`${indentation}color: ${brush};`,
);
}
}
}
break;
case "font-family":
// Keep using resolved family name. Variable structure for FontName is complex.
if ("fontName" in sceneNode) {
const fontName = sceneNode.fontName;
if (typeof fontName !== "symbol" && fontName) {
properties.push(
`${indentation}font-family: "${fontName.family}";`,
);
}
}
break;
case "font-size":
// Access ID via array index [0]
const boundSizeVarId = (sceneNode as any).boundVariables
?.fontSize?.[0]?.id;
let sizeValue: string | null = null;
if (boundSizeVarId && useVariables) {
sizeValue = await getVariablePathString(boundSizeVarId);
}
if (
!sizeValue &&
"fontSize" in sceneNode &&
typeof sceneNode.fontSize === "number"
) {
const fontSize = roundNumber(sceneNode.fontSize);
if (fontSize) {
sizeValue = `${fontSize}px`;
}
}
if (sizeValue) {
properties.push(
`${indentation}font-size: ${sizeValue};`,
);
}
break;
case "font-weight":
const boundWeightVarId = (sceneNode as any).boundVariables
?.fontWeight?.[0]?.id; // Still use [0] based on Text node structure
let weightValue: string | number | null = null;
let isVariable = false; // Flag to track if value is from a variable
if (boundWeightVarId && useVariables) {
const path =
await getVariablePathString(boundWeightVarId);
if (path) {
weightValue = path;
isVariable = true;
}
}
// Fallback if not bound or variable path failed
if (
weightValue === null &&
"fontWeight" in sceneNode &&
typeof sceneNode.fontWeight === "number"
) {
weightValue = sceneNode.fontWeight;
}
if (weightValue !== null) {
// Append '/ 1px' if it's a variable path (string)
const finalWeightValue = isVariable
? `${weightValue} / 1px`
: weightValue;
properties.push(
`${indentation}font-weight: ${finalWeightValue};`,
);
}
break;
case "horizontal-alignment":
if (
"textAlignHorizontal" in sceneNode &&
typeof sceneNode.textAlignHorizontal === "string"
) {
let slintValue: string | null = null;
let comment = "";
switch (sceneNode.textAlignHorizontal) {
case "LEFT":
slintValue = "left";
break;
case "CENTER":
slintValue = "center";
break;
case "RIGHT":
slintValue = "right";
break;
case "JUSTIFIED":
slintValue = "left";
comment =
"// Note: The value was justified in Figma, but this isn't supported right now";
break;
}
if (slintValue) {
properties.push(
`${indentation}horizontal-alignment: ${slintValue}; ${comment}`,
);
}
}
break;
}
} catch (err) {
console.error(
`[generateTextSnippet] Error processing property "${property}":`,
err,
);
properties.push(
`${indentation}// Error processing ${property}: ${err instanceof Error ? err.message : err}`,
);
}
}
return `${nodeId} := Text {\n${properties.join("\n")}\n}`;
}