component-inference.ts•10.8 kB
import type {
FlatElement,
ComponentContext,
ComponentPattern,
ChildPattern,
PseudoCodeOptions,
} from "../types/generator.js";
/**
* Pre-defined component patterns for common UI element types
* These patterns help identify and properly name components based on common design patterns
* Can be extended with additional patterns for specific design systems
*/
export const COMPONENT_PATTERNS: ComponentPattern[] = [
// Modal/Dialog patterns
{
rootKeywords: [
"modal",
"dialog",
"popup",
"overlay",
"data grid",
"datagrid",
],
childPatterns: [
{ keywords: ["header", "top"], componentSuffix: "Header", priority: 10 },
{
keywords: ["footer", "bottom", "actions"],
componentSuffix: "Footer",
priority: 10,
},
{
keywords: ["body", "content", "main"],
componentSuffix: "Body",
priority: 8,
},
{ keywords: ["title", "heading"], componentSuffix: "Title", priority: 9 },
{
keywords: ["close", "dismiss"],
componentSuffix: "CloseButton",
priority: 7,
},
],
},
// Card patterns
{
rootKeywords: ["card", "panel", "tile"],
childPatterns: [
{ keywords: ["header", "top"], componentSuffix: "Header", priority: 10 },
{
keywords: ["footer", "bottom"],
componentSuffix: "Footer",
priority: 10,
},
{
keywords: ["body", "content", "main"],
componentSuffix: "Body",
priority: 8,
},
{ keywords: ["title", "heading"], componentSuffix: "Title", priority: 9 },
{ keywords: ["image", "media"], componentSuffix: "Media", priority: 7 },
{
keywords: ["actions", "buttons"],
componentSuffix: "Actions",
priority: 6,
},
],
},
// Form patterns
{
rootKeywords: ["form", "formgroup", "fieldset"],
childPatterns: [
{
keywords: ["field", "input", "control"],
componentSuffix: "Field",
priority: 10,
},
{ keywords: ["label"], componentSuffix: "Label", priority: 9 },
{
keywords: ["error", "validation"],
componentSuffix: "Error",
priority: 8,
},
{ keywords: ["help", "hint"], componentSuffix: "Help", priority: 7 },
{
keywords: ["actions", "buttons"],
componentSuffix: "Actions",
priority: 6,
},
],
},
// Table patterns
{
rootKeywords: ["table", "datagrid", "datatable"],
childPatterns: [
{
keywords: ["header", "thead"],
componentSuffix: "Header",
priority: 10,
},
{ keywords: ["body", "tbody"], componentSuffix: "Body", priority: 10 },
{
keywords: ["footer", "tfoot"],
componentSuffix: "Footer",
priority: 10,
},
{ keywords: ["row", "tr"], componentSuffix: "Row", priority: 8 },
{ keywords: ["cell", "td", "th"], componentSuffix: "Cell", priority: 7 },
],
},
// Navigation patterns
{
rootKeywords: ["nav", "navigation", "menu", "navbar"],
childPatterns: [
{ keywords: ["item", "link"], componentSuffix: "Item", priority: 10 },
{ keywords: ["brand", "logo"], componentSuffix: "Brand", priority: 9 },
{
keywords: ["toggle", "hamburger"],
componentSuffix: "Toggle",
priority: 8,
},
{ keywords: ["dropdown"], componentSuffix: "Dropdown", priority: 7 },
],
},
// Layout patterns (top-level only)
{
rootKeywords: ["header", "footer", "sidebar", "main", "aside"],
childPatterns: [],
conditions: { level: [0, 1] },
},
// Section patterns
{
rootKeywords: ["section", "region", "area"],
childPatterns: [
{
keywords: ["header", "title"],
componentSuffix: "Header",
priority: 10,
},
{
keywords: ["content", "body"],
componentSuffix: "Content",
priority: 8,
},
{ keywords: ["footer"], componentSuffix: "Footer", priority: 9 },
],
},
// List patterns
{
rootKeywords: ["list", "menu", "collection"],
childPatterns: [
{ keywords: ["item", "entry"], componentSuffix: "Item", priority: 10 },
{ keywords: ["header"], componentSuffix: "Header", priority: 9 },
{ keywords: ["footer"], componentSuffix: "Footer", priority: 8 },
],
},
];
/**
* Main entry point for semantic component inference
* Tries to determine the most appropriate component type based on context and naming patterns.
*/
export const inferSemanticComponent = ({
element,
context,
options,
}: {
element: FlatElement;
context: ComponentContext;
options?: PseudoCodeOptions;
}): string | null => {
const name = element.name.toLowerCase();
const parentName = context.parentName?.toLowerCase() || "";
const patterns = options?.customPatterns
? [...options.customPatterns, ...COMPONENT_PATTERNS]
: COMPONENT_PATTERNS;
const childComponent = findChildComponent({
name,
parentName,
context,
patterns,
});
if (childComponent) {
return childComponent;
}
const rootComponent = findRootComponent({ name, context, patterns });
if (rootComponent) {
return rootComponent;
}
const heuristicComponent = inferBySimpleHeuristics({ name, context });
return heuristicComponent;
};
/**
* Tries to identify a child component based on parent context and component patterns
* Used to determine if an element should be rendered as a specialized child component.
*/
export const findChildComponent = ({
name,
parentName,
context,
patterns,
}: {
name: string;
parentName: string;
context: ComponentContext;
patterns: ComponentPattern[];
}): string | null => {
if (!parentName) return null;
for (const pattern of patterns) {
const parentKeyword = pattern.rootKeywords.find((keyword) =>
parentName.includes(keyword)
);
if (!parentKeyword) continue;
let bestMatch: { pattern: ChildPattern; keyword: string } | null = null;
let highestPriority = 0;
for (const childPattern of pattern.childPatterns) {
const matchedKeyword = childPattern.keywords.find((keyword) =>
name.includes(keyword)
);
if (matchedKeyword && childPattern.priority > highestPriority) {
bestMatch = { pattern: childPattern, keyword: matchedKeyword };
highestPriority = childPattern.priority;
}
}
if (bestMatch) {
const parentComponent = capitalizeComponentName(parentKeyword);
return `${parentComponent}${bestMatch.pattern.componentSuffix}`;
}
}
return null;
};
/**
* Attempts to identify a top-level/root component based on naming patterns
* Used to determine the main component type for an element.
*/
export const findRootComponent = ({
name,
context,
patterns,
}: {
name: string;
context: ComponentContext;
patterns: ComponentPattern[];
}): string | null => {
for (const pattern of patterns) {
const matchedKeyword = pattern.rootKeywords.find((keyword) =>
name.includes(keyword)
);
if (!matchedKeyword) continue;
if (
pattern.conditions?.level &&
!pattern.conditions.level.includes(context.level)
) {
continue;
}
if (context.parentName) {
const parentName = context.parentName.toLowerCase();
const hasParentWithSameKeyword = pattern.rootKeywords.some((keyword) =>
parentName.includes(keyword)
);
if (hasParentWithSameKeyword) {
continue;
}
}
return capitalizeComponentName(matchedKeyword);
}
return null;
};
/**
* Applies simple heuristics to identify common components
* This is a fallback mechanism when the pattern matching doesn't yield results
* Uses common naming conventions to identify basic component types.
*/
export const inferBySimpleHeuristics = ({
name,
context,
}: {
name: string;
context: ComponentContext;
}): string | null => {
const parentName = context.parentName?.toLowerCase() || "";
if (name.includes("container") && context.level <= 1) return "Container";
if (name.includes("wrapper") && context.level <= 2) return "Wrapper";
if (name.includes("label")) return "Label";
if (name.includes("text") && !name.includes("input")) return "Text";
if (name.includes("header") && context.level === 0) return "Header";
if (name.includes("footer") && context.level === 0) return "Footer";
if (name.includes("divider")) return "Divider";
if (name.includes("separator")) return "Separator";
return null;
};
/**
* Capitalizes and formats component names following React conventions
* Handles special cases and ensures proper naming format.
*/
export function capitalizeComponentName(keyword: string): string {
console.log("Capitalize component name:", keyword);
return keyword
.trim()
.split(/\s+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join("");
}
/**
* Infers the appropriate text component type based on properties
* Analyzes font size, weight, and name to determine if text should be heading, paragraph, etc.
*/
export const inferTextComponent = (element: FlatElement): string => {
const name = element.name.toLowerCase();
const fontSize =
typeof element.text?.fontSize === "number"
? element.text.fontSize
: parseFloat(element.text?.fontSize?.toString() || "16") || 16;
const fontWeight = element.text?.fontWeight || 400;
if (name.includes("title") || name.includes("heading")) {
if (fontSize >= 24 || fontWeight >= 600) return "Title";
return "Subtitle";
}
if (name.includes("subtitle")) return "Subtitle";
if (name.includes("caption")) return "Caption";
if (name.includes("label")) return "Label";
if (fontSize >= 24 && fontWeight >= 600) return "Title";
if (fontSize >= 18 && fontWeight >= 500) return "Subtitle";
if (fontSize <= 12) return "Caption";
return "Text";
};
/**
* Cleans and formats Figma component names for React usage
* Handles various Figma naming patterns and converts them to proper React component names.
*/
export const cleanComponentName = (figmaName: string): string => {
const specialWordsRemoval: string[] = JSON.parse(
process.env.SKIP_WORDS_IN_COMPONENTS || "[]"
);
if (specialWordsRemoval.some((word) => figmaName.includes(word))) {
figmaName =
figmaName
.split(new RegExp(specialWordsRemoval?.join("|"), "i"))?.[1]
?.trim?.() || "";
}
if (figmaName.includes(":")) {
figmaName = (figmaName.split(":")[0] || "").trim();
}
if (figmaName.includes("|")) {
figmaName = (figmaName.split("|")[0] || "").trim();
}
return figmaName
.split(/\s+/)
.filter((word) => word.length > 0)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join("");
};