@seed-design/figma 1.1.17 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sources":["../src/normalizer/types.ts","../src/normalizer/from-rest.ts","../src/normalizer/from-plugin.ts","../src/entities/component.interface.ts","../src/entities/icon.interface.ts","../src/entities/variable.interface.ts","../src/entities/variable.repository.ts","../src/entities/style.interface.ts","../src/entities/style.repository.ts","../src/entities/icon.repository.ts","../src/entities/icon.service.ts","../src/entities/style.service.ts","../src/entities/variable.service.ts","../src/entities/component.repository.ts","../src/entities/index.ts"],"sourcesContent":["import type * as FigmaRestSpec from \"@figma/rest-api-spec\";\n\nexport type NormalizedIsLayerTrait = Pick<FigmaRestSpec.IsLayerTrait, \"type\" | \"id\" | \"name\"> & {\n boundVariables?: Pick<\n NonNullable<FigmaRestSpec.IsLayerTrait[\"boundVariables\"]>,\n | \"fills\"\n | \"strokes\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n | \"bottomLeftRadius\"\n | \"bottomRightRadius\"\n | \"topLeftRadius\"\n | \"topRightRadius\"\n | \"paddingBottom\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"maxHeight\"\n | \"minHeight\"\n | \"maxWidth\"\n | \"minWidth\"\n | \"fontSize\"\n | \"fontWeight\"\n | \"lineHeight\"\n | \"size\"\n >;\n};\n\nexport type NormalizedCornerTrait = Pick<\n FigmaRestSpec.CornerTrait,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n>;\n\nexport type NormalizedHasChildrenTrait = {\n children: NormalizedSceneNode[];\n};\n\nexport type NormalizedHasLayoutTrait = Pick<\n FigmaRestSpec.HasLayoutTrait,\n | \"layoutAlign\"\n | \"layoutGrow\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n>;\n\nexport type NormalizedSolidPaint = FigmaRestSpec.SolidPaint;\n\nexport type NormalizedPaint =\n | NormalizedSolidPaint\n | FigmaRestSpec.GradientPaint\n | FigmaRestSpec.ImagePaint;\n\nexport type NormalizedHasGeometryTrait = Omit<\n Pick<FigmaRestSpec.HasGeometryTrait, \"fills\" | \"strokes\" | \"strokeWeight\">,\n \"fills\" | \"strokes\"\n> & {\n fills: NormalizedPaint[];\n strokes: NormalizedPaint[];\n fillStyleKey?: string;\n};\n\nexport type NormalizedShadow =\n | (Pick<\n FigmaRestSpec.DropShadowEffect,\n \"color\" | \"offset\" | \"radius\" | \"spread\" | \"boundVariables\"\n > &\n Required<Pick<FigmaRestSpec.DropShadowEffect, \"type\">>)\n | (Pick<\n FigmaRestSpec.InnerShadowEffect,\n \"color\" | \"offset\" | \"radius\" | \"spread\" | \"boundVariables\"\n > &\n Required<Pick<FigmaRestSpec.InnerShadowEffect, \"type\">>);\n\nexport type NormalizedHasEffectsTrait = Omit<FigmaRestSpec.HasEffectsTrait, \"effects\"> & {\n effects: NormalizedShadow[];\n effectStyleKey?: string;\n};\n\nexport type NormalizedHasFramePropertiesTrait = Pick<\n FigmaRestSpec.HasFramePropertiesTrait,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n>;\n\nexport interface NormalizedTextSegment {\n characters: string;\n start: number;\n end: number;\n style: {\n fontFamily?: string;\n fontWeight?: number;\n fontSize?: number;\n italic?: boolean;\n textDecoration?: string;\n letterSpacing?: number;\n /**\n * in pixels\n */\n lineHeight?: number;\n };\n}\n\nexport type NormalizedTypePropertiesTrait = Pick<\n FigmaRestSpec.TypePropertiesTrait,\n \"style\" | \"characters\"\n> & {\n segments: NormalizedTextSegment[];\n\n textStyleKey?: string;\n};\n\nexport type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasEffectsTrait;\n\nexport type NormalizedFrameTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasEffectsTrait &\n NormalizedHasChildrenTrait &\n NormalizedCornerTrait &\n NormalizedHasFramePropertiesTrait;\n\nexport interface NormalizedFrameNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.FrameNode[\"type\"];\n}\n\nexport interface NormalizedRectangleNode\n extends NormalizedDefaultShapeTrait,\n NormalizedCornerTrait {\n type: FigmaRestSpec.RectangleNode[\"type\"];\n}\n\nexport interface NormalizedTextNode\n extends NormalizedDefaultShapeTrait,\n NormalizedTypePropertiesTrait {\n type: FigmaRestSpec.TextNode[\"type\"];\n}\n\nexport interface NormalizedComponentNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.ComponentNode[\"type\"];\n}\n\nexport interface NormalizedInstanceNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.InstanceNode[\"type\"];\n\n componentProperties: {\n [key: string]: FigmaRestSpec.ComponentProperty & {\n componentKey?: string;\n componentSetKey?: string;\n };\n };\n\n componentKey: string;\n\n componentSetKey?: string;\n\n overrides?: FigmaRestSpec.InstanceNode[\"overrides\"];\n\n children: NormalizedSceneNode[];\n}\n\nexport interface NormalizedVectorNode extends NormalizedDefaultShapeTrait, NormalizedCornerTrait {\n type: FigmaRestSpec.VectorNode[\"type\"];\n}\n\nexport interface NormalizedBooleanOperationNode\n extends NormalizedIsLayerTrait,\n NormalizedHasChildrenTrait,\n NormalizedHasLayoutTrait,\n NormalizedHasGeometryTrait,\n NormalizedHasEffectsTrait {\n type: FigmaRestSpec.BooleanOperationNode[\"type\"];\n}\n\nexport interface NormalizedUnhandledNode {\n type: \"UNHANDLED\";\n id: string;\n original: FigmaRestSpec.Node | SceneNode;\n}\n\nexport type NormalizedSceneNode =\n | NormalizedFrameNode\n | NormalizedRectangleNode\n | NormalizedTextNode\n | NormalizedComponentNode\n | NormalizedInstanceNode\n | NormalizedVectorNode\n | NormalizedBooleanOperationNode\n | NormalizedUnhandledNode;\n","/**\n * from-rest could be run outside of the Figma Plugin environment\n * so we cannot use the Plugin API types directly e.g. getNodeByIdAsync\n */\n\n/**\n * NOTE: types of MinimalFillsTrait[\"styles\"] can be found here:\n * https://developers.figma.com/docs/rest-api/component-types/#style-type\n * Record<\"text\" | \"fill\" | \"stroke\" | \"effect\" | \"grid\", string>\n */\n\nimport type * as FigmaRestSpec from \"@figma/rest-api-spec\";\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedTextSegment,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n NormalizedShadow,\n NormalizedCornerTrait,\n NormalizedHasFramePropertiesTrait,\n NormalizedPaint,\n NormalizedDefaultShapeTrait,\n NormalizedHasEffectsTrait,\n NormalizedIsLayerTrait,\n} from \"./types\";\n\nexport interface RestNormalizerContext {\n /**\n * A map of style **ID** to style data\n */\n styles: Record<string, FigmaRestSpec.Style>;\n /**\n * A map of component **ID** to component data\n */\n components: Record<string, FigmaRestSpec.Component>;\n /**\n * A map of component set **ID** to component set data\n */\n componentSets: Record<string, FigmaRestSpec.ComponentSet>;\n}\n\nexport function createRestNormalizer(\n ctx: RestNormalizerContext,\n): (node: FigmaRestSpec.Node) => NormalizedSceneNode {\n function normalizeNodes(nodes: readonly FigmaRestSpec.Node[]): NormalizedSceneNode[] {\n // Figma REST API omits default values for some fields, \"visible\" is one of them\n return nodes.filter((node) => !(\"visible\" in node) || node.visible).map(normalizeNode);\n }\n\n function normalizeNode(node: FigmaRestSpec.Node): NormalizedSceneNode {\n switch (node.type) {\n case \"FRAME\":\n return normalizeFrameNode(node);\n case \"RECTANGLE\":\n return normalizeRectangleNode(node);\n case \"TEXT\":\n return normalizeTextNode(node);\n case \"COMPONENT\":\n return normalizeComponentNode(node);\n case \"INSTANCE\":\n return normalizeInstanceNode(node);\n case \"VECTOR\":\n return normalizeVectorNode(node);\n case \"BOOLEAN_OPERATION\":\n return normalizeBooleanOperationNode(node);\n case \"GROUP\":\n return normalizeGroupNodeAsFrameNode(node);\n default:\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n }\n\n function normalizeBoundVariables(\n boundVariables: FigmaRestSpec.IsLayerTrait[\"boundVariables\"] | undefined,\n ) {\n if (!boundVariables) return undefined;\n\n return {\n fills: boundVariables.fills,\n strokes: boundVariables.strokes,\n itemSpacing: boundVariables.itemSpacing,\n counterAxisSpacing: boundVariables.counterAxisSpacing,\n topLeftRadius: boundVariables.topLeftRadius,\n topRightRadius: boundVariables.topRightRadius,\n bottomLeftRadius: boundVariables.bottomLeftRadius,\n bottomRightRadius: boundVariables.bottomRightRadius,\n paddingTop: boundVariables.paddingTop,\n paddingRight: boundVariables.paddingRight,\n paddingBottom: boundVariables.paddingBottom,\n paddingLeft: boundVariables.paddingLeft,\n minWidth: boundVariables.minWidth,\n maxWidth: boundVariables.maxWidth,\n minHeight: boundVariables.minHeight,\n maxHeight: boundVariables.maxHeight,\n fontSize: boundVariables.fontSize,\n fontWeight: boundVariables.fontWeight,\n lineHeight: boundVariables.lineHeight,\n size: boundVariables.size,\n };\n }\n\n function normalizePaint(paint: FigmaRestSpec.Paint): NormalizedPaint {\n switch (paint.type) {\n case \"SOLID\":\n case \"IMAGE\":\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return paint;\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(paints: FigmaRestSpec.Paint[] | undefined): NormalizedPaint[] {\n if (!paints) return [];\n\n return paints.map(normalizePaint);\n }\n\n function normalizeRadiusProps({\n cornerRadius,\n rectangleCornerRadii,\n }: Pick<\n FigmaRestSpec.RectangleNode,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n >): NormalizedCornerTrait {\n return { cornerRadius, rectangleCornerRadii };\n }\n\n function normalizeEffectProps(\n node: Pick<FigmaRestSpec.FrameNode, \"effects\" | \"styles\">,\n ): NormalizedHasEffectsTrait {\n const effects = (node.effects ?? [])\n .filter(\n (effect): effect is FigmaRestSpec.DropShadowEffect | FigmaRestSpec.InnerShadowEffect =>\n effect.visible !== false &&\n (effect.type === \"DROP_SHADOW\" || effect.type === \"INNER_SHADOW\"),\n )\n .map((effect): NormalizedShadow => {\n const { type, color, offset, radius, spread, boundVariables } = effect;\n\n return {\n type,\n color,\n offset,\n radius,\n spread,\n boundVariables,\n };\n });\n\n return {\n effects,\n effectStyleKey: node.styles?.[\"effect\"] ? ctx.styles[node.styles[\"effect\"]]?.key : undefined,\n };\n }\n\n function normalizeShapeProps(\n node: Pick<\n FigmaRestSpec.FrameNode,\n | \"fills\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"styles\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n | \"effects\"\n >,\n ): Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait> {\n return {\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: normalizePaints(node.fills),\n fillStyleKey: node.styles?.[\"fill\"] ? ctx.styles[node.styles[\"fill\"]]?.key : undefined,\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...normalizeEffectProps(node),\n };\n }\n\n function normalizeAutolayoutProps(\n node: Pick<\n FigmaRestSpec.FrameNode,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n >,\n ): NormalizedHasFramePropertiesTrait {\n return {\n layoutMode: node.layoutMode,\n layoutWrap: node.layoutWrap,\n paddingLeft: node.paddingLeft,\n paddingRight: node.paddingRight,\n paddingTop: node.paddingTop,\n paddingBottom: node.paddingBottom,\n primaryAxisAlignItems: node.primaryAxisAlignItems,\n primaryAxisSizingMode: node.primaryAxisSizingMode,\n counterAxisAlignItems: node.counterAxisAlignItems,\n counterAxisSizingMode: node.counterAxisSizingMode,\n itemSpacing: node.itemSpacing,\n counterAxisSpacing: node.counterAxisSpacing,\n };\n }\n\n function normalizeFrameNode(node: FigmaRestSpec.FrameNode): NormalizedFrameNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait, NormalizedHasFramePropertiesTrait\n ...normalizeShapeProps(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeRectangleNode(node: FigmaRestSpec.RectangleNode): NormalizedRectangleNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeTextNode(node: FigmaRestSpec.TextNode): NormalizedTextNode {\n // Convert TypeStyle to NormalizedTextSegment.style format\n function normalizeSegmentStyle(\n typeStyle: FigmaRestSpec.TypeStyle,\n ): NormalizedTextSegment[\"style\"] {\n return {\n fontFamily: typeStyle.fontFamily,\n fontWeight: typeStyle.fontWeight,\n fontSize: typeStyle.fontSize,\n italic: typeStyle.italic,\n textDecoration: typeStyle.textDecoration,\n letterSpacing: typeStyle.letterSpacing,\n lineHeight: typeStyle.lineHeightPx,\n };\n }\n\n // Function to segment a text node based on style overrides\n function segmentTextNode(textNode: FigmaRestSpec.TextNode): NormalizedTextSegment[] {\n const segments: NormalizedTextSegment[] = [];\n const characters = textNode.characters;\n const styleOverrides = textNode.characterStyleOverrides || [];\n const styleTable = textNode.styleOverrideTable || {};\n\n // If no style overrides, return the entire text as one segment\n if (!styleOverrides.length) {\n return [\n {\n characters: characters,\n start: 0,\n end: characters.length,\n style: normalizeSegmentStyle(textNode.style),\n },\n ];\n }\n\n let currentSegment: NormalizedTextSegment = {\n characters: \"\",\n start: 0,\n end: 0,\n style: {},\n };\n\n let currentStyleId: string | null = null;\n\n for (let i = 0; i < characters.length; i++) {\n const styleId = styleOverrides[i]?.toString() || null;\n\n // If style changes or it's the first character\n if (styleId !== currentStyleId || i === 0) {\n // Add the previous segment if it exists\n if (i > 0) {\n currentSegment.end = i;\n currentSegment.characters = characters.substring(\n currentSegment.start,\n currentSegment.end,\n );\n segments.push({ ...currentSegment });\n }\n\n // Start a new segment\n currentStyleId = styleId;\n currentSegment = {\n characters: \"\",\n start: i,\n end: 0,\n style: styleId ? normalizeSegmentStyle(styleTable[styleId]) : {},\n };\n }\n }\n\n // Add the last segment\n if (currentSegment.start < characters.length) {\n currentSegment.end = characters.length;\n currentSegment.characters = characters.substring(currentSegment.start, currentSegment.end);\n segments.push(currentSegment);\n }\n\n return segments;\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedTypePropertiesTrait\n style: node.style, // this style is the style of the first segment\n characters: node.characters,\n textStyleKey: node.styles?.[\"text\"] ? ctx.styles[node.styles[\"text\"]]?.key : undefined,\n segments: segmentTextNode(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeComponentNode(node: FigmaRestSpec.ComponentNode): NormalizedComponentNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n\n // NormalizedHasCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeInstanceNode(node: FigmaRestSpec.InstanceNode): NormalizedInstanceNode {\n const mainComponent = ctx.components[node.componentId];\n if (!mainComponent) {\n throw new Error(`Component ${node.componentId} not found`);\n }\n\n const componentSet = mainComponent.componentSetId\n ? ctx.componentSets[mainComponent.componentSetId]\n : undefined;\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties ?? {})) {\n componentProperties[key] = value;\n\n if (value.type === \"INSTANCE_SWAP\") {\n // unless value.type === \"BOOLEAN\", value.value is string\n const swappedComponent = ctx.components[value.value as string];\n\n if (swappedComponent) {\n componentProperties[key].componentKey = swappedComponent.key;\n\n const swappedComponentSet = swappedComponent?.componentSetId\n ? ctx.componentSets[swappedComponent.componentSetId]\n : undefined;\n\n if (swappedComponentSet) {\n componentProperties[key].componentSetKey = swappedComponentSet.key;\n }\n }\n }\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n\n // NormalizedInstanceNode specific\n componentProperties,\n componentKey: mainComponent.key,\n componentSetKey: componentSet?.key,\n overrides: node.overrides,\n };\n }\n\n function normalizeVectorNode(node: FigmaRestSpec.VectorNode): NormalizedVectorNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeBooleanOperationNode(\n node: FigmaRestSpec.BooleanOperationNode,\n ): NormalizedBooleanOperationNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: normalizePaints(node.fills),\n fillStyleKey: node.styles?.[\"fill\"] ? ctx.styles[node.styles[\"fill\"]]?.key : undefined,\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...normalizeEffectProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeGroupNodeAsFrameNode(node: FigmaRestSpec.GroupNode): NormalizedFrameNode {\n return {\n // NormalizedIsLayerTrait\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: [],\n fillStyleKey: undefined,\n strokes: [],\n strokeWeight: undefined,\n\n // NormalizedHasEffectsTrait\n effects: [],\n effectStyleKey: undefined,\n\n // NormalizedCornerTrait\n cornerRadius: undefined,\n rectangleCornerRadii: undefined,\n\n // NormalizedHasFramePropertiesTrait\n // these are undefined compared to from-plugin normalizer\n // since inferredAutoLayout isn't available in REST API\n layoutMode: undefined,\n layoutWrap: undefined,\n paddingLeft: undefined,\n paddingRight: undefined,\n paddingTop: undefined,\n paddingBottom: undefined,\n primaryAxisAlignItems: undefined,\n primaryAxisSizingMode: undefined,\n counterAxisAlignItems: undefined,\n counterAxisSizingMode: undefined,\n itemSpacing: undefined,\n counterAxisSpacing: undefined,\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n return normalizeNode;\n}\n","/**\n * from-plugin is guaranteed to be run in the Figma Plugin environment\n * so we can use the Plugin API types directly (figma.getNodeByIdAsync, node.getMainComponentAsync etc)\n * however it could be better to make users can DI later\n */\n\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n NormalizedHasEffectsTrait,\n NormalizedShadow,\n NormalizedDefaultShapeTrait,\n NormalizedHasFramePropertiesTrait,\n NormalizedCornerTrait,\n NormalizedIsLayerTrait,\n NormalizedPaint,\n NormalizedTextSegment,\n} from \"./types\";\nimport { convertTransformToGradientHandles } from \"@/utils/figma-gradient\";\n\nexport function createPluginNormalizer(): (node: SceneNode) => Promise<NormalizedSceneNode> {\n async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {\n return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));\n }\n\n async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {\n switch (node.type) {\n case \"FRAME\":\n return normalizeFrameNode(node);\n case \"RECTANGLE\":\n return normalizeRectangleNode(node);\n case \"TEXT\":\n return normalizeTextNode(node);\n case \"COMPONENT\":\n return normalizeComponentNode(node);\n case \"INSTANCE\":\n return normalizeInstanceNode(node);\n case \"VECTOR\":\n return normalizeVectorNode(node);\n case \"BOOLEAN_OPERATION\":\n return normalizeBooleanOperationNode(node);\n case \"GROUP\":\n return normalizeGroupNodeAsFrameNode(node);\n default:\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n }\n\n /**\n * Pick specific fields from boundVariables\n */\n function normalizeBoundVariables({\n boundVariables,\n }: Pick<FrameNode, \"boundVariables\">): NormalizedIsLayerTrait[\"boundVariables\"] {\n if (!boundVariables) return undefined;\n\n return {\n fills: boundVariables.fills,\n strokes: boundVariables.strokes,\n itemSpacing: boundVariables.itemSpacing,\n counterAxisSpacing: boundVariables.counterAxisSpacing,\n topLeftRadius: boundVariables.topLeftRadius,\n topRightRadius: boundVariables.topRightRadius,\n bottomLeftRadius: boundVariables.bottomLeftRadius,\n bottomRightRadius: boundVariables.bottomRightRadius,\n paddingTop: boundVariables.paddingTop,\n paddingRight: boundVariables.paddingRight,\n paddingBottom: boundVariables.paddingBottom,\n paddingLeft: boundVariables.paddingLeft,\n minWidth: boundVariables.minWidth,\n maxWidth: boundVariables.maxWidth,\n minHeight: boundVariables.minHeight,\n maxHeight: boundVariables.maxHeight,\n fontSize: boundVariables.fontSize,\n fontWeight: boundVariables.fontWeight,\n lineHeight: boundVariables.lineHeight,\n size: {\n x: boundVariables.width,\n y: boundVariables.height,\n },\n };\n }\n\n function normalizeSolidPaint(paint: SolidPaint): NormalizedPaint {\n return {\n type: paint.type,\n color: {\n r: paint.color.r,\n g: paint.color.g,\n b: paint.color.b,\n a: paint.opacity ?? 1,\n },\n visible: paint.visible,\n blendMode: paint.blendMode ?? \"NORMAL\",\n opacity: paint.opacity,\n boundVariables: paint.boundVariables,\n };\n }\n\n function normalizePaint(paint: Paint): NormalizedPaint {\n switch (paint.type) {\n case \"SOLID\":\n return normalizeSolidPaint(paint);\n case \"IMAGE\":\n return {\n type: \"IMAGE\",\n scaleMode: paint.scaleMode === \"CROP\" ? \"STRETCH\" : paint.scaleMode,\n imageTransform: paint.imageTransform,\n scalingFactor: paint.scalingFactor,\n filters: paint.filters,\n rotation: paint.rotation,\n imageRef: paint.imageHash ?? \"\",\n blendMode: paint.blendMode ?? \"NORMAL\",\n visible: paint.visible,\n opacity: paint.opacity,\n };\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return {\n type: paint.type,\n gradientStops: [...paint.gradientStops],\n visible: paint.visible,\n opacity: paint.opacity,\n blendMode: paint.blendMode ?? \"NORMAL\",\n gradientHandlePositions: convertTransformToGradientHandles(paint.gradientTransform),\n };\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(fills: readonly Paint[] | PluginAPI[\"mixed\"]): NormalizedPaint[] {\n if (fills === figma.mixed) {\n console.warn(\"Mixed fills are not supported\");\n\n return [];\n }\n\n return fills.map(normalizePaint);\n }\n\n function normalizeRadiusProps(\n node: Pick<\n RectangleNode,\n \"cornerRadius\" | \"topLeftRadius\" | \"topRightRadius\" | \"bottomRightRadius\" | \"bottomLeftRadius\"\n >,\n ): NormalizedCornerTrait {\n return {\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: [\n node.topLeftRadius,\n node.topRightRadius,\n node.bottomRightRadius,\n node.bottomLeftRadius,\n ],\n };\n }\n\n async function normalizeEffectProps(\n node: Pick<RectangleNode, \"effects\" | \"effectStyleId\">,\n ): Promise<NormalizedHasEffectsTrait> {\n const effectStyleKey =\n typeof node.effectStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.effectStyleId))?.key\n : undefined;\n\n const effects = node.effects\n .filter((effect): effect is DropShadowEffect | InnerShadowEffect => {\n if (!effect.visible) return false;\n\n return effect.type === \"DROP_SHADOW\" || effect.type === \"INNER_SHADOW\";\n })\n .map(({ blendMode, visible, ...rest }): NormalizedShadow => rest);\n\n return {\n ...(effectStyleKey ? { effectStyleKey } : {}),\n effects,\n };\n }\n\n async function normalizeShapeProps(\n node: Pick<\n RectangleNode,\n | \"fills\"\n | \"fillStyleId\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n | \"effects\"\n | \"effectStyleId\"\n > &\n Partial<Pick<FrameNode, \"inferredAutoLayout\">>,\n ): Promise<Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait>> {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n // NormalizedHasLayoutTrait\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: await normalizePaints(node.fills),\n fillStyleKey,\n strokes: await normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...(await normalizeEffectProps(node)),\n };\n }\n\n async function normalizeAutolayoutProps(\n node: Omit<FrameNode, \"type\" | \"clone\">,\n ): Promise<NormalizedHasFramePropertiesTrait> {\n return {\n layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,\n primaryAxisAlignItems:\n node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,\n counterAxisAlignItems:\n node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,\n primaryAxisSizingMode:\n node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,\n counterAxisSizingMode:\n node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,\n counterAxisSpacing:\n node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,\n };\n }\n\n async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {\n const segments = node.getStyledTextSegments([\n \"fontName\",\n \"fontWeight\",\n \"fontSize\",\n \"letterSpacing\",\n \"lineHeight\",\n \"paragraphSpacing\",\n \"textStyleId\",\n \"fills\",\n \"boundVariables\",\n \"textDecoration\",\n ]);\n const first = segments[0];\n\n const textStyleKey =\n typeof node.textStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.textStyleId))?.key\n : undefined;\n\n const normalizeLetterSpacing = (\n letterSpacing: LetterSpacing,\n fontSize: number,\n ): NormalizedTextSegment[\"style\"][\"letterSpacing\"] => {\n if (letterSpacing.unit === \"PIXELS\") return letterSpacing.value;\n if (letterSpacing.unit === \"PERCENT\") return (fontSize * letterSpacing.value) / 100;\n\n return undefined;\n };\n\n const normalizeLineHeight = (\n lineHeight: LineHeight,\n fontSize: number,\n ): NormalizedTextSegment[\"style\"][\"lineHeight\"] => {\n if (lineHeight.unit === \"PIXELS\") return lineHeight.value;\n if (lineHeight.unit === \"PERCENT\") return (fontSize * lineHeight.value) / 100;\n\n return undefined;\n };\n\n const isItalic = (fontName: FontName): boolean => {\n // {\n // family: \"SF Mono\",\n // style: \"Bold Italic\"\n // }\n return fontName.style.toLowerCase().includes(\"italic\");\n };\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedTypePropertiesTrait\n // NOTE: this normalization is incomplete compared to from-rest.ts normalizer\n style: {\n fontFamily: first.fontName.family,\n fontPostScriptName: null,\n fontStyle: first.fontName.style,\n italic: isItalic(first.fontName),\n fontWeight: first.fontWeight,\n fontSize: first.fontSize,\n textAlignHorizontal: node.textAlignHorizontal,\n textAlignVertical: node.textAlignVertical,\n letterSpacing: normalizeLetterSpacing(first.letterSpacing, first.fontSize),\n paragraphSpacing: first.paragraphSpacing,\n textDecoration: first.textDecoration,\n lineHeightPx: normalizeLineHeight(first.lineHeight, first.fontSize),\n lineHeightUnit:\n first.lineHeight.unit === \"PIXELS\"\n ? \"PIXELS\"\n : first.lineHeight.unit === \"PERCENT\"\n ? \"FONT_SIZE_%\"\n : undefined,\n boundVariables: first.boundVariables,\n maxLines: node.maxLines ?? undefined,\n },\n characters: node.characters,\n textStyleKey,\n segments: segments.map((segment) => ({\n characters: segment.characters,\n start: segment.start,\n end: segment.end,\n style: {\n fontSize: segment.fontSize,\n fontWeight: segment.fontWeight,\n fontFamily: segment.fontName.family,\n italic: isItalic(segment.fontName),\n letterSpacing: normalizeLetterSpacing(segment.letterSpacing, segment.fontSize),\n lineHeight: normalizeLineHeight(segment.lineHeight, segment.fontSize),\n textDecoration: segment.textDecoration,\n },\n })),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeInstanceNode(node: InstanceNode): Promise<NormalizedInstanceNode> {\n const mainComponent = await node.getMainComponentAsync();\n if (!mainComponent) {\n throw new Error(\"Instance node has no main component\");\n }\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties)) {\n componentProperties[key] = value;\n\n if (value.type === \"INSTANCE_SWAP\") {\n // unless value.type === \"BOOLEAN\", value.value is string\n const swappedComponent = (await figma.getNodeByIdAsync(\n value.value as string,\n )) as ComponentNode;\n\n if (swappedComponent) {\n componentProperties[key].componentKey = swappedComponent.key;\n\n if (swappedComponent.parent?.type === \"COMPONENT_SET\") {\n componentProperties[key].componentSetKey = swappedComponent.parent.key;\n }\n }\n }\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n\n // NormalizedInstanceNode specific\n componentProperties,\n componentKey: mainComponent.key,\n componentSetKey:\n mainComponent.parent?.type === \"COMPONENT_SET\" ? mainComponent.parent.key : undefined,\n overrides: node.overrides,\n };\n }\n\n async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedCornerTrait\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: undefined, // VectorNode does not have individual corner radii\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeBooleanOperationNode(\n node: BooleanOperationNode,\n ): Promise<NormalizedBooleanOperationNode> {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow as 0 | 1 | undefined,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: await normalizePaints(node.fills),\n fillStyleKey,\n strokes: await normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...(await normalizeEffectProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeGroupNodeAsFrameNode(\n node: GroupNode & { inferredAutoLayout?: FrameNode[\"inferredAutoLayout\"] },\n ): Promise<NormalizedFrameNode> {\n return {\n // NormalizedIsLayerTrait\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: [],\n fillStyleKey: undefined,\n strokes: [],\n strokeWeight: undefined,\n\n // NormalizedHasEffectsTrait\n effects: [],\n effectStyleKey: undefined,\n\n // NormalizedCornerTrait\n cornerRadius: undefined,\n rectangleCornerRadii: undefined,\n\n // NormalizedHasFramePropertiesTrait\n layoutMode: node.inferredAutoLayout?.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom,\n primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,\n counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,\n primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,\n counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing,\n counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n return normalizeNode;\n}\n","import type { ComponentPropertyDefinition } from \"@/codegen\";\n\nexport interface ComponentMetadata {\n name: string;\n key: string;\n componentPropertyDefinitions: Record<string, ComponentPropertyDefinition>;\n}\n","export interface IconData {\n name: string;\n type: \"monochrome\" | \"multicolor\";\n weight?: string;\n}\n","import type {\n LocalVariable,\n LocalVariableCollection,\n VariableAlias,\n VariableResolvedDataType,\n} from \"@figma/rest-api-spec\";\n\nexport type Variable = LocalVariable;\n\nexport type VariableCollection = LocalVariableCollection;\n\nexport type VariableType = VariableResolvedDataType;\n\nexport type VariableValue = Variable[\"valuesByMode\"][string];\n\nexport type VariableValueResolved = Exclude<VariableValue, VariableAlias>;\n\nexport type { VariableScope } from \"@figma/rest-api-spec\";\n","import type { Variable, VariableCollection } from \"./variable.interface\";\n\nexport interface VariableRepository {\n getVariableList(): Variable[];\n getVariableCollectionList(): VariableCollection[];\n findVariableByKey(key: string): Variable | undefined;\n findVariableById(id: string): Variable | undefined;\n findVariableByName(name: string): Variable | undefined;\n findVariableCollectionByKey(key: string): VariableCollection | undefined;\n findVariableCollectionById(id: string): VariableCollection | undefined;\n}\n\nexport function createStaticVariableRepository({\n variables,\n variableCollections,\n}: {\n variables: Record<string, Variable>;\n variableCollections: Record<string, VariableCollection>;\n}): VariableRepository {\n const variablesKeyMap = new Map<string, Variable>();\n const variablesIdMap = new Map<string, Variable>();\n const variablesNameMap = new Map<string, Variable>();\n const variableCollectionsKeyMap = new Map<string, VariableCollection>();\n const variableCollectionsIdMap = new Map<string, VariableCollection>();\n\n for (const variable of Object.values(variables)) {\n if (variable.remote) {\n continue;\n }\n\n variablesKeyMap.set(variable.key, variable);\n variablesIdMap.set(variable.id, variable);\n variablesNameMap.set(variable.name, variable);\n }\n\n for (const variableCollection of Object.values(variableCollections)) {\n if (variableCollection.remote) {\n continue;\n }\n\n variableCollectionsKeyMap.set(variableCollection.key, variableCollection);\n variableCollectionsIdMap.set(variableCollection.id, variableCollection);\n }\n\n const variablesList = [...variablesKeyMap.values()];\n const variableCollectionsList = [...variableCollectionsKeyMap.values()];\n\n return {\n getVariableList: () => variablesList,\n getVariableCollectionList: () => variableCollectionsList,\n findVariableByName: (name: string) => variablesNameMap.get(name),\n findVariableByKey: (key: string) => variablesKeyMap.get(key),\n findVariableById: (id: string) => variablesIdMap.get(id),\n findVariableCollectionByKey: (key: string) => variableCollectionsKeyMap.get(key),\n findVariableCollectionById: (id: string) => variableCollectionsIdMap.get(id),\n };\n}\n","import type { Style as FigmaStyle, StyleType as FigmaStyleType } from \"@figma/rest-api-spec\";\n\nexport type Style = FigmaStyle;\n\nexport type StyleType = FigmaStyleType;\n","import type { Style } from \"./style.interface\";\n\nexport interface StyleRepository {\n getAll(): Style[];\n getTextStyles(): Style[];\n getColorStyles(): Style[];\n findOneByKey(key: string): Style | undefined;\n findOneByName(name: string): Style | undefined;\n}\n\nexport function createStaticStyleRepository(styles: Style[]): StyleRepository {\n const stylesMap = new Map<string, Style>();\n const stylesNameMap = new Map<string, Style>();\n\n for (const style of styles) {\n stylesMap.set(style.key, style);\n stylesNameMap.set(style.name, style);\n }\n\n return {\n getAll: () => styles,\n getTextStyles: () => styles.filter((style) => style.styleType === \"TEXT\"),\n getColorStyles: () => styles.filter((style) => style.styleType === \"FILL\"),\n findOneByKey: (key) => stylesMap.get(key),\n findOneByName: (name) => stylesNameMap.get(name),\n };\n}\n","import type { IconData } from \"./icon.interface\";\n\nexport interface IconRepository {\n getOne(key: string): IconData;\n}\n\nexport function createStaticIconRepository(iconRecord: Record<string, IconData>) {\n return {\n getOne: (key: string) => iconRecord[key],\n };\n}\n","import type { IconData } from \"./icon.interface\";\nimport type { IconRepository } from \"./icon.repository\";\n\nexport interface IconService {\n isAvailable: (componentKey: string) => boolean;\n getOne: (componentKey: string) => IconData;\n}\n\nexport function createIconService({\n iconRepository,\n}: {\n iconRepository: IconRepository;\n}): IconService {\n function isAvailable(componentKey: string) {\n return iconRepository.getOne(componentKey) !== undefined;\n }\n\n function getOne(componentKey: string) {\n return iconRepository.getOne(componentKey);\n }\n\n return {\n isAvailable,\n getOne,\n };\n}\n","import type { StyleRepository } from \"./style.repository\";\n\nexport interface StyleService {\n getSlug: (id: string) => string[] | undefined;\n}\n\n// TODO: inferStyleName 추가해야 함, rest api에서 style value가 제공되지 않고 있어 보류\nexport function createStyleService({\n styleRepository,\n}: {\n styleRepository: StyleRepository;\n}): StyleService {\n function getName(id: string) {\n const style = styleRepository.findOneByKey(id);\n\n if (!style) {\n return undefined;\n }\n\n return style.name;\n }\n\n function getSlug(id: string): string[] | undefined {\n const name = getName(id);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n return {\n getSlug,\n };\n}\n","import {\n isIdenticalVariableValue,\n isInsideScope,\n isVariableAlias,\n sanitizeVariableId,\n} from \"@/utils/figma-variable\";\nimport type { Variable, VariableScope, VariableValueResolved } from \"./variable.interface\";\nimport type { VariableRepository } from \"./variable.repository\";\n\nexport interface VariableService {\n getSlug: (id: string) => string[] | undefined;\n resolveValue: (variable: Variable, mode: string) => VariableValueResolved;\n infer: (value: VariableValueResolved, scope: VariableScope) => Variable | undefined;\n}\n\nexport interface VariableServiceDeps {\n variableRepository: VariableRepository;\n inferCompareFunction: (a: Variable, b: Variable) => number;\n}\n\nexport function createVariableService({\n variableRepository,\n inferCompareFunction,\n}: VariableServiceDeps): VariableService {\n const variables = variableRepository.getVariableList();\n\n // private\n function getName(key: string) {\n const sanitizedId = sanitizeVariableId(key);\n const variable = variableRepository.findVariableByKey(sanitizedId);\n\n if (!variable) {\n return undefined;\n }\n\n return variable.name;\n }\n\n function getDefaultModeId(variable: Variable) {\n const variableCollection = variableRepository.findVariableCollectionById(\n variable.variableCollectionId,\n );\n\n if (!variableCollection) {\n // Variable collection not found: ${variable.variableCollectionId}, falling back to variable.valuesByMode key\n return Object.keys(variable.valuesByMode)[0]!;\n }\n\n return variableCollection.defaultModeId;\n }\n\n // public\n function getSlug(key: string): string[] | undefined {\n const name = getName(key);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n function resolveValue(variable: Variable, mode: string): VariableValueResolved {\n const value = variable.valuesByMode[mode];\n\n if (value === undefined) {\n throw new Error(`Variable value not found: ${variable.id} ${mode}`);\n }\n\n if (isVariableAlias(value)) {\n const resolvedVariable = variableRepository.findVariableById(value.id);\n\n if (!resolvedVariable) {\n throw new Error(`Variable not found: ${value.id}`);\n }\n\n return resolveValue(resolvedVariable, mode);\n }\n\n return value;\n }\n\n function infer(value: VariableValueResolved, scope: VariableScope) {\n // NOTE: We assume that the variable is in the default mode or value is equal between all modes for simplicity.\n const inferredVariables = variables.filter(\n (variable) =>\n isInsideScope(variable, scope) &&\n isIdenticalVariableValue(resolveValue(variable, getDefaultModeId(variable)), value),\n );\n\n const sortedVariables = inferredVariables.sort(inferCompareFunction);\n\n return sortedVariables[0];\n }\n\n return {\n getSlug,\n resolveValue,\n infer,\n };\n}\n","import type { ComponentMetadata } from \"./component.interface\";\n\nexport interface ComponentRepository {\n getOne(key: string): ComponentMetadata | undefined;\n}\n\nexport function createStaticComponentRepository(data: Record<string, ComponentMetadata>) {\n const componentRecord: Record<string, ComponentMetadata> = {};\n Object.values(data).forEach((component) => {\n componentRecord[component.key] = component;\n });\n\n return {\n getOne: (key: string) => componentRecord[key],\n };\n}\n","import { createStaticIconRepository } from \"./icon.repository\";\nimport { FIGMA_ICONS } from \"./data/__generated__/icons\";\nimport { FIGMA_STYLES } from \"./data/__generated__/styles\";\nimport { FIGMA_VARIABLE_COLLECTIONS } from \"./data/__generated__/variable-collections\";\nimport { FIGMA_VARIABLES } from \"./data/__generated__/variables\";\nimport * as FIGMA_COMPONENTS from \"./data/__generated__/component-sets\";\nimport { createStaticStyleRepository } from \"./style.repository\";\nimport { createStaticVariableRepository } from \"./variable.repository\";\nimport { createStaticComponentRepository } from \"./component.repository\";\n\nexport * from \"./icon.interface\";\nexport * from \"./icon.repository\";\nexport * from \"./icon.service\";\nexport * from \"./style.interface\";\nexport * from \"./style.repository\";\nexport * from \"./style.service\";\nexport * from \"./variable.interface\";\nexport * from \"./variable.repository\";\nexport * from \"./variable.service\";\nexport * from \"./component.interface\";\nexport * from \"./component.repository\";\n\nexport const styleRepository = createStaticStyleRepository(FIGMA_STYLES);\nexport const variableRepository = createStaticVariableRepository({\n variables: FIGMA_VARIABLES,\n variableCollections: FIGMA_VARIABLE_COLLECTIONS,\n});\nexport const iconRepository = createStaticIconRepository(FIGMA_ICONS);\nexport const componentRepository = createStaticComponentRepository(FIGMA_COMPONENTS);\n\nexport function getFigmaVariableKey(name: string) {\n return variableRepository.findVariableByName(name)?.key;\n}\n\nexport function getFigmaStyleKey(name: string) {\n return styleRepository.findOneByName(name)?.key;\n}\n\nexport function getFigmaColorVariableNames(scopes: Array<\"fg\" | \"bg\" | \"stroke\" | \"palette\">) {\n const variables = variableRepository.getVariableList();\n return variables\n .filter((variable) =>\n scopes.includes(variable.name.split(\"/\")[0] as \"fg\" | \"bg\" | \"stroke\" | \"palette\"),\n )\n .map((variable) => variable.name);\n}\n"],"names":[],"mappings":";;;;;;AACO;AACP;AACA;AACO;AACA;AACP;AACA;AACO;AACA;AACA;AACA;AACP;AACA;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACO;;ACjFP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACzBP;AACA;AACA;AACA;AACA;;AAEO;;ACLA;AACP;AACA;AACA;AACA;;ACLO;AACP;AACA;AACA;AACA;;ACHO;AACA;AACA;AACA;AACA;;ACJA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACZO;AACA;;ACDA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;;ACPA;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACO;AACP;AACA;;ACPO;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;;ACVA;AACP;AACA;AACO;AACP;AACA;;ACKO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACA;AACA;;;"}
1
+ {"version":3,"file":"index.d.ts","sources":["../src/normalizer/types.ts","../src/normalizer/from-rest.ts","../src/normalizer/from-plugin.ts","../src/entities/component.interface.ts","../src/entities/icon.interface.ts","../src/entities/variable.interface.ts","../src/entities/variable.repository.ts","../src/entities/style.interface.ts","../src/entities/style.repository.ts","../src/entities/icon.repository.ts","../src/entities/icon.service.ts","../src/entities/style.service.ts","../src/entities/variable.service.ts","../src/entities/component.repository.ts","../src/entities/index.ts"],"sourcesContent":["import type * as FigmaRestSpec from \"@figma/rest-api-spec\";\n\nexport type NormalizedIsLayerTrait = Pick<FigmaRestSpec.IsLayerTrait, \"type\" | \"id\" | \"name\"> & {\n boundVariables?: Pick<\n NonNullable<FigmaRestSpec.IsLayerTrait[\"boundVariables\"]>,\n | \"fills\"\n | \"strokes\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n | \"bottomLeftRadius\"\n | \"bottomRightRadius\"\n | \"topLeftRadius\"\n | \"topRightRadius\"\n | \"paddingBottom\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"maxHeight\"\n | \"minHeight\"\n | \"maxWidth\"\n | \"minWidth\"\n | \"fontSize\"\n | \"fontWeight\"\n | \"lineHeight\"\n | \"size\"\n >;\n};\n\nexport type NormalizedCornerTrait = Pick<\n FigmaRestSpec.CornerTrait,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n>;\n\nexport type NormalizedHasChildrenTrait = {\n children: NormalizedSceneNode[];\n};\n\nexport type NormalizedHasLayoutTrait = Pick<\n FigmaRestSpec.HasLayoutTrait,\n | \"layoutAlign\"\n | \"layoutGrow\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n>;\n\nexport type NormalizedSolidPaint = FigmaRestSpec.SolidPaint;\n\nexport type NormalizedPaint =\n | NormalizedSolidPaint\n | FigmaRestSpec.GradientPaint\n | FigmaRestSpec.ImagePaint;\n\nexport type NormalizedHasGeometryTrait = Omit<\n Pick<FigmaRestSpec.HasGeometryTrait, \"fills\" | \"strokes\" | \"strokeWeight\">,\n \"fills\" | \"strokes\"\n> & {\n fills: NormalizedPaint[];\n strokes: NormalizedPaint[];\n fillStyleKey?: string;\n};\n\nexport type NormalizedShadow =\n | (Pick<\n FigmaRestSpec.DropShadowEffect,\n \"color\" | \"offset\" | \"radius\" | \"spread\" | \"boundVariables\"\n > &\n Required<Pick<FigmaRestSpec.DropShadowEffect, \"type\">>)\n | (Pick<\n FigmaRestSpec.InnerShadowEffect,\n \"color\" | \"offset\" | \"radius\" | \"spread\" | \"boundVariables\"\n > &\n Required<Pick<FigmaRestSpec.InnerShadowEffect, \"type\">>);\n\nexport type NormalizedHasEffectsTrait = Omit<FigmaRestSpec.HasEffectsTrait, \"effects\"> & {\n effects: NormalizedShadow[];\n effectStyleKey?: string;\n};\n\nexport type NormalizedHasFramePropertiesTrait = Pick<\n FigmaRestSpec.HasFramePropertiesTrait,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n>;\n\nexport interface NormalizedTextSegment {\n characters: string;\n start: number;\n end: number;\n style: {\n fontFamily?: string;\n fontWeight?: number;\n fontSize?: number;\n italic?: boolean;\n textDecoration?: string;\n letterSpacing?: number;\n /**\n * in pixels\n */\n lineHeight?: number;\n };\n}\n\nexport type NormalizedTypePropertiesTrait = Pick<\n FigmaRestSpec.TypePropertiesTrait,\n \"style\" | \"characters\"\n> & {\n segments: NormalizedTextSegment[];\n\n textStyleKey?: string;\n};\n\nexport type NormalizedDefaultShapeTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasEffectsTrait;\n\nexport type NormalizedFrameTrait = NormalizedIsLayerTrait &\n NormalizedHasLayoutTrait &\n NormalizedHasGeometryTrait &\n NormalizedHasEffectsTrait &\n NormalizedHasChildrenTrait &\n NormalizedCornerTrait &\n NormalizedHasFramePropertiesTrait;\n\nexport interface NormalizedFrameNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.FrameNode[\"type\"];\n}\n\nexport interface NormalizedRectangleNode\n extends NormalizedDefaultShapeTrait,\n NormalizedCornerTrait {\n type: FigmaRestSpec.RectangleNode[\"type\"];\n}\n\nexport interface NormalizedTextNode\n extends NormalizedDefaultShapeTrait,\n NormalizedTypePropertiesTrait {\n type: FigmaRestSpec.TextNode[\"type\"];\n}\n\nexport interface NormalizedComponentNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.ComponentNode[\"type\"];\n}\n\nexport interface NormalizedInstanceNode extends NormalizedFrameTrait {\n type: FigmaRestSpec.InstanceNode[\"type\"];\n\n componentProperties: {\n [key: string]: FigmaRestSpec.ComponentProperty & {\n componentKey?: string;\n componentSetKey?: string;\n };\n };\n\n componentKey: string;\n\n componentSetKey?: string;\n\n overrides?: FigmaRestSpec.InstanceNode[\"overrides\"];\n\n children: NormalizedSceneNode[];\n}\n\nexport interface NormalizedVectorNode extends NormalizedDefaultShapeTrait, NormalizedCornerTrait {\n type: FigmaRestSpec.VectorNode[\"type\"];\n}\n\nexport interface NormalizedBooleanOperationNode\n extends NormalizedIsLayerTrait,\n NormalizedHasChildrenTrait,\n NormalizedHasLayoutTrait,\n NormalizedHasGeometryTrait,\n NormalizedHasEffectsTrait {\n type: FigmaRestSpec.BooleanOperationNode[\"type\"];\n}\n\nexport interface NormalizedUnhandledNode {\n type: \"UNHANDLED\";\n id: string;\n original: FigmaRestSpec.Node | SceneNode;\n}\n\nexport type NormalizedSceneNode =\n | NormalizedFrameNode\n | NormalizedRectangleNode\n | NormalizedTextNode\n | NormalizedComponentNode\n | NormalizedInstanceNode\n | NormalizedVectorNode\n | NormalizedBooleanOperationNode\n | NormalizedUnhandledNode;\n","/**\n * from-rest could be run outside of the Figma Plugin environment\n * so we cannot use the Plugin API types directly e.g. getNodeByIdAsync\n */\n\n/**\n * NOTE: types of MinimalFillsTrait[\"styles\"] can be found here:\n * https://developers.figma.com/docs/rest-api/component-types/#style-type\n * Record<\"text\" | \"fill\" | \"stroke\" | \"effect\" | \"grid\", string>\n */\n\nimport type * as FigmaRestSpec from \"@figma/rest-api-spec\";\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedTextSegment,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n NormalizedShadow,\n NormalizedCornerTrait,\n NormalizedHasFramePropertiesTrait,\n NormalizedPaint,\n NormalizedDefaultShapeTrait,\n NormalizedHasEffectsTrait,\n NormalizedIsLayerTrait,\n} from \"./types\";\n\nexport interface RestNormalizerContext {\n /**\n * A map of style **ID** to style data\n */\n styles: Record<string, FigmaRestSpec.Style>;\n /**\n * A map of component **ID** to component data\n */\n components: Record<string, FigmaRestSpec.Component>;\n /**\n * A map of component set **ID** to component set data\n */\n componentSets: Record<string, FigmaRestSpec.ComponentSet>;\n}\n\nexport function createRestNormalizer(\n ctx: RestNormalizerContext,\n): (node: FigmaRestSpec.Node) => NormalizedSceneNode {\n function normalizeNodes(nodes: readonly FigmaRestSpec.Node[]): NormalizedSceneNode[] {\n // Figma REST API omits default values for some fields, \"visible\" is one of them\n return nodes.filter((node) => !(\"visible\" in node) || node.visible).map(normalizeNode);\n }\n\n function normalizeNode(node: FigmaRestSpec.Node): NormalizedSceneNode {\n switch (node.type) {\n case \"FRAME\":\n return normalizeFrameNode(node);\n case \"RECTANGLE\":\n return normalizeRectangleNode(node);\n case \"TEXT\":\n return normalizeTextNode(node);\n case \"COMPONENT\":\n return normalizeComponentNode(node);\n case \"INSTANCE\":\n return normalizeInstanceNode(node);\n case \"VECTOR\":\n return normalizeVectorNode(node);\n case \"BOOLEAN_OPERATION\":\n return normalizeBooleanOperationNode(node);\n case \"GROUP\":\n return normalizeGroupNodeAsFrameNode(node);\n default:\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n }\n\n function normalizeBoundVariables(\n boundVariables: FigmaRestSpec.IsLayerTrait[\"boundVariables\"] | undefined,\n ) {\n if (!boundVariables) return undefined;\n\n return {\n fills: boundVariables.fills,\n strokes: boundVariables.strokes,\n itemSpacing: boundVariables.itemSpacing,\n counterAxisSpacing: boundVariables.counterAxisSpacing,\n topLeftRadius: boundVariables.topLeftRadius,\n topRightRadius: boundVariables.topRightRadius,\n bottomLeftRadius: boundVariables.bottomLeftRadius,\n bottomRightRadius: boundVariables.bottomRightRadius,\n paddingTop: boundVariables.paddingTop,\n paddingRight: boundVariables.paddingRight,\n paddingBottom: boundVariables.paddingBottom,\n paddingLeft: boundVariables.paddingLeft,\n minWidth: boundVariables.minWidth,\n maxWidth: boundVariables.maxWidth,\n minHeight: boundVariables.minHeight,\n maxHeight: boundVariables.maxHeight,\n fontSize: boundVariables.fontSize,\n fontWeight: boundVariables.fontWeight,\n lineHeight: boundVariables.lineHeight,\n size: boundVariables.size,\n };\n }\n\n function normalizePaint(paint: FigmaRestSpec.Paint): NormalizedPaint {\n switch (paint.type) {\n case \"SOLID\":\n case \"IMAGE\":\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return paint;\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(paints: FigmaRestSpec.Paint[] | undefined): NormalizedPaint[] {\n if (!paints) return [];\n\n return paints.map(normalizePaint);\n }\n\n function normalizeRadiusProps({\n cornerRadius,\n rectangleCornerRadii,\n }: Pick<\n FigmaRestSpec.RectangleNode,\n \"cornerRadius\" | \"rectangleCornerRadii\"\n >): NormalizedCornerTrait {\n return { cornerRadius, rectangleCornerRadii };\n }\n\n function normalizeEffectProps(\n node: Pick<FigmaRestSpec.FrameNode, \"effects\" | \"styles\">,\n ): NormalizedHasEffectsTrait {\n const effects = (node.effects ?? [])\n .filter(\n (effect): effect is FigmaRestSpec.DropShadowEffect | FigmaRestSpec.InnerShadowEffect =>\n effect.visible !== false &&\n (effect.type === \"DROP_SHADOW\" || effect.type === \"INNER_SHADOW\"),\n )\n .map((effect): NormalizedShadow => {\n const { type, color, offset, radius, spread, boundVariables } = effect;\n\n return {\n // remove fallback when resolved: https://github.com/figma/rest-api-spec/issues/84\n type: type ?? \"INNER_SHADOW\",\n color,\n offset,\n radius,\n spread,\n boundVariables,\n };\n });\n\n return {\n effects,\n effectStyleKey: node.styles?.[\"effect\"] ? ctx.styles[node.styles[\"effect\"]]?.key : undefined,\n };\n }\n\n function normalizeShapeProps(\n node: Pick<\n FigmaRestSpec.FrameNode,\n | \"fills\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"styles\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n | \"effects\"\n >,\n ): Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait> {\n return {\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: normalizePaints(node.fills),\n fillStyleKey: node.styles?.[\"fill\"] ? ctx.styles[node.styles[\"fill\"]]?.key : undefined,\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...normalizeEffectProps(node),\n };\n }\n\n function normalizeAutolayoutProps(\n node: Pick<\n FigmaRestSpec.FrameNode,\n | \"layoutMode\"\n | \"layoutWrap\"\n | \"paddingLeft\"\n | \"paddingRight\"\n | \"paddingTop\"\n | \"paddingBottom\"\n | \"primaryAxisAlignItems\"\n | \"primaryAxisSizingMode\"\n | \"counterAxisAlignItems\"\n | \"counterAxisSizingMode\"\n | \"itemSpacing\"\n | \"counterAxisSpacing\"\n >,\n ): NormalizedHasFramePropertiesTrait {\n return {\n layoutMode: node.layoutMode,\n layoutWrap: node.layoutWrap,\n paddingLeft: node.paddingLeft,\n paddingRight: node.paddingRight,\n paddingTop: node.paddingTop,\n paddingBottom: node.paddingBottom,\n primaryAxisAlignItems: node.primaryAxisAlignItems,\n primaryAxisSizingMode: node.primaryAxisSizingMode,\n counterAxisAlignItems: node.counterAxisAlignItems,\n counterAxisSizingMode: node.counterAxisSizingMode,\n itemSpacing: node.itemSpacing,\n counterAxisSpacing: node.counterAxisSpacing,\n };\n }\n\n function normalizeFrameNode(node: FigmaRestSpec.FrameNode): NormalizedFrameNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait, NormalizedHasFramePropertiesTrait\n ...normalizeShapeProps(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeRectangleNode(node: FigmaRestSpec.RectangleNode): NormalizedRectangleNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeTextNode(node: FigmaRestSpec.TextNode): NormalizedTextNode {\n // Convert TypeStyle to NormalizedTextSegment.style format\n function normalizeSegmentStyle(\n typeStyle: FigmaRestSpec.TypeStyle,\n ): NormalizedTextSegment[\"style\"] {\n return {\n fontFamily: typeStyle.fontFamily,\n fontWeight: typeStyle.fontWeight,\n fontSize: typeStyle.fontSize,\n italic: typeStyle.italic,\n textDecoration: typeStyle.textDecoration,\n letterSpacing: typeStyle.letterSpacing,\n lineHeight: typeStyle.lineHeightPx,\n };\n }\n\n // Function to segment a text node based on style overrides\n function segmentTextNode(textNode: FigmaRestSpec.TextNode): NormalizedTextSegment[] {\n const segments: NormalizedTextSegment[] = [];\n const characters = textNode.characters;\n const styleOverrides = textNode.characterStyleOverrides || [];\n const styleTable = textNode.styleOverrideTable || {};\n\n // If no style overrides, return the entire text as one segment\n if (!styleOverrides.length) {\n return [\n {\n characters: characters,\n start: 0,\n end: characters.length,\n style: normalizeSegmentStyle(textNode.style),\n },\n ];\n }\n\n let currentSegment: NormalizedTextSegment = {\n characters: \"\",\n start: 0,\n end: 0,\n style: {},\n };\n\n let currentStyleId: string | null = null;\n\n for (let i = 0; i < characters.length; i++) {\n const styleId = styleOverrides[i]?.toString() || null;\n\n // If style changes or it's the first character\n if (styleId !== currentStyleId || i === 0) {\n // Add the previous segment if it exists\n if (i > 0) {\n currentSegment.end = i;\n currentSegment.characters = characters.substring(\n currentSegment.start,\n currentSegment.end,\n );\n segments.push({ ...currentSegment });\n }\n\n // Start a new segment\n currentStyleId = styleId;\n currentSegment = {\n characters: \"\",\n start: i,\n end: 0,\n style: styleId ? normalizeSegmentStyle(styleTable[styleId]) : {},\n };\n }\n }\n\n // Add the last segment\n if (currentSegment.start < characters.length) {\n currentSegment.end = characters.length;\n currentSegment.characters = characters.substring(currentSegment.start, currentSegment.end);\n segments.push(currentSegment);\n }\n\n return segments;\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedTypePropertiesTrait\n style: node.style, // this style is the style of the first segment\n characters: node.characters,\n textStyleKey: node.styles?.[\"text\"] ? ctx.styles[node.styles[\"text\"]]?.key : undefined,\n segments: segmentTextNode(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeComponentNode(node: FigmaRestSpec.ComponentNode): NormalizedComponentNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n\n // NormalizedHasCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeInstanceNode(node: FigmaRestSpec.InstanceNode): NormalizedInstanceNode {\n const mainComponent = ctx.components[node.componentId];\n if (!mainComponent) {\n throw new Error(`Component ${node.componentId} not found`);\n }\n\n const componentSet = mainComponent.componentSetId\n ? ctx.componentSets[mainComponent.componentSetId]\n : undefined;\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties ?? {})) {\n componentProperties[key] = value;\n\n if (value.type === \"INSTANCE_SWAP\") {\n // unless value.type === \"BOOLEAN\", value.value is string\n const swappedComponent = ctx.components[value.value as string];\n\n if (swappedComponent) {\n componentProperties[key].componentKey = swappedComponent.key;\n\n const swappedComponentSet = swappedComponent?.componentSetId\n ? ctx.componentSets[swappedComponent.componentSetId]\n : undefined;\n\n if (swappedComponentSet) {\n componentProperties[key].componentSetKey = swappedComponentSet.key;\n }\n }\n }\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...normalizeAutolayoutProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n\n // NormalizedInstanceNode specific\n componentProperties,\n componentKey: mainComponent.key,\n componentSetKey: componentSet?.key,\n overrides: node.overrides,\n };\n }\n\n function normalizeVectorNode(node: FigmaRestSpec.VectorNode): NormalizedVectorNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...normalizeShapeProps(node),\n };\n }\n\n function normalizeBooleanOperationNode(\n node: FigmaRestSpec.BooleanOperationNode,\n ): NormalizedBooleanOperationNode {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: normalizePaints(node.fills),\n fillStyleKey: node.styles?.[\"fill\"] ? ctx.styles[node.styles[\"fill\"]]?.key : undefined,\n strokes: normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...normalizeEffectProps(node),\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n function normalizeGroupNodeAsFrameNode(node: FigmaRestSpec.GroupNode): NormalizedFrameNode {\n return {\n // NormalizedIsLayerTrait\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node.boundVariables),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight,\n minWidth: node.minWidth,\n maxHeight: node.maxHeight,\n maxWidth: node.maxWidth,\n\n // NormalizedHasGeometryTrait\n fills: [],\n fillStyleKey: undefined,\n strokes: [],\n strokeWeight: undefined,\n\n // NormalizedHasEffectsTrait\n effects: [],\n effectStyleKey: undefined,\n\n // NormalizedCornerTrait\n cornerRadius: undefined,\n rectangleCornerRadii: undefined,\n\n // NormalizedHasFramePropertiesTrait\n // these are undefined compared to from-plugin normalizer\n // since inferredAutoLayout isn't available in REST API\n layoutMode: undefined,\n layoutWrap: undefined,\n paddingLeft: undefined,\n paddingRight: undefined,\n paddingTop: undefined,\n paddingBottom: undefined,\n primaryAxisAlignItems: undefined,\n primaryAxisSizingMode: undefined,\n counterAxisAlignItems: undefined,\n counterAxisSizingMode: undefined,\n itemSpacing: undefined,\n counterAxisSpacing: undefined,\n\n // NormalizedHasChildrenTrait\n children: normalizeNodes(node.children),\n };\n }\n\n return normalizeNode;\n}\n","/**\n * from-plugin is guaranteed to be run in the Figma Plugin environment\n * so we can use the Plugin API types directly (figma.getNodeByIdAsync, node.getMainComponentAsync etc)\n * however it could be better to make users can DI later\n */\n\nimport type {\n NormalizedSceneNode,\n NormalizedFrameNode,\n NormalizedRectangleNode,\n NormalizedTextNode,\n NormalizedComponentNode,\n NormalizedInstanceNode,\n NormalizedVectorNode,\n NormalizedBooleanOperationNode,\n NormalizedHasEffectsTrait,\n NormalizedShadow,\n NormalizedDefaultShapeTrait,\n NormalizedHasFramePropertiesTrait,\n NormalizedCornerTrait,\n NormalizedIsLayerTrait,\n NormalizedPaint,\n NormalizedTextSegment,\n} from \"./types\";\nimport { convertTransformToGradientHandles } from \"@/utils/figma-gradient\";\n\nexport function createPluginNormalizer(): (node: SceneNode) => Promise<NormalizedSceneNode> {\n async function normalizeNodes(nodes: readonly SceneNode[]): Promise<NormalizedSceneNode[]> {\n return Promise.all(nodes.filter((node) => node.visible).map(normalizeNode));\n }\n\n async function normalizeNode(node: SceneNode): Promise<NormalizedSceneNode> {\n switch (node.type) {\n case \"FRAME\":\n return normalizeFrameNode(node);\n case \"RECTANGLE\":\n return normalizeRectangleNode(node);\n case \"TEXT\":\n return normalizeTextNode(node);\n case \"COMPONENT\":\n return normalizeComponentNode(node);\n case \"INSTANCE\":\n return normalizeInstanceNode(node);\n case \"VECTOR\":\n return normalizeVectorNode(node);\n case \"BOOLEAN_OPERATION\":\n return normalizeBooleanOperationNode(node);\n case \"GROUP\":\n return normalizeGroupNodeAsFrameNode(node);\n default:\n return {\n type: \"UNHANDLED\",\n id: node.id,\n original: node,\n };\n }\n }\n\n /**\n * Pick specific fields from boundVariables\n */\n function normalizeBoundVariables({\n boundVariables,\n }: Pick<FrameNode, \"boundVariables\">): NormalizedIsLayerTrait[\"boundVariables\"] {\n if (!boundVariables) return undefined;\n\n return {\n fills: boundVariables.fills,\n strokes: boundVariables.strokes,\n itemSpacing: boundVariables.itemSpacing,\n counterAxisSpacing: boundVariables.counterAxisSpacing,\n topLeftRadius: boundVariables.topLeftRadius,\n topRightRadius: boundVariables.topRightRadius,\n bottomLeftRadius: boundVariables.bottomLeftRadius,\n bottomRightRadius: boundVariables.bottomRightRadius,\n paddingTop: boundVariables.paddingTop,\n paddingRight: boundVariables.paddingRight,\n paddingBottom: boundVariables.paddingBottom,\n paddingLeft: boundVariables.paddingLeft,\n minWidth: boundVariables.minWidth,\n maxWidth: boundVariables.maxWidth,\n minHeight: boundVariables.minHeight,\n maxHeight: boundVariables.maxHeight,\n fontSize: boundVariables.fontSize,\n fontWeight: boundVariables.fontWeight,\n lineHeight: boundVariables.lineHeight,\n size: {\n x: boundVariables.width,\n y: boundVariables.height,\n },\n };\n }\n\n function normalizeSolidPaint(paint: SolidPaint): NormalizedPaint {\n return {\n type: paint.type,\n color: {\n r: paint.color.r,\n g: paint.color.g,\n b: paint.color.b,\n a: paint.opacity ?? 1,\n },\n visible: paint.visible,\n blendMode: paint.blendMode ?? \"NORMAL\",\n opacity: paint.opacity,\n boundVariables: paint.boundVariables,\n };\n }\n\n function normalizePaint(paint: Paint): NormalizedPaint {\n switch (paint.type) {\n case \"SOLID\":\n return normalizeSolidPaint(paint);\n case \"IMAGE\":\n return {\n type: \"IMAGE\",\n scaleMode: paint.scaleMode === \"CROP\" ? \"STRETCH\" : paint.scaleMode,\n imageTransform: paint.imageTransform,\n scalingFactor: paint.scalingFactor,\n filters: paint.filters,\n rotation: paint.rotation,\n imageRef: paint.imageHash ?? \"\",\n blendMode: paint.blendMode ?? \"NORMAL\",\n visible: paint.visible,\n opacity: paint.opacity,\n };\n case \"GRADIENT_LINEAR\":\n case \"GRADIENT_RADIAL\":\n case \"GRADIENT_ANGULAR\":\n case \"GRADIENT_DIAMOND\":\n return {\n type: paint.type,\n gradientStops: [...paint.gradientStops],\n visible: paint.visible,\n opacity: paint.opacity,\n blendMode: paint.blendMode ?? \"NORMAL\",\n gradientHandlePositions: convertTransformToGradientHandles(paint.gradientTransform),\n };\n default:\n throw new Error(`Unimplemented paint type: ${paint.type}`);\n }\n }\n\n function normalizePaints(fills: readonly Paint[] | PluginAPI[\"mixed\"]): NormalizedPaint[] {\n if (fills === figma.mixed) {\n console.warn(\"Mixed fills are not supported\");\n\n return [];\n }\n\n return fills.map(normalizePaint);\n }\n\n function normalizeRadiusProps(\n node: Pick<\n RectangleNode,\n \"cornerRadius\" | \"topLeftRadius\" | \"topRightRadius\" | \"bottomRightRadius\" | \"bottomLeftRadius\"\n >,\n ): NormalizedCornerTrait {\n return {\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: [\n node.topLeftRadius,\n node.topRightRadius,\n node.bottomRightRadius,\n node.bottomLeftRadius,\n ],\n };\n }\n\n async function normalizeEffectProps(\n node: Pick<RectangleNode, \"effects\" | \"effectStyleId\">,\n ): Promise<NormalizedHasEffectsTrait> {\n const effectStyleKey =\n typeof node.effectStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.effectStyleId))?.key\n : undefined;\n\n const effects = node.effects\n .filter((effect): effect is DropShadowEffect | InnerShadowEffect => {\n if (!effect.visible) return false;\n\n return effect.type === \"DROP_SHADOW\" || effect.type === \"INNER_SHADOW\";\n })\n .map(({ blendMode, visible, ...rest }): NormalizedShadow => rest);\n\n return {\n ...(effectStyleKey ? { effectStyleKey } : {}),\n effects,\n };\n }\n\n async function normalizeShapeProps(\n node: Pick<\n RectangleNode,\n | \"fills\"\n | \"fillStyleId\"\n | \"strokes\"\n | \"strokeWeight\"\n | \"layoutGrow\"\n | \"layoutAlign\"\n | \"layoutSizingHorizontal\"\n | \"layoutSizingVertical\"\n | \"absoluteBoundingBox\"\n | \"relativeTransform\"\n | \"layoutPositioning\"\n | \"minHeight\"\n | \"minWidth\"\n | \"maxHeight\"\n | \"maxWidth\"\n | \"effects\"\n | \"effectStyleId\"\n > &\n Partial<Pick<FrameNode, \"inferredAutoLayout\">>,\n ): Promise<Omit<NormalizedDefaultShapeTrait, keyof NormalizedIsLayerTrait>> {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n // NormalizedHasLayoutTrait\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: await normalizePaints(node.fills),\n fillStyleKey,\n strokes: await normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...(await normalizeEffectProps(node)),\n };\n }\n\n async function normalizeAutolayoutProps(\n node: Omit<FrameNode, \"type\" | \"clone\">,\n ): Promise<NormalizedHasFramePropertiesTrait> {\n return {\n layoutMode: node.inferredAutoLayout?.layoutMode ?? node.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap ?? node.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft ?? node.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight ?? node.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop ?? node.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom ?? node.paddingBottom,\n primaryAxisAlignItems:\n node.inferredAutoLayout?.primaryAxisAlignItems ?? node.primaryAxisAlignItems,\n counterAxisAlignItems:\n node.inferredAutoLayout?.counterAxisAlignItems ?? node.counterAxisAlignItems,\n primaryAxisSizingMode:\n node.inferredAutoLayout?.primaryAxisSizingMode ?? node.primaryAxisSizingMode,\n counterAxisSizingMode:\n node.inferredAutoLayout?.counterAxisSizingMode ?? node.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing ?? node.itemSpacing,\n counterAxisSpacing:\n node.inferredAutoLayout?.counterAxisSpacing ?? node.counterAxisSpacing ?? undefined,\n };\n }\n\n async function normalizeFrameNode(node: FrameNode): Promise<NormalizedFrameNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeRectangleNode(node: RectangleNode): Promise<NormalizedRectangleNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeTextNode(node: TextNode): Promise<NormalizedTextNode> {\n const segments = node.getStyledTextSegments([\n \"fontName\",\n \"fontWeight\",\n \"fontSize\",\n \"letterSpacing\",\n \"lineHeight\",\n \"paragraphSpacing\",\n \"textStyleId\",\n \"fills\",\n \"boundVariables\",\n \"textDecoration\",\n ]);\n const first = segments[0];\n\n const textStyleKey =\n typeof node.textStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.textStyleId))?.key\n : undefined;\n\n const normalizeLetterSpacing = (\n letterSpacing: LetterSpacing,\n fontSize: number,\n ): NormalizedTextSegment[\"style\"][\"letterSpacing\"] => {\n if (letterSpacing.unit === \"PIXELS\") return letterSpacing.value;\n if (letterSpacing.unit === \"PERCENT\") return (fontSize * letterSpacing.value) / 100;\n\n return undefined;\n };\n\n const normalizeLineHeight = (\n lineHeight: LineHeight,\n fontSize: number,\n ): NormalizedTextSegment[\"style\"][\"lineHeight\"] => {\n if (lineHeight.unit === \"PIXELS\") return lineHeight.value;\n if (lineHeight.unit === \"PERCENT\") return (fontSize * lineHeight.value) / 100;\n\n return undefined;\n };\n\n const isItalic = (fontName: FontName): boolean => {\n // {\n // family: \"SF Mono\",\n // style: \"Bold Italic\"\n // }\n return fontName.style.toLowerCase().includes(\"italic\");\n };\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedTypePropertiesTrait\n // NOTE: this normalization is incomplete compared to from-rest.ts normalizer\n style: {\n fontFamily: first.fontName.family,\n fontPostScriptName: null,\n fontStyle: first.fontName.style,\n italic: isItalic(first.fontName),\n fontWeight: first.fontWeight,\n fontSize: first.fontSize,\n textAlignHorizontal: node.textAlignHorizontal,\n textAlignVertical: node.textAlignVertical,\n letterSpacing: normalizeLetterSpacing(first.letterSpacing, first.fontSize),\n paragraphSpacing: first.paragraphSpacing,\n textDecoration: first.textDecoration,\n lineHeightPx: normalizeLineHeight(first.lineHeight, first.fontSize),\n lineHeightUnit:\n first.lineHeight.unit === \"PIXELS\"\n ? \"PIXELS\"\n : first.lineHeight.unit === \"PERCENT\"\n ? \"FONT_SIZE_%\"\n : undefined,\n boundVariables: first.boundVariables,\n maxLines: node.maxLines ?? undefined,\n },\n characters: node.characters,\n textStyleKey,\n segments: segments.map((segment) => ({\n characters: segment.characters,\n start: segment.start,\n end: segment.end,\n style: {\n fontSize: segment.fontSize,\n fontWeight: segment.fontWeight,\n fontFamily: segment.fontName.family,\n italic: isItalic(segment.fontName),\n letterSpacing: normalizeLetterSpacing(segment.letterSpacing, segment.fontSize),\n lineHeight: normalizeLineHeight(segment.lineHeight, segment.fontSize),\n textDecoration: segment.textDecoration,\n },\n })),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeComponentNode(node: ComponentNode): Promise<NormalizedComponentNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeInstanceNode(node: InstanceNode): Promise<NormalizedInstanceNode> {\n const mainComponent = await node.getMainComponentAsync();\n if (!mainComponent) {\n throw new Error(\"Instance node has no main component\");\n }\n\n const componentProperties: NormalizedInstanceNode[\"componentProperties\"] = {};\n\n for (const [key, value] of Object.entries(node.componentProperties)) {\n componentProperties[key] = value;\n\n if (value.type === \"INSTANCE_SWAP\") {\n // unless value.type === \"BOOLEAN\", value.value is string\n const swappedComponent = (await figma.getNodeByIdAsync(\n value.value as string,\n )) as ComponentNode;\n\n if (swappedComponent) {\n componentProperties[key].componentKey = swappedComponent.key;\n\n if (swappedComponent.parent?.type === \"COMPONENT_SET\") {\n componentProperties[key].componentSetKey = swappedComponent.parent.key;\n }\n }\n }\n }\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n\n // NormalizedCornerTrait\n ...normalizeRadiusProps(node),\n\n // NormalizedHasFramePropertiesTrait\n ...(await normalizeAutolayoutProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n\n // NormalizedInstanceNode specific\n componentProperties,\n componentKey: mainComponent.key,\n componentSetKey:\n mainComponent.parent?.type === \"COMPONENT_SET\" ? mainComponent.parent.key : undefined,\n overrides: node.overrides,\n };\n }\n\n async function normalizeVectorNode(node: VectorNode): Promise<NormalizedVectorNode> {\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedCornerTrait\n cornerRadius: node.cornerRadius === figma.mixed ? undefined : node.cornerRadius,\n rectangleCornerRadii: undefined, // VectorNode does not have individual corner radii\n\n // NormalizedHasLayoutTrait, NormalizedHasGeometryTrait, NormalizedHasEffectsTrait\n ...(await normalizeShapeProps(node)),\n };\n }\n\n async function normalizeBooleanOperationNode(\n node: BooleanOperationNode,\n ): Promise<NormalizedBooleanOperationNode> {\n const fillStyleKey =\n typeof node.fillStyleId === \"string\"\n ? (await figma.getStyleByIdAsync(node.fillStyleId))?.key\n : undefined;\n\n return {\n // NormalizedIsLayerTrait\n type: node.type,\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait\n layoutGrow: node.layoutGrow as 0 | 1 | undefined,\n layoutAlign: node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: await normalizePaints(node.fills),\n fillStyleKey,\n strokes: await normalizePaints(node.strokes),\n strokeWeight: node.strokeWeight === figma.mixed ? undefined : node.strokeWeight,\n\n // NormalizedHasEffectsTrait\n ...(await normalizeEffectProps(node)),\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n async function normalizeGroupNodeAsFrameNode(\n node: GroupNode & { inferredAutoLayout?: FrameNode[\"inferredAutoLayout\"] },\n ): Promise<NormalizedFrameNode> {\n return {\n // NormalizedIsLayerTrait\n type: \"FRAME\",\n id: node.id,\n name: node.name,\n boundVariables: normalizeBoundVariables(node),\n\n // NormalizedHasLayoutTrait\n layoutGrow: (node.inferredAutoLayout?.layoutGrow ?? node.layoutGrow) as 0 | 1 | undefined,\n layoutAlign: node.inferredAutoLayout?.layoutAlign ?? node.layoutAlign,\n layoutSizingHorizontal: node.layoutSizingHorizontal,\n layoutSizingVertical: node.layoutSizingVertical,\n absoluteBoundingBox: node.absoluteBoundingBox,\n relativeTransform: node.relativeTransform,\n layoutPositioning: node.layoutPositioning,\n minHeight: node.minHeight ?? undefined,\n minWidth: node.minWidth ?? undefined,\n maxHeight: node.maxHeight ?? undefined,\n maxWidth: node.maxWidth ?? undefined,\n\n // NormalizedHasGeometryTrait\n fills: [],\n fillStyleKey: undefined,\n strokes: [],\n strokeWeight: undefined,\n\n // NormalizedHasEffectsTrait\n effects: [],\n effectStyleKey: undefined,\n\n // NormalizedCornerTrait\n cornerRadius: undefined,\n rectangleCornerRadii: undefined,\n\n // NormalizedHasFramePropertiesTrait\n layoutMode: node.inferredAutoLayout?.layoutMode,\n layoutWrap: node.inferredAutoLayout?.layoutWrap,\n paddingLeft: node.inferredAutoLayout?.paddingLeft,\n paddingRight: node.inferredAutoLayout?.paddingRight,\n paddingTop: node.inferredAutoLayout?.paddingTop,\n paddingBottom: node.inferredAutoLayout?.paddingBottom,\n primaryAxisAlignItems: node.inferredAutoLayout?.primaryAxisAlignItems,\n counterAxisAlignItems: node.inferredAutoLayout?.counterAxisAlignItems,\n primaryAxisSizingMode: node.inferredAutoLayout?.primaryAxisSizingMode,\n counterAxisSizingMode: node.inferredAutoLayout?.counterAxisSizingMode,\n itemSpacing: node.inferredAutoLayout?.itemSpacing,\n counterAxisSpacing: node.inferredAutoLayout?.counterAxisSpacing ?? undefined,\n\n // NormalizedHasChildrenTrait\n children: await normalizeNodes(node.children),\n };\n }\n\n return normalizeNode;\n}\n","import type { ComponentPropertyDefinition } from \"@/codegen\";\n\nexport interface ComponentMetadata {\n name: string;\n key: string;\n componentPropertyDefinitions: Record<string, ComponentPropertyDefinition>;\n}\n","export interface IconData {\n name: string;\n type: \"monochrome\" | \"multicolor\";\n weight?: string;\n}\n","import type {\n LocalVariable,\n LocalVariableCollection,\n VariableAlias,\n VariableResolvedDataType,\n} from \"@figma/rest-api-spec\";\n\nexport type Variable = LocalVariable;\n\nexport type VariableCollection = LocalVariableCollection;\n\nexport type VariableType = VariableResolvedDataType;\n\nexport type VariableValue = Variable[\"valuesByMode\"][string];\n\nexport type VariableValueResolved = Exclude<VariableValue, VariableAlias>;\n\nexport type { VariableScope } from \"@figma/rest-api-spec\";\n","import type { Variable, VariableCollection } from \"./variable.interface\";\n\nexport interface VariableRepository {\n getVariableList(): Variable[];\n getVariableCollectionList(): VariableCollection[];\n findVariableByKey(key: string): Variable | undefined;\n findVariableById(id: string): Variable | undefined;\n findVariableByName(name: string): Variable | undefined;\n findVariableCollectionByKey(key: string): VariableCollection | undefined;\n findVariableCollectionById(id: string): VariableCollection | undefined;\n}\n\nexport function createStaticVariableRepository({\n variables,\n variableCollections,\n}: {\n variables: Record<string, Variable>;\n variableCollections: Record<string, VariableCollection>;\n}): VariableRepository {\n const variablesKeyMap = new Map<string, Variable>();\n const variablesIdMap = new Map<string, Variable>();\n const variablesNameMap = new Map<string, Variable>();\n const variableCollectionsKeyMap = new Map<string, VariableCollection>();\n const variableCollectionsIdMap = new Map<string, VariableCollection>();\n\n for (const variable of Object.values(variables)) {\n if (variable.remote) {\n continue;\n }\n\n variablesKeyMap.set(variable.key, variable);\n variablesIdMap.set(variable.id, variable);\n variablesNameMap.set(variable.name, variable);\n }\n\n for (const variableCollection of Object.values(variableCollections)) {\n if (variableCollection.remote) {\n continue;\n }\n\n variableCollectionsKeyMap.set(variableCollection.key, variableCollection);\n variableCollectionsIdMap.set(variableCollection.id, variableCollection);\n }\n\n const variablesList = [...variablesKeyMap.values()];\n const variableCollectionsList = [...variableCollectionsKeyMap.values()];\n\n return {\n getVariableList: () => variablesList,\n getVariableCollectionList: () => variableCollectionsList,\n findVariableByName: (name: string) => variablesNameMap.get(name),\n findVariableByKey: (key: string) => variablesKeyMap.get(key),\n findVariableById: (id: string) => variablesIdMap.get(id),\n findVariableCollectionByKey: (key: string) => variableCollectionsKeyMap.get(key),\n findVariableCollectionById: (id: string) => variableCollectionsIdMap.get(id),\n };\n}\n","import type { Style as FigmaStyle, StyleType as FigmaStyleType } from \"@figma/rest-api-spec\";\n\nexport type Style = FigmaStyle;\n\nexport type StyleType = FigmaStyleType;\n","import type { Style } from \"./style.interface\";\n\nexport interface StyleRepository {\n getAll(): Style[];\n getTextStyles(): Style[];\n getColorStyles(): Style[];\n findOneByKey(key: string): Style | undefined;\n findOneByName(name: string): Style | undefined;\n}\n\nexport function createStaticStyleRepository(styles: Style[]): StyleRepository {\n const stylesMap = new Map<string, Style>();\n const stylesNameMap = new Map<string, Style>();\n\n for (const style of styles) {\n stylesMap.set(style.key, style);\n stylesNameMap.set(style.name, style);\n }\n\n return {\n getAll: () => styles,\n getTextStyles: () => styles.filter((style) => style.styleType === \"TEXT\"),\n getColorStyles: () => styles.filter((style) => style.styleType === \"FILL\"),\n findOneByKey: (key) => stylesMap.get(key),\n findOneByName: (name) => stylesNameMap.get(name),\n };\n}\n","import type { IconData } from \"./icon.interface\";\n\nexport interface IconRepository {\n getOne(key: string): IconData;\n}\n\nexport function createStaticIconRepository(iconRecord: Record<string, IconData>) {\n return {\n getOne: (key: string) => iconRecord[key],\n };\n}\n","import type { IconData } from \"./icon.interface\";\nimport type { IconRepository } from \"./icon.repository\";\n\nexport interface IconService {\n isAvailable: (componentKey: string) => boolean;\n getOne: (componentKey: string) => IconData;\n}\n\nexport function createIconService({\n iconRepository,\n}: {\n iconRepository: IconRepository;\n}): IconService {\n function isAvailable(componentKey: string) {\n return iconRepository.getOne(componentKey) !== undefined;\n }\n\n function getOne(componentKey: string) {\n return iconRepository.getOne(componentKey);\n }\n\n return {\n isAvailable,\n getOne,\n };\n}\n","import type { StyleRepository } from \"./style.repository\";\n\nexport interface StyleService {\n getSlug: (id: string) => string[] | undefined;\n}\n\n// TODO: inferStyleName 추가해야 함, rest api에서 style value가 제공되지 않고 있어 보류\nexport function createStyleService({\n styleRepository,\n}: {\n styleRepository: StyleRepository;\n}): StyleService {\n function getName(id: string) {\n const style = styleRepository.findOneByKey(id);\n\n if (!style) {\n return undefined;\n }\n\n return style.name;\n }\n\n function getSlug(id: string): string[] | undefined {\n const name = getName(id);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n return {\n getSlug,\n };\n}\n","import {\n isIdenticalVariableValue,\n isInsideScope,\n isVariableAlias,\n sanitizeVariableId,\n} from \"@/utils/figma-variable\";\nimport type { Variable, VariableScope, VariableValueResolved } from \"./variable.interface\";\nimport type { VariableRepository } from \"./variable.repository\";\n\nexport interface VariableService {\n getSlug: (id: string) => string[] | undefined;\n resolveValue: (variable: Variable, mode: string) => VariableValueResolved;\n infer: (value: VariableValueResolved, scope: VariableScope) => Variable | undefined;\n}\n\nexport interface VariableServiceDeps {\n variableRepository: VariableRepository;\n inferCompareFunction: (a: Variable, b: Variable) => number;\n}\n\nexport function createVariableService({\n variableRepository,\n inferCompareFunction,\n}: VariableServiceDeps): VariableService {\n const variables = variableRepository.getVariableList();\n\n // private\n function getName(key: string) {\n const sanitizedId = sanitizeVariableId(key);\n const variable = variableRepository.findVariableByKey(sanitizedId);\n\n if (!variable) {\n return undefined;\n }\n\n return variable.name;\n }\n\n function getDefaultModeId(variable: Variable) {\n const variableCollection = variableRepository.findVariableCollectionById(\n variable.variableCollectionId,\n );\n\n if (!variableCollection) {\n // Variable collection not found: ${variable.variableCollectionId}, falling back to variable.valuesByMode key\n return Object.keys(variable.valuesByMode)[0]!;\n }\n\n return variableCollection.defaultModeId;\n }\n\n // public\n function getSlug(key: string): string[] | undefined {\n const name = getName(key);\n\n if (!name) {\n return undefined;\n }\n\n return name.split(\"/\");\n }\n\n function resolveValue(variable: Variable, mode: string): VariableValueResolved {\n const value = variable.valuesByMode[mode];\n\n if (value === undefined) {\n throw new Error(`Variable value not found: ${variable.id} ${mode}`);\n }\n\n if (isVariableAlias(value)) {\n const resolvedVariable = variableRepository.findVariableById(value.id);\n\n if (!resolvedVariable) {\n throw new Error(`Variable not found: ${value.id}`);\n }\n\n return resolveValue(resolvedVariable, mode);\n }\n\n return value;\n }\n\n function infer(value: VariableValueResolved, scope: VariableScope) {\n // NOTE: We assume that the variable is in the default mode or value is equal between all modes for simplicity.\n const inferredVariables = variables.filter(\n (variable) =>\n isInsideScope(variable, scope) &&\n isIdenticalVariableValue(resolveValue(variable, getDefaultModeId(variable)), value),\n );\n\n const sortedVariables = inferredVariables.sort(inferCompareFunction);\n\n return sortedVariables[0];\n }\n\n return {\n getSlug,\n resolveValue,\n infer,\n };\n}\n","import type { ComponentMetadata } from \"./component.interface\";\n\nexport interface ComponentRepository {\n getOne(key: string): ComponentMetadata | undefined;\n}\n\nexport function createStaticComponentRepository(data: Record<string, ComponentMetadata>) {\n const componentRecord: Record<string, ComponentMetadata> = {};\n Object.values(data).forEach((component) => {\n componentRecord[component.key] = component;\n });\n\n return {\n getOne: (key: string) => componentRecord[key],\n };\n}\n","import { createStaticIconRepository } from \"./icon.repository\";\nimport { FIGMA_ICONS } from \"./data/__generated__/icons\";\nimport { FIGMA_STYLES } from \"./data/__generated__/styles\";\nimport { FIGMA_VARIABLE_COLLECTIONS } from \"./data/__generated__/variable-collections\";\nimport { FIGMA_VARIABLES } from \"./data/__generated__/variables\";\nimport * as FIGMA_COMPONENTS from \"./data/__generated__/component-sets\";\nimport { createStaticStyleRepository } from \"./style.repository\";\nimport { createStaticVariableRepository } from \"./variable.repository\";\nimport { createStaticComponentRepository } from \"./component.repository\";\n\nexport * from \"./icon.interface\";\nexport * from \"./icon.repository\";\nexport * from \"./icon.service\";\nexport * from \"./style.interface\";\nexport * from \"./style.repository\";\nexport * from \"./style.service\";\nexport * from \"./variable.interface\";\nexport * from \"./variable.repository\";\nexport * from \"./variable.service\";\nexport * from \"./component.interface\";\nexport * from \"./component.repository\";\n\nexport const styleRepository = createStaticStyleRepository(FIGMA_STYLES);\nexport const variableRepository = createStaticVariableRepository({\n variables: FIGMA_VARIABLES,\n variableCollections: FIGMA_VARIABLE_COLLECTIONS,\n});\nexport const iconRepository = createStaticIconRepository(FIGMA_ICONS);\nexport const componentRepository = createStaticComponentRepository(FIGMA_COMPONENTS);\n\nexport function getFigmaVariableKey(name: string) {\n return variableRepository.findVariableByName(name)?.key;\n}\n\nexport function getFigmaStyleKey(name: string) {\n return styleRepository.findOneByName(name)?.key;\n}\n\nexport function getFigmaColorVariableNames(scopes: Array<\"fg\" | \"bg\" | \"stroke\" | \"palette\">) {\n const variables = variableRepository.getVariableList();\n return variables\n .filter((variable) =>\n scopes.includes(variable.name.split(\"/\")[0] as \"fg\" | \"bg\" | \"stroke\" | \"palette\"),\n )\n .map((variable) => variable.name);\n}\n"],"names":[],"mappings":";;;;;;AACO;AACP;AACA;AACO;AACA;AACP;AACA;AACO;AACA;AACA;AACA;AACP;AACA;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACO;;ACjFP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACzBP;AACA;AACA;AACA;AACA;;AAEO;;ACLA;AACP;AACA;AACA;AACA;;ACLO;AACP;AACA;AACA;AACA;;ACHO;AACA;AACA;AACA;AACA;;ACJA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACZO;AACA;;ACDA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;;ACPA;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACO;AACP;AACA;;ACPO;AACP;AACA;AACO;AACP;AACA;;ACJO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;;ACVA;AACP;AACA;AACO;AACP;AACA;;ACKO;AACA;AACA;AACP;AACA;AACO;AACP;AACA;AACO;AACA;AACA;;;"}