@design-embed/plugin-figma-html 0.1.0 → 1.0.0
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.
- package/LICENSE +1 -1
- package/dist/compilers/compilerUtils.d.mts +11 -0
- package/dist/compilers/compilerUtils.mjs +119 -0
- package/dist/compilers/htmlCompiler.d.mts +6 -0
- package/dist/compilers/htmlCompiler.mjs +31 -0
- package/dist/compilers/index.d.mts +11 -0
- package/dist/compilers/index.mjs +17 -0
- package/dist/compilers/index.test.d.mts +1 -0
- package/dist/compilers/index.test.mjs +45 -0
- package/dist/compilers/reactCompiler.d.mts +6 -0
- package/dist/compilers/reactCompiler.mjs +47 -0
- package/dist/compilers/vanjsCompiler.d.mts +6 -0
- package/dist/compilers/vanjsCompiler.mjs +47 -0
- package/dist/design-embed/src/core/diagnostics/diagnostic.d.mts +16 -0
- package/dist/design-embed/src/core/nodes.d.mts +14 -0
- package/dist/design-embed/src/core/plugins/pluginApi.d.mts +34 -0
- package/dist/external/figmaApi.d.mts +17 -0
- package/dist/external/figmaApi.mjs +55 -0
- package/dist/external/figmaApi.test.d.mts +1 -0
- package/dist/external/figmaApi.test.mjs +101 -0
- package/dist/external/imageDownloader.d.mts +17 -0
- package/dist/external/imageDownloader.mjs +66 -0
- package/dist/external/imageDownloader.test.d.mts +1 -0
- package/dist/external/imageDownloader.test.mjs +42 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +7 -0
- package/dist/plugin.d.mts +16 -0
- package/dist/plugin.mjs +43 -0
- package/dist/types.d.mts +84 -0
- package/package.json +11 -9
- package/src/plugin.ts +2 -3
- package/dist/compilers/compilerUtils.js +0 -182
- package/dist/compilers/htmlCompiler.js +0 -35
- package/dist/compilers/index.js +0 -17
- package/dist/compilers/reactCompiler.js +0 -58
- package/dist/compilers/vanjsCompiler.js +0 -55
- package/dist/external/figmaApi.js +0 -74
- package/dist/external/imageDownloader.js +0 -82
- package/dist/index.js +0 -3
- package/dist/plugin.js +0 -56
- package/node_modules/@design-embed/config/README.md +0 -5
- package/node_modules/@design-embed/config/dist/index.js +0 -283
- package/node_modules/@design-embed/config/package.json +0 -19
- package/node_modules/@design-embed/config/src/index.ts +0 -518
- package/node_modules/@design-embed/core/README.md +0 -5
- package/node_modules/@design-embed/core/dist/diagnostics/diagnostic.js +0 -3
- package/node_modules/@design-embed/core/dist/diagnostics/jsonDiagnostic.js +0 -35
- package/node_modules/@design-embed/core/dist/index.js +0 -351
- package/node_modules/@design-embed/core/dist/pipeline/checkMode.js +0 -29
- package/node_modules/@design-embed/core/dist/plugins/pluginApi.js +0 -1
- package/node_modules/@design-embed/core/dist/plugins/pluginRegistry.js +0 -25
- package/node_modules/@design-embed/core/package.json +0 -19
- package/node_modules/@design-embed/core/src/diagnostics/diagnostic.ts +0 -18
- package/node_modules/@design-embed/core/src/diagnostics/jsonDiagnostic.ts +0 -51
- package/node_modules/@design-embed/core/src/index.ts +0 -591
- package/node_modules/@design-embed/core/src/pipeline/checkMode.ts +0 -46
- package/node_modules/@design-embed/core/src/plugins/pluginApi.ts +0 -78
- package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +0 -37
- /package/dist/{types.js → types.mjs} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { join, posix } from "node:path";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
//#region packages/plugin-figma-html/src/external/imageDownloader.ts
|
|
4
|
+
async function downloadFigmaImageFills(rootNode, outDir, options = {}) {
|
|
5
|
+
const targets = collectImageFillTargets(rootNode);
|
|
6
|
+
const uniqueTargets = Array.from(new Map(targets.map((target) => [target.imageRef, target])).values());
|
|
7
|
+
if (uniqueTargets.length === 0) return [];
|
|
8
|
+
mkdirSync(outDir, { recursive: true });
|
|
9
|
+
const downloadedImages = await Promise.all(uniqueTargets.map((target) => downloadImageFill(target, outDir, options)));
|
|
10
|
+
const publicPathByRef = new Map(downloadedImages.map((image) => [image.imageRef, image.publicPath]));
|
|
11
|
+
for (const target of targets) {
|
|
12
|
+
const fill = target.fills[target.fillIndex];
|
|
13
|
+
if (fill) fill.imageLocalPath = publicPathByRef.get(target.imageRef);
|
|
14
|
+
}
|
|
15
|
+
return downloadedImages;
|
|
16
|
+
}
|
|
17
|
+
function collectImageFillTargets(node) {
|
|
18
|
+
const targets = [];
|
|
19
|
+
node.fills?.forEach((fill, fillIndex, fills) => {
|
|
20
|
+
if (fill.type === "IMAGE" && fill.imageRef && fill.imageUrl) targets.push({
|
|
21
|
+
imageRef: fill.imageRef,
|
|
22
|
+
imageUrl: fill.imageUrl,
|
|
23
|
+
fills,
|
|
24
|
+
fillIndex
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
for (const child of node.children || []) targets.push(...collectImageFillTargets(child));
|
|
28
|
+
return targets;
|
|
29
|
+
}
|
|
30
|
+
async function downloadImageFill(target, outDir, options) {
|
|
31
|
+
const response = await (options.fetcher ?? fetch)(target.imageUrl);
|
|
32
|
+
if (!response.ok) throw new Error(`Figma image download failed for ${target.imageRef}: ${response.status} ${response.statusText}`);
|
|
33
|
+
const extension = extensionFromResponse(response, target.imageUrl);
|
|
34
|
+
const filename = `${sanitizeFilename(target.imageRef)}.${extension}`;
|
|
35
|
+
const filePath = join(outDir, filename);
|
|
36
|
+
const publicPath = posix.join(options.publicPath || outDir, filename);
|
|
37
|
+
writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
|
|
38
|
+
return {
|
|
39
|
+
imageRef: target.imageRef,
|
|
40
|
+
sourceUrl: target.imageUrl,
|
|
41
|
+
filePath,
|
|
42
|
+
publicPath
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function extensionFromResponse(response, url) {
|
|
46
|
+
switch (response.headers.get("content-type")?.split(";")[0]) {
|
|
47
|
+
case "image/jpeg": return "jpg";
|
|
48
|
+
case "image/png": return "png";
|
|
49
|
+
case "image/svg+xml": return "svg";
|
|
50
|
+
case "image/webp": return "webp";
|
|
51
|
+
case "image/gif": return "gif";
|
|
52
|
+
}
|
|
53
|
+
return safeUrlPathname(url).match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || "img";
|
|
54
|
+
}
|
|
55
|
+
function safeUrlPathname(url) {
|
|
56
|
+
try {
|
|
57
|
+
return new URL(url).pathname;
|
|
58
|
+
} catch {
|
|
59
|
+
return url;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function sanitizeFilename(value) {
|
|
63
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { downloadFigmaImageFills };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getNodeStyles } from "../compilers/compilerUtils.mjs";
|
|
2
|
+
import { downloadFigmaImageFills } from "./imageDownloader.mjs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { describe, test } from "node:test";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
//#region packages/plugin-figma-html/src/external/imageDownloader.test.ts
|
|
9
|
+
describe("downloadFigmaImageFills", () => {
|
|
10
|
+
test("downloads image fills and attaches local paths for compilers", async () => {
|
|
11
|
+
const outDir = mkdtempSync(join(tmpdir(), "figma-images-"));
|
|
12
|
+
const node = {
|
|
13
|
+
name: "Hero",
|
|
14
|
+
type: "RECTANGLE",
|
|
15
|
+
fills: [{
|
|
16
|
+
type: "IMAGE",
|
|
17
|
+
scaleMode: "FILL",
|
|
18
|
+
imageRef: "image/ref",
|
|
19
|
+
imageUrl: "https://example.com/image.png"
|
|
20
|
+
}]
|
|
21
|
+
};
|
|
22
|
+
const fetcher = async () => new Response("image-bytes", { headers: { "content-type": "image/png" } });
|
|
23
|
+
try {
|
|
24
|
+
const downloaded = await downloadFigmaImageFills(node, outDir, {
|
|
25
|
+
fetcher,
|
|
26
|
+
publicPath: "assets"
|
|
27
|
+
});
|
|
28
|
+
assert.equal(downloaded.length, 1);
|
|
29
|
+
assert.equal(downloaded[0]?.publicPath, "assets/image_ref.png");
|
|
30
|
+
assert.equal(readFileSync(downloaded[0]?.filePath || "", "utf-8"), "image-bytes");
|
|
31
|
+
assert.equal(node.fills?.[0]?.imageLocalPath, "assets/image_ref.png");
|
|
32
|
+
assert.equal(getNodeStyles(node).backgroundImage, "url(\"assets/image_ref.png\")");
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(outDir, {
|
|
35
|
+
recursive: true,
|
|
36
|
+
force: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//#endregion
|
|
42
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CompilerMode, ExtractedParams, FigmaCompiler, FigmaNode, GeneratedFile } from "./types.mjs";
|
|
2
|
+
import { compileHtml } from "./compilers/htmlCompiler.mjs";
|
|
3
|
+
import { compileReact } from "./compilers/reactCompiler.mjs";
|
|
4
|
+
import { compileVanjs } from "./compilers/vanjsCompiler.mjs";
|
|
5
|
+
import { getCompiler, isCompilerMode } from "./compilers/index.mjs";
|
|
6
|
+
import { FigmaApiResponse, FigmaClientOptions, FigmaFetcher, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode } from "./external/figmaApi.mjs";
|
|
7
|
+
import { FigmaHtmlPlugin, FigmaHtmlPluginOptions } from "./plugin.mjs";
|
|
8
|
+
export { type CompilerMode, type ExtractedParams, type FigmaApiResponse, type FigmaClientOptions, type FigmaCompiler, type FigmaFetcher, FigmaHtmlPlugin, type FigmaHtmlPluginOptions, type FigmaNode, type GeneratedFile, compileHtml, compileReact, compileVanjs, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode, getCompiler, isCompilerMode };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { compileHtml } from "./compilers/htmlCompiler.mjs";
|
|
2
|
+
import { compileReact } from "./compilers/reactCompiler.mjs";
|
|
3
|
+
import { compileVanjs } from "./compilers/vanjsCompiler.mjs";
|
|
4
|
+
import { getCompiler, isCompilerMode } from "./compilers/index.mjs";
|
|
5
|
+
import { extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode } from "./external/figmaApi.mjs";
|
|
6
|
+
import { FigmaHtmlPlugin } from "./plugin.mjs";
|
|
7
|
+
export { FigmaHtmlPlugin, compileHtml, compileReact, compileVanjs, extractParamsFromURL, fetchFigmaApiResponse, fetchFigmaNode, getCompiler, isCompilerMode };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SourcePlugin, SourcePluginInput, SourcePluginResult } from "./design-embed/src/core/plugins/pluginApi.mjs";
|
|
2
|
+
|
|
3
|
+
//#region packages/plugin-figma-html/src/plugin.d.ts
|
|
4
|
+
interface FigmaHtmlPluginOptions {
|
|
5
|
+
url: string;
|
|
6
|
+
token?: string;
|
|
7
|
+
assetsDir?: string;
|
|
8
|
+
}
|
|
9
|
+
declare class FigmaHtmlPlugin implements SourcePlugin {
|
|
10
|
+
private readonly options;
|
|
11
|
+
readonly name = "figma-html";
|
|
12
|
+
constructor(options: FigmaHtmlPluginOptions);
|
|
13
|
+
run(input: SourcePluginInput): Promise<SourcePluginResult>;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { FigmaHtmlPlugin, FigmaHtmlPluginOptions };
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { compileHtml } from "./compilers/htmlCompiler.mjs";
|
|
2
|
+
import { extractParamsFromURL, fetchFigmaNode } from "./external/figmaApi.mjs";
|
|
3
|
+
import { downloadFigmaImageFills } from "./external/imageDownloader.mjs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region packages/plugin-figma-html/src/plugin.ts
|
|
6
|
+
var FigmaHtmlPlugin = class {
|
|
7
|
+
options;
|
|
8
|
+
name = "figma-html";
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
async run(input) {
|
|
13
|
+
const { url, token: optionsToken, assetsDir = "assets" } = this.options;
|
|
14
|
+
const token = optionsToken ?? process.env.FIGMA_TOKEN;
|
|
15
|
+
if (!token) return { diagnostics: [{
|
|
16
|
+
code: "FIGMA_TOKEN_REQUIRED",
|
|
17
|
+
message: "figma-html requires a Figma token. Pass token in the FigmaHtmlPlugin constructor or set the FIGMA_TOKEN environment variable.",
|
|
18
|
+
severity: "error"
|
|
19
|
+
}] };
|
|
20
|
+
try {
|
|
21
|
+
const { fileKey, nodeId } = extractParamsFromURL(url);
|
|
22
|
+
const rootNode = await fetchFigmaNode(fileKey, nodeId, { token });
|
|
23
|
+
const downloadedImages = await downloadFigmaImageFills(rootNode, join(input.cwd, assetsDir), { publicPath: assetsDir });
|
|
24
|
+
const [htmlFile] = compileHtml(rootNode);
|
|
25
|
+
return {
|
|
26
|
+
html: htmlFile?.contents,
|
|
27
|
+
diagnostics: downloadedImages.length > 0 ? [{
|
|
28
|
+
code: "FIGMA_ASSETS_DOWNLOADED",
|
|
29
|
+
message: `Downloaded ${downloadedImages.length} image asset(s).`,
|
|
30
|
+
severity: "info"
|
|
31
|
+
}] : []
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { diagnostics: [{
|
|
35
|
+
code: "FIGMA_HTML_FAILED",
|
|
36
|
+
message: error instanceof Error ? error.message : String(error),
|
|
37
|
+
severity: "error"
|
|
38
|
+
}] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { FigmaHtmlPlugin };
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region packages/plugin-figma-html/src/types.d.ts
|
|
2
|
+
interface ExtractedParams {
|
|
3
|
+
fileKey: string;
|
|
4
|
+
nodeId: string | null;
|
|
5
|
+
}
|
|
6
|
+
interface FigmaNode {
|
|
7
|
+
id?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
visible?: boolean;
|
|
11
|
+
characters?: string;
|
|
12
|
+
children?: FigmaNode[];
|
|
13
|
+
layoutMode?: "NONE" | "HORIZONTAL" | "VERTICAL" | "GRID" | string;
|
|
14
|
+
layoutSizingHorizontal?: "FIXED" | "HUG" | "FILL" | string;
|
|
15
|
+
layoutSizingVertical?: "FIXED" | "HUG" | "FILL" | string;
|
|
16
|
+
layoutWrap?: "NO_WRAP" | "WRAP" | string;
|
|
17
|
+
primaryAxisSizingMode?: "FIXED" | "AUTO" | string;
|
|
18
|
+
counterAxisSizingMode?: "FIXED" | "AUTO" | string;
|
|
19
|
+
primaryAxisAlignItems?: "MIN" | "CENTER" | "MAX" | "SPACE_BETWEEN" | string;
|
|
20
|
+
counterAxisAlignItems?: "MIN" | "CENTER" | "MAX" | "BASELINE" | string;
|
|
21
|
+
layoutPositioning?: "ABSOLUTE" | "AUTO" | string;
|
|
22
|
+
layoutAlign?: "INHERIT" | "STRETCH" | "MIN" | "CENTER" | "MAX" | string;
|
|
23
|
+
layoutGrow?: number;
|
|
24
|
+
itemSpacing?: number;
|
|
25
|
+
counterAxisSpacing?: number;
|
|
26
|
+
gridRowGap?: number;
|
|
27
|
+
gridColumnGap?: number;
|
|
28
|
+
gridColumnsSizing?: string;
|
|
29
|
+
gridRowsSizing?: string;
|
|
30
|
+
gridColumnSpan?: number;
|
|
31
|
+
gridRowSpan?: number;
|
|
32
|
+
paddingTop?: number;
|
|
33
|
+
paddingBottom?: number;
|
|
34
|
+
paddingLeft?: number;
|
|
35
|
+
paddingRight?: number;
|
|
36
|
+
absoluteBoundingBox?: {
|
|
37
|
+
x?: number;
|
|
38
|
+
y?: number;
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
};
|
|
42
|
+
fills?: Array<{
|
|
43
|
+
type?: string;
|
|
44
|
+
opacity?: number;
|
|
45
|
+
scaleMode?: string;
|
|
46
|
+
imageRef?: string;
|
|
47
|
+
imageUrl?: string;
|
|
48
|
+
imageLocalPath?: string;
|
|
49
|
+
color?: {
|
|
50
|
+
r: number;
|
|
51
|
+
g: number;
|
|
52
|
+
b: number;
|
|
53
|
+
};
|
|
54
|
+
}>;
|
|
55
|
+
cornerRadius?: number;
|
|
56
|
+
rectangleCornerRadii?: number[];
|
|
57
|
+
strokes?: Array<{
|
|
58
|
+
type?: string;
|
|
59
|
+
opacity?: number;
|
|
60
|
+
color?: {
|
|
61
|
+
r: number;
|
|
62
|
+
g: number;
|
|
63
|
+
b: number;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
strokeWeight?: number;
|
|
67
|
+
strokeAlign?: "INSIDE" | "OUTSIDE" | "CENTER" | string;
|
|
68
|
+
opacity?: number;
|
|
69
|
+
clipsContent?: boolean;
|
|
70
|
+
style?: {
|
|
71
|
+
fontSize?: number;
|
|
72
|
+
fontWeight?: number | string;
|
|
73
|
+
fontFamily?: string;
|
|
74
|
+
lineHeightPx?: number;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
interface GeneratedFile {
|
|
78
|
+
path: string;
|
|
79
|
+
contents: string;
|
|
80
|
+
}
|
|
81
|
+
type CompilerMode = "react" | "html" | "vanjs";
|
|
82
|
+
type FigmaCompiler = (node: FigmaNode) => GeneratedFile[];
|
|
83
|
+
//#endregion
|
|
84
|
+
export { CompilerMode, ExtractedParams, FigmaCompiler, FigmaNode, GeneratedFile };
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design-embed/plugin-figma-html",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
|
-
"access": "public"
|
|
7
|
+
"access": "public",
|
|
8
|
+
"provenance": true
|
|
8
9
|
},
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
@@ -19,12 +20,13 @@
|
|
|
19
20
|
"!src/**/*.test.ts",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"@design-embed/core": "0.1.0"
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"design-embed": "*"
|
|
25
25
|
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"design-embed": "0.2.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --conditions=development --test src/**/*.test.ts"
|
|
31
|
+
}
|
|
30
32
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import type { PluginDefinition } from "@design-embed/config";
|
|
3
2
|
import type {
|
|
4
3
|
SourcePlugin,
|
|
5
4
|
SourcePluginInput,
|
|
6
5
|
SourcePluginResult,
|
|
7
|
-
} from "
|
|
6
|
+
} from "design-embed";
|
|
8
7
|
import { compileHtml } from "./compilers/index.ts";
|
|
9
8
|
import { extractParamsFromURL, fetchFigmaNode } from "./external/figmaApi.ts";
|
|
10
9
|
import { downloadFigmaImageFills } from "./external/imageDownloader.ts";
|
|
@@ -15,7 +14,7 @@ export interface FigmaHtmlPluginOptions {
|
|
|
15
14
|
assetsDir?: string;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export class FigmaHtmlPlugin implements
|
|
17
|
+
export class FigmaHtmlPlugin implements SourcePlugin {
|
|
19
18
|
readonly name = "figma-html";
|
|
20
19
|
|
|
21
20
|
constructor(private readonly options: FigmaHtmlPluginOptions) {}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
export function toComponentName(name, fallback = "FigmaExport") {
|
|
2
|
-
const cleaned = (name || "").replace(/[^a-zA-Z0-9가-힣]/g, "");
|
|
3
|
-
if (!cleaned)
|
|
4
|
-
return fallback;
|
|
5
|
-
return /^[0-9]/.test(cleaned) ? `${fallback}${cleaned}` : cleaned;
|
|
6
|
-
}
|
|
7
|
-
export function escapeHtml(value) {
|
|
8
|
-
return (value || "")
|
|
9
|
-
.replace(/&/g, "&")
|
|
10
|
-
.replace(/</g, "<")
|
|
11
|
-
.replace(/>/g, ">")
|
|
12
|
-
.replace(/"/g, """)
|
|
13
|
-
.replace(/'/g, "'");
|
|
14
|
-
}
|
|
15
|
-
export function escapeJsString(value) {
|
|
16
|
-
return JSON.stringify(value || "");
|
|
17
|
-
}
|
|
18
|
-
export function getNodeStyles(node, parent) {
|
|
19
|
-
const styles = {};
|
|
20
|
-
const bounds = node.absoluteBoundingBox;
|
|
21
|
-
const parentBounds = parent?.absoluteBoundingBox;
|
|
22
|
-
const parentUsesLayout = Boolean(parent?.layoutMode && parent.layoutMode !== "NONE");
|
|
23
|
-
const isAbsoluteChild = Boolean(parent && (!parentUsesLayout || node.layoutPositioning === "ABSOLUTE"));
|
|
24
|
-
if (isAbsoluteChild && bounds && parentBounds) {
|
|
25
|
-
styles.position = "absolute";
|
|
26
|
-
styles.left = `${Math.round((bounds.x || 0) - (parentBounds.x || 0))}px`;
|
|
27
|
-
styles.top = `${Math.round((bounds.y || 0) - (parentBounds.y || 0))}px`;
|
|
28
|
-
}
|
|
29
|
-
else if (node.children?.length || node.layoutMode === "NONE") {
|
|
30
|
-
styles.position = "relative";
|
|
31
|
-
}
|
|
32
|
-
if (bounds && node.layoutSizingHorizontal !== "HUG") {
|
|
33
|
-
styles.width = `${Math.round(bounds.width || 0)}px`;
|
|
34
|
-
}
|
|
35
|
-
if (bounds && node.layoutSizingVertical !== "HUG") {
|
|
36
|
-
styles.height = `${Math.round(bounds.height || 0)}px`;
|
|
37
|
-
}
|
|
38
|
-
if (node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL") {
|
|
39
|
-
styles.display = "flex";
|
|
40
|
-
styles.flexDirection = node.layoutMode === "HORIZONTAL" ? "row" : "column";
|
|
41
|
-
styles.boxSizing = "border-box";
|
|
42
|
-
if (node.layoutWrap === "WRAP")
|
|
43
|
-
styles.flexWrap = "wrap";
|
|
44
|
-
if (node.itemSpacing !== undefined)
|
|
45
|
-
styles.gap = `${Math.round(node.itemSpacing)}px`;
|
|
46
|
-
if (node.counterAxisSpacing !== undefined && node.layoutWrap === "WRAP") {
|
|
47
|
-
styles.rowGap = `${Math.round(node.counterAxisSpacing)}px`;
|
|
48
|
-
}
|
|
49
|
-
styles.justifyContent = mapPrimaryAxisAlignment(node.primaryAxisAlignItems);
|
|
50
|
-
styles.alignItems = mapCounterAxisAlignment(node.counterAxisAlignItems);
|
|
51
|
-
}
|
|
52
|
-
else if (node.layoutMode === "GRID") {
|
|
53
|
-
styles.display = "grid";
|
|
54
|
-
styles.boxSizing = "border-box";
|
|
55
|
-
if (node.gridColumnsSizing)
|
|
56
|
-
styles.gridTemplateColumns = node.gridColumnsSizing;
|
|
57
|
-
if (node.gridRowsSizing)
|
|
58
|
-
styles.gridTemplateRows = node.gridRowsSizing;
|
|
59
|
-
if (node.gridColumnGap !== undefined)
|
|
60
|
-
styles.columnGap = `${Math.round(node.gridColumnGap)}px`;
|
|
61
|
-
if (node.gridRowGap !== undefined)
|
|
62
|
-
styles.rowGap = `${Math.round(node.gridRowGap)}px`;
|
|
63
|
-
}
|
|
64
|
-
if (node.layoutSizingHorizontal === "FILL" || node.layoutGrow === 1) {
|
|
65
|
-
styles.flex = 1;
|
|
66
|
-
styles.width = "100%";
|
|
67
|
-
}
|
|
68
|
-
if (node.layoutSizingVertical === "FILL") {
|
|
69
|
-
styles.height = "100%";
|
|
70
|
-
}
|
|
71
|
-
if (node.layoutAlign === "STRETCH") {
|
|
72
|
-
styles.alignSelf = "stretch";
|
|
73
|
-
}
|
|
74
|
-
if (node.gridColumnSpan && node.gridColumnSpan > 1) {
|
|
75
|
-
styles.gridColumn = `span ${node.gridColumnSpan}`;
|
|
76
|
-
}
|
|
77
|
-
if (node.gridRowSpan && node.gridRowSpan > 1) {
|
|
78
|
-
styles.gridRow = `span ${node.gridRowSpan}`;
|
|
79
|
-
}
|
|
80
|
-
if (node.paddingTop !== undefined)
|
|
81
|
-
styles.paddingTop = `${node.paddingTop}px`;
|
|
82
|
-
if (node.paddingBottom !== undefined)
|
|
83
|
-
styles.paddingBottom = `${node.paddingBottom}px`;
|
|
84
|
-
if (node.paddingLeft !== undefined)
|
|
85
|
-
styles.paddingLeft = `${node.paddingLeft}px`;
|
|
86
|
-
if (node.paddingRight !== undefined)
|
|
87
|
-
styles.paddingRight = `${node.paddingRight}px`;
|
|
88
|
-
if (node.clipsContent)
|
|
89
|
-
styles.overflow = "hidden";
|
|
90
|
-
if (node.opacity !== undefined && node.opacity !== 1)
|
|
91
|
-
styles.opacity = node.opacity;
|
|
92
|
-
const fill = node.fills?.find((item) => item.type === "SOLID" && item.color);
|
|
93
|
-
if (fill?.color) {
|
|
94
|
-
if (node.type === "TEXT") {
|
|
95
|
-
styles.color = toRgba(fill.color, fill.opacity ?? 1);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
styles.backgroundColor = toRgba(fill.color, fill.opacity ?? 1);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const imageFill = node.fills?.find((item) => item.type === "IMAGE" && (item.imageLocalPath || item.imageUrl));
|
|
102
|
-
const imageSource = imageFill?.imageLocalPath || imageFill?.imageUrl;
|
|
103
|
-
if (imageSource) {
|
|
104
|
-
styles.backgroundImage = `url("${imageSource}")`;
|
|
105
|
-
styles.backgroundRepeat = "no-repeat";
|
|
106
|
-
styles.backgroundPosition = "center";
|
|
107
|
-
styles.backgroundSize = mapImageScaleMode(imageFill.scaleMode);
|
|
108
|
-
}
|
|
109
|
-
const stroke = node.strokes?.find((item) => item.type === "SOLID" && item.color);
|
|
110
|
-
if (stroke?.color && node.strokeWeight) {
|
|
111
|
-
styles.border = `${Math.round(node.strokeWeight)}px solid ${toRgba(stroke.color, stroke.opacity ?? 1)}`;
|
|
112
|
-
styles.boxSizing = "border-box";
|
|
113
|
-
}
|
|
114
|
-
if (node.rectangleCornerRadii?.length === 4) {
|
|
115
|
-
styles.borderRadius = node.rectangleCornerRadii
|
|
116
|
-
.map((radius) => `${Math.round(radius)}px`)
|
|
117
|
-
.join(" ");
|
|
118
|
-
}
|
|
119
|
-
else if (node.cornerRadius) {
|
|
120
|
-
styles.borderRadius = `${Math.round(node.cornerRadius)}px`;
|
|
121
|
-
}
|
|
122
|
-
if (node.type === "TEXT") {
|
|
123
|
-
const textStyle = node.style || {};
|
|
124
|
-
if (textStyle.fontSize)
|
|
125
|
-
styles.fontSize = `${textStyle.fontSize}px`;
|
|
126
|
-
if (textStyle.fontWeight)
|
|
127
|
-
styles.fontWeight = textStyle.fontWeight;
|
|
128
|
-
if (textStyle.fontFamily)
|
|
129
|
-
styles.fontFamily = `"${textStyle.fontFamily}", sans-serif`;
|
|
130
|
-
if (textStyle.lineHeightPx)
|
|
131
|
-
styles.lineHeight = `${Math.round(textStyle.lineHeightPx)}px`;
|
|
132
|
-
}
|
|
133
|
-
return styles;
|
|
134
|
-
}
|
|
135
|
-
export function toReactStyle(styles) {
|
|
136
|
-
return JSON.stringify(styles).replace(/"([^"]+)":/g, "$1:");
|
|
137
|
-
}
|
|
138
|
-
export function toCssText(styles) {
|
|
139
|
-
return Object.entries(styles)
|
|
140
|
-
.map(([key, value]) => `${toKebabCase(key)}: ${value};`)
|
|
141
|
-
.join(" ");
|
|
142
|
-
}
|
|
143
|
-
function toKebabCase(value) {
|
|
144
|
-
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
145
|
-
}
|
|
146
|
-
function mapPrimaryAxisAlignment(value) {
|
|
147
|
-
switch (value) {
|
|
148
|
-
case "CENTER":
|
|
149
|
-
return "center";
|
|
150
|
-
case "MAX":
|
|
151
|
-
return "flex-end";
|
|
152
|
-
case "SPACE_BETWEEN":
|
|
153
|
-
return "space-between";
|
|
154
|
-
default:
|
|
155
|
-
return "flex-start";
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function mapCounterAxisAlignment(value) {
|
|
159
|
-
switch (value) {
|
|
160
|
-
case "CENTER":
|
|
161
|
-
return "center";
|
|
162
|
-
case "MAX":
|
|
163
|
-
return "flex-end";
|
|
164
|
-
case "BASELINE":
|
|
165
|
-
return "baseline";
|
|
166
|
-
default:
|
|
167
|
-
return "flex-start";
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function mapImageScaleMode(value) {
|
|
171
|
-
switch (value) {
|
|
172
|
-
case "FIT":
|
|
173
|
-
return "contain";
|
|
174
|
-
case "TILE":
|
|
175
|
-
return "auto";
|
|
176
|
-
default:
|
|
177
|
-
return "cover";
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function toRgba(color, opacity) {
|
|
181
|
-
return `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${opacity})`;
|
|
182
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { escapeHtml, getNodeStyles, toCssText } from "./compilerUtils.js";
|
|
2
|
-
export const compileHtml = (node) => [
|
|
3
|
-
{
|
|
4
|
-
path: "index.html",
|
|
5
|
-
contents: `<!doctype html>
|
|
6
|
-
<html lang="en">
|
|
7
|
-
<head>
|
|
8
|
-
<meta charset="UTF-8" />
|
|
9
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
|
-
<title>${escapeHtml(node.name || "Figma Export")}</title>
|
|
11
|
-
</head>
|
|
12
|
-
<body>
|
|
13
|
-
${walkHtml(node, undefined, 2).trimEnd()}
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|
|
16
|
-
`,
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
function walkHtml(node, parent, depth = 0) {
|
|
20
|
-
if (!node || node.visible === false)
|
|
21
|
-
return "";
|
|
22
|
-
const indent = " ".repeat(depth);
|
|
23
|
-
const childIndent = " ".repeat(depth + 1);
|
|
24
|
-
const name = escapeHtml(node.name || "LayoutBox");
|
|
25
|
-
const style = escapeHtml(toCssText(getNodeStyles(node, parent)));
|
|
26
|
-
if (node.type === "TEXT") {
|
|
27
|
-
return `${indent}<span style="${style}" data-layer="${name}">
|
|
28
|
-
${childIndent}${escapeHtml(node.characters)}
|
|
29
|
-
${indent}</span>\n`;
|
|
30
|
-
}
|
|
31
|
-
const children = node.children?.map((child) => walkHtml(child, node, depth + 1)).join("") ||
|
|
32
|
-
"";
|
|
33
|
-
return `${indent}<div style="${style}" data-layer="${name}">
|
|
34
|
-
${children}${indent}</div>\n`;
|
|
35
|
-
}
|
package/dist/compilers/index.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { compileHtml } from "./htmlCompiler.js";
|
|
2
|
-
import { compileReact } from "./reactCompiler.js";
|
|
3
|
-
import { compileVanjs } from "./vanjsCompiler.js";
|
|
4
|
-
export { compileHtml } from "./htmlCompiler.js";
|
|
5
|
-
export { compileReact } from "./reactCompiler.js";
|
|
6
|
-
export { compileVanjs } from "./vanjsCompiler.js";
|
|
7
|
-
export const compilers = {
|
|
8
|
-
react: compileReact,
|
|
9
|
-
html: compileHtml,
|
|
10
|
-
vanjs: compileVanjs,
|
|
11
|
-
};
|
|
12
|
-
export function isCompilerMode(value) {
|
|
13
|
-
return Boolean(value && value in compilers);
|
|
14
|
-
}
|
|
15
|
-
export function getCompiler(mode) {
|
|
16
|
-
return compilers[mode];
|
|
17
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { escapeHtml, escapeJsString, getNodeStyles, toComponentName, toReactStyle, } from "./compilerUtils.js";
|
|
2
|
-
export const compileReact = (node) => {
|
|
3
|
-
const componentName = toComponentName(node.name);
|
|
4
|
-
return [
|
|
5
|
-
{
|
|
6
|
-
path: "FigmaComponent.tsx",
|
|
7
|
-
contents: `import React from 'react';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Auto-generated UI component from Figma frame: "${escapeHtml(node.name)}"
|
|
11
|
-
*/
|
|
12
|
-
export const ${componentName}: React.FC = () => {
|
|
13
|
-
return (
|
|
14
|
-
${walkReact(node).trimEnd()}
|
|
15
|
-
);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default ${componentName};
|
|
19
|
-
`,
|
|
20
|
-
},
|
|
21
|
-
];
|
|
22
|
-
};
|
|
23
|
-
function walkReact(node, depth = 2) {
|
|
24
|
-
if (!node || node.visible === false)
|
|
25
|
-
return "";
|
|
26
|
-
const indent = " ".repeat(depth);
|
|
27
|
-
const childIndent = " ".repeat(depth + 1);
|
|
28
|
-
const name = escapeHtml(node.name || "LayoutBox");
|
|
29
|
-
const styles = toReactStyle(getNodeStyles(node));
|
|
30
|
-
if (node.type === "TEXT") {
|
|
31
|
-
return `${indent}<span style={${styles}} data-layer={${escapeJsString(node.name || "LayoutBox")}}>
|
|
32
|
-
${childIndent}{${escapeJsString(node.characters)}}
|
|
33
|
-
${indent}</span>\n`;
|
|
34
|
-
}
|
|
35
|
-
const children = node.children
|
|
36
|
-
?.map((child) => walkReactWithParent(child, node, depth + 1))
|
|
37
|
-
.join("") || "";
|
|
38
|
-
return `${indent}<div style={${styles}} data-layer="${name}">
|
|
39
|
-
${children}${indent}</div>\n`;
|
|
40
|
-
}
|
|
41
|
-
function walkReactWithParent(node, parent, depth) {
|
|
42
|
-
if (!node || node.visible === false)
|
|
43
|
-
return "";
|
|
44
|
-
const indent = " ".repeat(depth);
|
|
45
|
-
const childIndent = " ".repeat(depth + 1);
|
|
46
|
-
const name = escapeHtml(node.name || "LayoutBox");
|
|
47
|
-
const styles = toReactStyle(getNodeStyles(node, parent));
|
|
48
|
-
if (node.type === "TEXT") {
|
|
49
|
-
return `${indent}<span style={${styles}} data-layer={${escapeJsString(node.name || "LayoutBox")}}>
|
|
50
|
-
${childIndent}{${escapeJsString(node.characters)}}
|
|
51
|
-
${indent}</span>\n`;
|
|
52
|
-
}
|
|
53
|
-
const children = node.children
|
|
54
|
-
?.map((child) => walkReactWithParent(child, node, depth + 1))
|
|
55
|
-
.join("") || "";
|
|
56
|
-
return `${indent}<div style={${styles}} data-layer="${name}">
|
|
57
|
-
${children}${indent}</div>\n`;
|
|
58
|
-
}
|