zp-figma-converter
Version:
Convert Figma designs to various code formats
307 lines • 12.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FigmaNodeParser = void 0;
const types_1 = require("./types");
const path = __importStar(require("path"));
/**
* Parser for Figma nodes to an intermediate representation
*/
class FigmaNodeParser {
constructor(assetsDir) {
this.assetsDir = assetsDir;
}
/**
* Parse a Figma node into an intermediate representation
* @param node Figma node
* @returns Intermediate node
*/
parse(node) {
const parsedNode = this.parseWithParent(node, undefined);
return parsedNode;
}
/**
* Parse a Figma node with parent reference
* @param node Figma node
* @param parent Parent node (if any)
* @returns Intermediate node
*/
parseWithParent(node, parent) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
// Set parent reference
node.parent = parent;
const metadata = {};
// Parse layout properties
if (node.layoutMode) {
metadata.layout = {
mode: node.layoutMode,
paddingLeft: node.paddingLeft,
paddingRight: node.paddingRight,
paddingTop: node.paddingTop,
paddingBottom: node.paddingBottom,
itemSpacing: node.itemSpacing,
counterAxisSpacing: node.counterAxisSpacing,
layoutAlign: node.layoutAlign,
layoutGrow: node.layoutGrow,
primaryAxisSizingMode: node.primaryAxisSizingMode,
counterAxisSizingMode: node.counterAxisSizingMode,
};
// Add auto layout metadata for CSD converter
metadata.autoLayout = {
direction: node.layoutMode.toLowerCase() === 'horizontal' ? 'horizontal' : 'vertical',
spacing: node.itemSpacing || 0,
padding: {
left: node.paddingLeft || 0,
right: node.paddingRight || 0,
top: node.paddingTop || 0,
bottom: node.paddingBottom || 0
},
alignment: node.primaryAxisAlignItems || 'start',
counterAxisAlignment: node.counterAxisAlignItems || 'start'
};
}
// Parse constraints
if (node.constraints) {
metadata.constraints = {
horizontal: node.constraints.horizontal,
vertical: node.constraints.vertical,
// Store original parent dimensions if available
originalParentWidth: (_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.absoluteBoundingBox) === null || _b === void 0 ? void 0 : _b.width,
originalParentHeight: (_d = (_c = node.parent) === null || _c === void 0 ? void 0 : _c.absoluteBoundingBox) === null || _d === void 0 ? void 0 : _d.height
};
}
// Parse component reference
if (node.componentId) {
metadata.componentId = node.componentId;
}
// Create intermediate node
const intermediateNode = {
id: node.id,
name: node.name,
type: node.type,
x: (_f = (_e = node.absoluteBoundingBox) === null || _e === void 0 ? void 0 : _e.x) !== null && _f !== void 0 ? _f : 0,
y: (_h = (_g = node.absoluteBoundingBox) === null || _g === void 0 ? void 0 : _g.y) !== null && _h !== void 0 ? _h : 0,
width: (_k = (_j = node.absoluteBoundingBox) === null || _j === void 0 ? void 0 : _j.width) !== null && _k !== void 0 ? _k : 0,
height: (_m = (_l = node.absoluteBoundingBox) === null || _l === void 0 ? void 0 : _l.height) !== null && _m !== void 0 ? _m : 0,
rotation: node.rotation,
opacity: node.opacity,
children: [], // Will be filled with children later
styles: this.parseStyles(node),
imageRef: this.getImageReference(node),
metadata
};
// Parse children with parent reference
if ((_o = node.children) === null || _o === void 0 ? void 0 : _o.length) {
intermediateNode.children = node.children.map(child => this.parseWithParent(child, node));
}
return intermediateNode;
}
/**
* Parse styles from a Figma node
* @param node Figma node
* @returns Styles object
*/
parseStyles(node) {
var _a, _b, _c, _d;
const styles = {};
// Parse multiple fills
if ((_a = node.fills) === null || _a === void 0 ? void 0 : _a.length) {
const fillColors = node.fills.map(fill => {
if (fill.type === 'SOLID') {
return this.rgbToHex(fill.color, fill.opacity);
}
else if (fill.type === 'IMAGE') {
// Mark for image export
return `image:${node.id}`;
}
else if (fill.type === 'GRADIENT_LINEAR' || fill.type === 'GRADIENT_RADIAL') {
// Basic gradient support
return fill.type.toLowerCase();
}
return '';
}).filter(color => color !== '');
if (fillColors.length === 1) {
styles.fill = fillColors[0];
}
else if (fillColors.length > 1) {
styles.fill = fillColors[0]; // Use first fill, store others in metadata
styles.additionalFills = fillColors.slice(1);
}
}
// Parse multiple strokes
if ((_b = node.strokes) === null || _b === void 0 ? void 0 : _b.length) {
const strokeColors = node.strokes.map(stroke => {
if (stroke.type === 'SOLID') {
return this.rgbToHex(stroke.color, stroke.opacity);
}
return '';
}).filter(color => color !== '');
if (strokeColors.length === 1) {
styles.stroke = strokeColors[0];
}
else if (strokeColors.length > 1) {
styles.stroke = strokeColors[0]; // Use first stroke, store others in metadata
styles.additionalStrokes = strokeColors.slice(1);
}
// Add stroke weight if present
if (node.strokeWeight !== undefined) {
styles.strokeWeight = node.strokeWeight;
}
}
// Parse corner radius
if (node.cornerRadius !== undefined) {
styles.cornerRadius = node.cornerRadius;
}
else if (((_c = node.rectangleCornerRadii) === null || _c === void 0 ? void 0 : _c.length) === 4) {
styles.cornerRadii = node.rectangleCornerRadii;
}
// Parse effects (shadows, blurs, etc)
if ((_d = node.effects) === null || _d === void 0 ? void 0 : _d.length) {
styles.effects = this.parseEffects(node.effects);
}
// Parse text properties
if (node.type === types_1.FigmaNodeType.TEXT && node.style) {
styles.font = {
family: node.style.fontFamily,
size: node.style.fontSize,
weight: node.style.fontWeight.toString()
};
// Additional text properties
if (node.style.textCase)
styles.font.textCase = node.style.textCase;
if (node.style.textDecoration)
styles.font.textDecoration = node.style.textDecoration;
if (node.style.textAlignHorizontal)
styles.font.alignHorizontal = node.style.textAlignHorizontal;
if (node.style.textAlignVertical)
styles.font.alignVertical = node.style.textAlignVertical;
if (node.style.letterSpacing)
styles.font.letterSpacing = node.style.letterSpacing;
if (node.style.lineHeightPx)
styles.font.lineHeight = node.style.lineHeightPx;
styles.textContent = node.characters;
}
return styles;
}
/**
* Parse effects (shadows, blurs)
* @param effects Array of Figma effects
* @returns Parsed effects
*/
parseEffects(effects) {
return effects.map(effect => {
var _a;
const parsedEffect = {
type: effect.type,
visible: (_a = effect.visible) !== null && _a !== void 0 ? _a : true
};
if (effect.color) {
parsedEffect.color = this.rgbToHex(effect.color);
}
if (effect.radius !== undefined) {
parsedEffect.radius = effect.radius;
}
if (effect.offset) {
parsedEffect.offsetX = effect.offset.x;
parsedEffect.offsetY = effect.offset.y;
}
return parsedEffect;
});
}
/**
* Convert RGB color to hex or rgba
* @param color RGB color
* @param opacity Additional opacity
* @returns Hex color string or rgba string
*/
rgbToHex(color, opacity) {
if (!color)
return '#000000';
const r = Math.round(color.r * 255);
const g = Math.round(color.g * 255);
const b = Math.round(color.b * 255);
const a = opacity !== undefined ? opacity : (color.a !== undefined ? color.a : 1);
if (a < 1) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
/**
* Get image reference for a node (if applicable)
* @param node Figma node
* @returns Image reference path or undefined
*/
getImageReference(node) {
if (!this.hasImageContent(node)) {
return undefined;
}
const fileName = this.getFileName(node);
return path.join(this.assetsDir, `${fileName}.png`);
}
/**
* Check if node contains image content
* @param node Figma node
* @returns True if node has image content
*/
hasImageContent(node) {
if (!node.fills) {
return false;
}
const hasImageFill = node.fills.some(fill => fill.type === 'IMAGE' || fill.imageRef);
return (node.type === types_1.FigmaNodeType.RECTANGLE || node.type === types_1.FigmaNodeType.FRAME) && (hasImageFill ||
node.fills.some(fill => fill.type === 'SOLID'));
}
/**
* Get sanitized filename for node
* @param node Figma node
* @returns Sanitized filename
*/
getFileName(node) {
var _a, _b;
const imageFill = (_a = node.fills) === null || _a === void 0 ? void 0 : _a.find(fill => fill.type === 'IMAGE');
const rawFileName = (_b = imageFill === null || imageFill === void 0 ? void 0 : imageFill.imageRef) !== null && _b !== void 0 ? _b : node.id;
return this.sanitizeFileName(rawFileName);
}
/**
* Sanitize file name to remove invalid characters
* @param fileName Original file name
* @returns Sanitized file name
*/
sanitizeFileName(fileName) {
return fileName.replace(/[\\/:*?"<>|;,=+]/g, "_");
}
}
exports.FigmaNodeParser = FigmaNodeParser;
//# sourceMappingURL=figma-node-parser.js.map