├── .editorconfig
├── .gitattributes
├── .gitignore
├── .plugma
└── versions.json
├── LICENSE
├── NOTES.md
├── README.md
├── TODO.md
├── dist
└── index.js
├── package-lock.json
├── package.json
├── package
└── index.ts
├── postcss.config.js
├── public
├── components.html
├── manifest.json
└── version-log.html
├── rollup.config.js
├── src
├── PluginUI.svelte
├── Toggle.svelte
├── code.ts
├── figma.d.ts
├── helpers.ts
├── logo.svg
├── main.js
├── pluginGeneration.ts
├── props.ts
├── setCharacters.ts
├── str.ts
├── syntax-theme.css
├── template.html
└── widgetGeneration.ts
├── stylup.config.js
├── stylup.js
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = tab
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 | insert_final_newline = false
14 |
15 | [*.{json,md,yml}]
16 | indent_style = space
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /public/build/
3 | public/index.html
4 | src/build
5 | .DS_Store
6 | public/code.js
7 |
--------------------------------------------------------------------------------
/.plugma/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "1.0.0": [
3 | "First release!"
4 | ]
5 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Thomas Lowry
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NOTES.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | Below is the order of things that need to be checked for
4 |
5 | 1. Create nodes
6 | 1. Walk node
7 | 1. If instance -> Check doesn't already exist -> Push component to array
8 | 2. Push font to array
9 | 3. Create nodes
10 | 1. If component check it doesn't match one in component array
11 | 1. Do not create nested instances
12 |
13 | 3. Create components
14 | 1. Walk nodes
15 | 1. Create nodes
16 |
17 | 2. Do not create nested instances
18 | 3. Do not push component to array
19 | 3. Do not push font to array
20 |
21 | 2. Create fonts
22 |
23 |
24 | Order in which nodes are created
25 |
26 | 1. Create components in reverse
27 | 2. Create all other nodes
28 | 3. If
29 |
30 |
31 | ## Functions
32 |
33 | ### `walkNodes()`
34 |
35 | A function which loops through each node, with special properties.
36 |
37 | ### `createProps()`
38 |
39 | ```js
40 | `${createProps()}`
41 | ```
42 |
43 | ### `appendNode()`
44 |
45 | ```js
46 | `${Ref(node.parent)}.appendChild(${Ref(node)})\n`
47 | ```
48 |
49 | ### `createNode()`
50 |
51 | ```js
52 | `// Create ${node.type}
53 | var ${Ref(node.parent)} = figma.create${v.titleCase(node.type)}
54 | ${createProps}`
55 | ```
56 |
57 | ### `createInstance()`
58 |
59 | ```js
60 | function createInstance(node) {
61 | var components = [];
62 |
63 | walkNodes([node], {
64 | during(node) {
65 | components.push(node.mainComponent)
66 | },
67 | after(node) {
68 | `// Create INSTANCE
69 | var ${Ref(node)} = ${Ref(node.mainComponent)}.createInstance()\n`
70 | }
71 | })
72 | }
73 | ```
74 |
75 | ### `createGroup()`
76 |
77 | ```js
78 | `// Create GROUP
79 | figma.group([${Ref(node.children)}], ${Ref(node.parent)})\n`
80 | ```
81 |
82 | ### `createComponentSet()`
83 |
84 | ```js
85 | `// Create COMPONENT_SET
86 | var ${Ref(node)} = figma.combineAsVariants([${Ref(node.children).join(", ")}], ${Ref(node.parent)})
87 | ${createProps}`
88 | ```
89 |
90 |
91 | ```js
92 | var fonts, components;
93 |
94 |
95 |
96 | walkNodes(nodes, {
97 | during(node) {
98 | // If instance push component to array
99 | createNode()
100 | createInstance()
101 | appendNode()
102 | },
103 | after(node) {
104 | createGroup()
105 | createComponent(() => {
106 | walkNodes(node.children, {
107 | during(node) {
108 | createProps()
109 | }
110 | })
111 | }
112 | )
113 | }
114 | }
115 | )
116 |
117 | createFonts(fonts)
118 |
119 | ```
120 |
121 |
122 | ## Questions
123 |
124 | - Should children of COMPONENT_SETs be appended?
125 | - Should Auto Layout be applied after all children are created?
126 | - Should appending happen after or before props are applied?
127 |
128 | ## Future refactoring
129 |
130 | - Consider modifying so that it creates a new object/array which the plugin then creates a string from. This would be better because it would avoid the need to use eval when decoding.
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node Decoder
2 |
3 | Node Decoder is a Figma plugin that generates plugin or widget source code from any Figma design as Javascript and JSX. This is useful for avoiding the need to code visual assets manually when developing for Figma.
4 |
5 | https://www.figma.com/community/plugin/933372797518031971/Node-Decoder
6 |
7 | ## JavaScript API (Alpha)
8 |
9 | To use the Node Decoder plugin in your own plugin, you can use the JavaScript API. It has been adapted so you can use it as a library inside your plugin.
10 |
11 | **Please note**: While you can convert JSON to Figma using the functions below, the JSON needs to reference all of a node's parent relations, ie, its parent, grandparent, great-grandparent, great-great-grandparent etc. I'll provide a custom helper for this at some point.
12 |
13 | ### Getting started
14 |
15 | For now, install it from Github as a node module.
16 |
17 | ```bash
18 | npm install --save-dev https://github.com/gavinmcfarland/figma-node-decoder/tarball/javascript-api
19 | ```
20 |
21 | Import the helpers into your plugin
22 |
23 | ```js
24 | // code.ts
25 | import { encodeAsync, decodeAsync } from 'figma-node-decoder'
26 | ```
27 |
28 | Pass in the nodes you want to encode.
29 |
30 | ```js
31 | // Pass nodes as an array and the platform you're encoding
32 | encodeAsync(figma.currentPage.selection, { platform: 'PLUGIN' }).then((string) => {
33 |
34 | // Store it somewhere
35 | figma.root.setPluginData("selectionAsString", string)
36 | })
37 | ```
38 |
39 | Decode the string when you want to recreate the nodes.
40 |
41 | ```js
42 | // Grab the string
43 | let selectionAsString = figma.root.getPluginData("selectionAsString")
44 |
45 | // Recreate the nodes from string
46 | decodeAsync(selectionAsString).then(({ nodes }) => {
47 | figma.currentPage.selection = nodes
48 | figma.viewport.scrollAndZoomIntoView(nodes)
49 | figma.closePlugin("Nodes created")
50 | })
51 | ```
52 |
53 | ## Development
54 |
55 | ### Setup
56 | ```bash
57 | npm install
58 | ```
59 |
60 | ### Development
61 | During development, watch your project for changes with the following command.
62 |
63 | ```bash
64 | npm run dev
65 | ```
66 |
67 | ### Build
68 | When ready to package up your final Figma Plugin:
69 | ```bash
70 | npm run build
71 | ```
72 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | 1. Decide how to package and publish this as a node module package
2 | 2. Fix issue where styles need font information to be loaded before being set
3 | 3. Encapsulate string in function
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "figma-node-decoder",
3 | "version": "1.0.0",
4 | "main": "dist/index.js",
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public"
9 | },
10 | "devDependencies": {
11 | "@figma/plugin-typings": "^1.63.0",
12 | "@fignite/helpers": "^0.0.0-alpha.10",
13 | "@rollup/plugin-commonjs": "^17.0.0",
14 | "@rollup/plugin-image": "^2.0.6",
15 | "@rollup/plugin-json": "^4.1.0",
16 | "@rollup/plugin-node-resolve": "^11.1.0",
17 | "@rollup/plugin-replace": "^3.0.0",
18 | "@types/common-tags": "^1.8.0",
19 | "@types/lodash": "^4.14.168",
20 | "common-tags": "^1.8.0",
21 | "cssnano": "^4.1.10",
22 | "figma-plugin-ds-svelte": "^1.0.7",
23 | "highlight.js": "^10.5.0",
24 | "lodash": "^4.17.20",
25 | "plugma": "*",
26 | "postcss": "^8.2.4",
27 | "postcss-nested": "^5.0.3",
28 | "rollup": "^2.36.2",
29 | "rollup-plugin-html-bundle": "0.0.3",
30 | "rollup-plugin-livereload": "^2.0.0",
31 | "rollup-plugin-postcss": "^4.0.0",
32 | "rollup-plugin-svelte": "^7.0.0",
33 | "rollup-plugin-svg": "^2.0.0",
34 | "rollup-plugin-terser": "^7.0.2",
35 | "rollup-plugin-typescript": "^1.0.1",
36 | "stylup": "0.0.0-alpha.3",
37 | "svelte": "^3.31.2",
38 | "svelte-highlight": "^0.6.2",
39 | "tslib": "^2.1.0",
40 | "typescript": "^4.1.3",
41 | "voca": "^1.4.0",
42 | "xregexp": "^4.4.1"
43 | },
44 | "dependencies": {
45 | "autoprefixer": "^10.2.1",
46 | "base-64": "^1.0.0",
47 | "fs-extra": "^9.1.0",
48 | "illuminate-js": "^1.0.0-alpha.2",
49 | "node-syntaxhighlighter": "^0.8.1",
50 | "postcss-logical": "^4.0.2",
51 | "rollup-plugin-node-globals": "^1.4.0",
52 | "rollup-plugin-node-polyfills": "^0.2.1",
53 | "sirv-cli": "^1.0.10",
54 | "uniqid": "^5.2.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/package/index.ts:
--------------------------------------------------------------------------------
1 | import { genPluginStr } from "../src/pluginGeneration";
2 | import { genWidgetStr } from "../src/widgetGeneration";
3 |
4 | export async function encodeAsync(array, options?) {
5 | console.log(options);
6 | if (options.platform === "PLUGIN" || options.platform === "plugin") {
7 | return await (
8 | await genPluginStr(array, {
9 | wrapInFunction: true,
10 | includeObject: true,
11 | })
12 | ).join("");
13 | }
14 | if (options.platform === "WIDGET" || options.platform === "widget") {
15 | return await genWidgetStr(array);
16 | }
17 | }
18 |
19 | export async function decodeAsync(string, options?) {
20 | let nodes;
21 |
22 | try {
23 | nodes = await eval(string);
24 | } catch {
25 | figma.triggerUndo();
26 | figma.notify("Error running code");
27 | }
28 |
29 | return {
30 | nodes,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | // 'postcss-import': {}
4 | // 'postcss-selector-matches': {},
5 | // 'postcss-for': {},
6 | // 'postcss-custom-selectors': {},
7 | // 'postcss-short': {},
8 | // 'postcss-nested': {},
9 | 'postcss-logical': {},
10 | // 'postcss-proportional-spacing': {},
11 | // 'postcss-pow': {}
12 | // 'autoprefixer': {}
13 | // require('autoprefixer')
14 | // 'postcss-extend-rule': {},
15 | // 'postcss-flexbugs-fixes': {},
16 | // 'postcss-preset-env': {
17 | // autoprefixer: {
18 | // flexbox: 'no-2009',
19 | // },
20 | // stage: 3,
21 | // features: {
22 | // 'custom-properties': false,
23 | // },
24 | // },
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Node Decoder",
3 | "id": "933372797518031971",
4 | "api": "1.0.0",
5 | "main": "code.js",
6 | "ui": {
7 | "main": "index.html"
8 | },
9 | "editorType": ["figma"],
10 | "networkAccess": {
11 | "allowedDomains": ["none"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import commonjs from '@rollup/plugin-commonjs';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 | import svg from 'rollup-plugin-svg';
7 | import typescript from 'rollup-plugin-typescript';
8 | import replace from '@rollup/plugin-replace'
9 | import json from '@rollup/plugin-json'
10 | import globals from 'rollup-plugin-node-globals'
11 | import nodePolyfills from 'rollup-plugin-node-polyfills'
12 |
13 | /* Post CSS */
14 | import postcss from 'rollup-plugin-postcss';
15 | // import cssnano from 'cssnano';
16 |
17 |
18 | /* Inline to single html */
19 | import htmlBundle from 'rollup-plugin-html-bundle';
20 |
21 | import { stylup } from './stylup'
22 |
23 | const production = !process.env.ROLLUP_WATCH;
24 |
25 | export default [{
26 | input: 'src/main.js',
27 | output: {
28 | format: 'iife',
29 | name: 'ui',
30 | file: 'src/build/bundle.js'
31 | },
32 | plugins: [
33 | svelte({
34 | // enable run-time checks when not in production
35 | // dev: !production,
36 | // preprocess: [stylup]
37 | }),
38 |
39 | // If you have external dependencies installed from
40 | // npm, you'll most likely need these plugins. In
41 | // some cases you'll need additional configuration —
42 | // consult the documentation for details:¡
43 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
44 | resolve({
45 | browser: true,
46 | dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/'),
47 | extensions: ['.svelte', '.mjs', '.js', '.json', '.node']
48 | }),
49 | commonjs(),
50 | svg(),
51 | postcss(),
52 | htmlBundle({
53 | template: 'src/template.html',
54 | target: 'public/index.html',
55 | inline: true
56 | }),
57 |
58 | // In dev mode, call `npm run start` once
59 | // the bundle has been generated
60 | !production && serve(),
61 |
62 | // Watch the `dist` directory and refresh the
63 | // browser on changes when not in production
64 | !production && livereload('public'),
65 |
66 | // If we're building for production (npm run build
67 | // instead of npm run dev), minify
68 | production && terser()
69 | ],
70 | watch: {
71 | clearScreen: false
72 | }
73 | },
74 | // Javascript API
75 | {
76 | input: 'package/index.ts',
77 | output: {
78 | file: 'dist/index.js',
79 | format: 'cjs',
80 | name: 'javascript-api'
81 | },
82 | plugins: [
83 | typescript(),
84 | resolve(),
85 | json(),
86 | commonjs(),
87 | production && terser()
88 | ]
89 | },
90 | {
91 | input: 'src/code.ts',
92 | output: {
93 | file: 'public/code.js',
94 | format: 'cjs',
95 | name: 'code'
96 | },
97 | plugins: [
98 | typescript(),
99 | nodePolyfills({ include: null, exclude: ['../**/node_modules/voca/*.js'] }),
100 | resolve(),
101 | replace({
102 | 'process.env.PKG_PATH': JSON.stringify(process.cwd() + '/package.json'),
103 | 'process.env.VERSIONS_PATH': JSON.stringify(process.cwd() + '/.plugma/versions.json')
104 | }),
105 | json(),
106 | commonjs(),
107 | production && terser()
108 | ]
109 | }
110 | ];
111 |
112 | function serve() {
113 | let started = false;
114 |
115 | return {
116 | writeBundle() {
117 | if (!started) {
118 | started = true;
119 |
120 | require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
121 | stdio: ['ignore', 'inherit', 'inherit'],
122 | shell: true
123 | });
124 | }
125 | }
126 | };
127 | }
128 |
--------------------------------------------------------------------------------
/src/PluginUI.svelte:
--------------------------------------------------------------------------------
1 |
172 |
173 |
174 |
175 |
176 | {@html github}
177 |
178 |
179 |
193 |
194 |
195 |
196 |
197 | {#await promise}
198 |
Formatting code...
199 | {:then data}
200 |
202 | {@html highlight(data.value, "js")}
203 |
206 |
207 | {:catch error}
208 |
{error.message}
209 | {/await}
210 |
211 |
212 |
219 |
220 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | Refresh
230 |
231 |
232 | Copy
233 |
234 |
235 |
236 |
241 |
242 |
380 |
--------------------------------------------------------------------------------
/src/Toggle.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 | {togglePlatform(platform, "plugin")}} style={checked ? "font-weight: bold; color: rgba(0, 0, 0, 0.8);" : ""}>Plugin
41 |
42 |
43 |
47 |
48 |
49 | {togglePlatform(platform, "widget")}} style={!checked ? "font-weight: bold; color: rgba(0, 0, 0, 0.8);" : ""}>Widget
50 |
51 |
52 |
53 |
125 |
--------------------------------------------------------------------------------
/src/code.ts:
--------------------------------------------------------------------------------
1 | import plugma from "plugma";
2 | import { genWidgetStr } from "./widgetGeneration";
3 | import { genPluginStr } from "./pluginGeneration";
4 | import { encodeAsync, decodeAsync } from "../package";
5 | import {
6 | getClientStorageAsync,
7 | setClientStorageAsync,
8 | updateClientStorageAsync,
9 | setPluginData,
10 | getPluginData,
11 | nodeToObject,
12 | ungroup,
13 | } from "@fignite/helpers";
14 | import { groupBy } from "lodash";
15 |
16 | // TODO: Different string output for running code and showing in UI
17 |
18 | console.clear();
19 |
20 | // console.log("topInstance", getTopInstance(figma.currentPage.selection[0]))
21 | // console.log("parentIstance", getParentInstance(figma.currentPage.selection[0]))
22 | // console.log("location", getNodeLocation(figma.currentPage.selection[0], getTopInstance(figma.currentPage.selection[0])))
23 | // console.log("counterPart1", getInstanceCounterpartUsingLocation(figma.currentPage.selection[0], getTopInstance(figma.currentPage.selection[0])))
24 | // console.log("backingNode", getInstanceCounterpartUsingLocation(figma.currentPage.selection[0], getTopInstance(figma.currentPage.selection[0])))
25 |
26 | plugma((plugin) => {
27 | var origSel = figma.currentPage.selection;
28 | var outputPlatform = "";
29 |
30 | var cachedPlugin;
31 | var cachedWidget;
32 |
33 | var successful;
34 |
35 | var uiDimensions = { width: 280, height: 420 };
36 |
37 | plugin.on("code-rendered", () => {
38 | successful = true;
39 | });
40 |
41 | plugin.on("code-copied", () => {
42 | figma.notify("Copied to clipboard");
43 | });
44 |
45 | plugin.on("set-platform", (msg) => {
46 | var platform = msg.platform;
47 | outputPlatform = platform;
48 | const handle = figma.notify("Generating code...", {
49 | timeout: 99999999999,
50 | });
51 |
52 | setClientStorageAsync("platform", platform);
53 |
54 | if (platform === "plugin") {
55 | if (cachedPlugin) {
56 | handle.cancel();
57 | figma.ui.postMessage({
58 | type: "string-received",
59 | value:
60 | "async function main() {\n\n" +
61 | cachedPlugin +
62 | "\n}\n\nmain()",
63 | platform,
64 | });
65 | } else {
66 | genPluginStr(origSel, {
67 | wrapInFunction: true,
68 | includeObject: true,
69 | })
70 | .then(async (string) => {
71 | handle.cancel();
72 |
73 | // figma.showUI(__uiFiles__.main, { width: 320, height: 480 });
74 |
75 | figma.ui.postMessage({
76 | type: "string-received",
77 | value: string.join(""),
78 | platform,
79 | });
80 |
81 | setTimeout(function () {
82 | if (!successful) {
83 | figma.notify("Plugin timed out");
84 | figma.closePlugin();
85 | }
86 | }, 8000);
87 | })
88 | .catch((error) => {
89 | handle.cancel();
90 | if (error.message === "cannot convert to object") {
91 | figma.closePlugin(
92 | `Could not generate ${platform} code for selection`
93 | );
94 | } else {
95 | figma.closePlugin(`${error}`);
96 | }
97 | });
98 | }
99 | }
100 |
101 | if (platform === "widget") {
102 | if (cachedWidget) {
103 | handle.cancel();
104 | figma.ui.postMessage({
105 | type: "string-received",
106 | value: cachedWidget,
107 | platform,
108 | });
109 | } else {
110 | genWidgetStr(origSel)
111 | .then((string) => {
112 | handle.cancel();
113 |
114 | // figma.showUI(__uiFiles__.main, { width: 320, height: 480 });
115 |
116 | figma.ui.postMessage({
117 | type: "string-received",
118 | value: string,
119 | platform,
120 | });
121 |
122 | setTimeout(function () {
123 | if (!successful) {
124 | figma.notify("Plugin timed out");
125 | figma.closePlugin();
126 | }
127 | }, 8000);
128 | })
129 | .catch((error) => {
130 | handle.cancel();
131 | if (error.message === "cannot convert to object") {
132 | figma.closePlugin(
133 | `Could not generate ${platform} code for selection`
134 | );
135 | } else {
136 | figma.closePlugin(`${error}`);
137 | }
138 | });
139 | }
140 | }
141 | });
142 |
143 | async function runPlugin(clearCache?) {
144 | const handle = figma.notify("Generating code...", {
145 | timeout: 99999999999,
146 | });
147 |
148 | let platform = await updateClientStorageAsync(
149 | "platform",
150 | (platform) => {
151 | platform = platform || "plugin";
152 | outputPlatform = platform;
153 | return platform;
154 | }
155 | );
156 |
157 | if (clearCache) {
158 | origSel = figma.currentPage.selection;
159 | }
160 |
161 | if (origSel.length > 0) {
162 | // If something is selected create new string and save to client storage
163 |
164 | cachedPlugin = await (
165 | await genPluginStr(origSel, { platform: "plugin" })
166 | ).join("");
167 | cachedWidget = await genWidgetStr(origSel);
168 |
169 | setClientStorageAsync(`cachedPlugin`, cachedPlugin);
170 | setClientStorageAsync(`cachedWidget`, cachedWidget);
171 | } else {
172 | // If no selection then get the cached version
173 | cachedPlugin = await getClientStorageAsync(`cachedPlugin`);
174 | cachedWidget = await getClientStorageAsync(`cachedWidget`);
175 | }
176 |
177 | // restore previous size
178 | let uiSize = await getClientStorageAsync("uiSize");
179 |
180 | if (!uiSize) {
181 | setClientStorageAsync("uiSize", uiDimensions);
182 | uiSize = uiDimensions;
183 | }
184 |
185 | // if (platform === "plugin") {
186 | // cachedPlugin = encodedString;
187 | // }
188 |
189 | // if (platform === "widget") {
190 | // cachedWidget = encodedString;
191 | // }
192 |
193 | handle.cancel();
194 |
195 | figma.showUI(__uiFiles__.main, uiSize);
196 |
197 | console.log(platform);
198 | if (platform === "plugin") {
199 | figma.ui.postMessage({
200 | type: "string-received",
201 | value:
202 | "async function main() {\n\n" +
203 | cachedPlugin +
204 | "\n}\n\nmain()",
205 | platform,
206 | });
207 | }
208 |
209 | if (platform === "widget") {
210 | figma.ui.postMessage({
211 | type: "string-received",
212 | value: cachedWidget,
213 | platform,
214 | });
215 | }
216 |
217 | setTimeout(function () {
218 | if (!successful) {
219 | figma.notify("Plugin timed out");
220 | figma.closePlugin();
221 | }
222 | }, 8000);
223 | }
224 |
225 | runPlugin();
226 |
227 | // figma.on("selectionchange", () => {
228 | // if (figma.currentPage.selection.length > 0) {
229 | // runPlugin(true);
230 | // }
231 | // });
232 |
233 | plugin.on("run-code", () => {
234 | if (outputPlatform === "plugin") {
235 | // getClientStorageAsync("encodedString").then((string) => {
236 |
237 | let string = cachedPlugin;
238 |
239 | string =
240 | `// Wrap in function
241 | async function createNodes() {
242 |
243 | // Create temporary page to pass nodes to function
244 | let oldPage = figma.currentPage
245 | let newPage = figma.createPage()
246 | figma.currentPage = newPage
247 | ` +
248 | string +
249 | `// Pass children to function
250 | let nodes = figma.currentPage.children
251 | figma.currentPage = oldPage
252 |
253 | for (let i = 0; i < nodes.length; i++) {
254 | let node = nodes[i]
255 | figma.currentPage.appendChild(node)
256 | }
257 |
258 | newPage.remove()
259 | figma.currentPage = oldPage
260 |
261 | return nodes
262 |
263 | }
264 |
265 | createNodes()`;
266 |
267 | decodeAsync(string).then(({ nodes }) => {
268 | function positionInCenter(node) {
269 | // Position newly created table in center of viewport
270 | node.x = figma.viewport.center.x - node.width / 2;
271 | node.y = figma.viewport.center.y - node.height / 2;
272 | }
273 |
274 | let group = figma.group(nodes, figma.currentPage);
275 |
276 | positionInCenter(group);
277 |
278 | nodes = ungroup(group, figma.currentPage);
279 | figma.currentPage.selection = nodes;
280 | // figma.viewport.scrollAndZoomIntoView(nodes)
281 | figma.notify("Code run");
282 | });
283 |
284 | // });
285 | }
286 | });
287 |
288 | plugin.on("refresh-selection", (msg) => {
289 | if (figma.currentPage.selection.length > 0) {
290 | runPlugin(true);
291 | } else {
292 | figma.notify("Select nodes to refresh");
293 | }
294 | });
295 |
296 | plugin.on("resize", (msg) => {
297 | figma.ui.resize(msg.size.width, msg.size.height);
298 | figma.clientStorage.setAsync("uiSize", msg.size).catch((err) => {}); // save size
299 | });
300 | });
301 |
302 | // async function encodeAsync(array) {
303 |
304 | // // return nodeToObject(node)
305 |
306 | // return await genPluginStr(array)
307 |
308 | // }
309 |
310 | // async function decodeAsync(string) {
311 | // return eval(string)
312 | // }
313 |
314 | // if (figma.command === "encode") {
315 | // var objects = []
316 |
317 | // for (var i = 0; i < figma.currentPage.selection.length; i++) {
318 | // var node = figma.currentPage.selection[i]
319 | // var object = nodeToObject(node, false)
320 | // console.log(object)
321 | // objects.push(object)
322 | // }
323 |
324 | // // Can use either nodes directly, or JSON representation of nodes. If using JSON, it must include id's and type's of all parent relations.
325 | // encodeAsync(objects).then((string) => {
326 | // console.log(string)
327 | // setPluginData(figma.root, "selectionAsString", string)
328 | // figma.closePlugin("Selection stored as string")
329 | // })
330 | // }
331 |
332 | // if (figma.command === "decode") {
333 | // var selectionAsString = getPluginData(figma.root, "selectionAsString")
334 | // // console.log(selectionAsString)
335 | // decodeAsync(selectionAsString).then(() => {
336 | // figma.closePlugin("String converted to node")
337 | // })
338 | // }
339 |
--------------------------------------------------------------------------------
/src/figma.d.ts:
--------------------------------------------------------------------------------
1 | // Figma Plugin API version 1, update 14
2 |
3 | declare global {
4 | // Global variable with Figma's plugin API.
5 | const figma: PluginAPI
6 | const __html__: string
7 |
8 | interface PluginAPI {
9 | readonly apiVersion: "1.0.0"
10 | readonly command: string
11 | readonly viewport: ViewportAPI
12 | closePlugin(message?: string): void
13 |
14 | notify(message: string, options?: NotificationOptions): NotificationHandler
15 |
16 | showUI(html: string, options?: ShowUIOptions): void
17 | readonly ui: UIAPI
18 |
19 | readonly clientStorage: ClientStorageAPI
20 |
21 | getNodeById(id: string): BaseNode | null
22 | getStyleById(id: string): BaseStyle | null
23 |
24 | readonly root: DocumentNode
25 | currentPage: PageNode
26 |
27 | on(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void
28 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void
29 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void
30 |
31 | readonly mixed: unique symbol
32 |
33 | createRectangle(): RectangleNode
34 | createLine(): LineNode
35 | createEllipse(): EllipseNode
36 | createPolygon(): PolygonNode
37 | createStar(): StarNode
38 | createVector(): VectorNode
39 | createText(): TextNode
40 | createFrame(): FrameNode
41 | createComponent(): ComponentNode
42 | createPage(): PageNode
43 | createSlice(): SliceNode
44 | /**
45 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead.
46 | */
47 | createBooleanOperation(): BooleanOperationNode
48 |
49 | createPaintStyle(): PaintStyle
50 | createTextStyle(): TextStyle
51 | createEffectStyle(): EffectStyle
52 | createGridStyle(): GridStyle
53 |
54 | // The styles are returned in the same order as displayed in the UI. Only
55 | // local styles are returned. Never styles from team library.
56 | getLocalPaintStyles(): PaintStyle[]
57 | getLocalTextStyles(): TextStyle[]
58 | getLocalEffectStyles(): EffectStyle[]
59 | getLocalGridStyles(): GridStyle[]
60 |
61 | importComponentByKeyAsync(key: string): Promise
62 | importStyleByKeyAsync(key: string): Promise
63 |
64 | listAvailableFontsAsync(): Promise
65 | loadFontAsync(fontName: FontName): Promise
66 | readonly hasMissingFont: boolean
67 |
68 | createNodeFromSvg(svg: string): FrameNode
69 |
70 | createImage(data: Uint8Array): Image
71 | getImageByHash(hash: string): Image
72 |
73 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): GroupNode
74 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode
75 |
76 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
77 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
78 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
79 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
80 | }
81 |
82 | interface ClientStorageAPI {
83 | getAsync(key: string): Promise
84 | setAsync(key: string, value: any): Promise
85 | }
86 |
87 | interface NotificationOptions {
88 | timeout?: number
89 | }
90 |
91 | interface NotificationHandler {
92 | cancel: () => void
93 | }
94 |
95 | interface ShowUIOptions {
96 | visible?: boolean
97 | width?: number
98 | height?: number
99 | }
100 |
101 | interface UIPostMessageOptions {
102 | origin?: string
103 | }
104 |
105 | interface OnMessageProperties {
106 | origin: string
107 | }
108 |
109 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void
110 |
111 | interface UIAPI {
112 | show(): void
113 | hide(): void
114 | resize(width: number, height: number): void
115 | close(): void
116 |
117 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void
118 | onmessage: MessageEventHandler | undefined
119 | on(type: "message", callback: MessageEventHandler): void
120 | once(type: "message", callback: MessageEventHandler): void
121 | off(type: "message", callback: MessageEventHandler): void
122 | }
123 |
124 | interface ViewportAPI {
125 | center: Vector
126 | zoom: number
127 | scrollAndZoomIntoView(nodes: ReadonlyArray): void
128 | readonly bounds: Rect
129 | }
130 |
131 | ////////////////////////////////////////////////////////////////////////////////
132 | // Datatypes
133 |
134 | type Transform = [
135 | [number, number, number],
136 | [number, number, number]
137 | ]
138 |
139 | interface Vector {
140 | readonly x: number
141 | readonly y: number
142 | }
143 |
144 | interface Rect {
145 | readonly x: number
146 | readonly y: number
147 | readonly width: number
148 | readonly height: number
149 | }
150 |
151 | interface RGB {
152 | readonly r: number
153 | readonly g: number
154 | readonly b: number
155 | }
156 |
157 | interface RGBA {
158 | readonly r: number
159 | readonly g: number
160 | readonly b: number
161 | readonly a: number
162 | }
163 |
164 | interface FontName {
165 | readonly family: string
166 | readonly style: string
167 | }
168 |
169 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE"
170 |
171 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH"
172 |
173 | interface ArcData {
174 | readonly startingAngle: number
175 | readonly endingAngle: number
176 | readonly innerRadius: number
177 | }
178 |
179 | interface ShadowEffect {
180 | readonly type: "DROP_SHADOW" | "INNER_SHADOW"
181 | readonly color: RGBA
182 | readonly offset: Vector
183 | readonly radius: number
184 | readonly visible: boolean
185 | readonly blendMode: BlendMode
186 | }
187 |
188 | interface BlurEffect {
189 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR"
190 | readonly radius: number
191 | readonly visible: boolean
192 | }
193 |
194 | type Effect = ShadowEffect | BlurEffect
195 |
196 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE"
197 |
198 | interface Constraints {
199 | readonly horizontal: ConstraintType
200 | readonly vertical: ConstraintType
201 | }
202 |
203 | interface ColorStop {
204 | readonly position: number
205 | readonly color: RGBA
206 | }
207 |
208 | interface ImageFilters {
209 | readonly exposure?: number
210 | readonly contrast?: number
211 | readonly saturation?: number
212 | readonly temperature?: number
213 | readonly tint?: number
214 | readonly highlights?: number
215 | readonly shadows?: number
216 | }
217 |
218 | interface SolidPaint {
219 | readonly type: "SOLID"
220 | readonly color: RGB
221 |
222 | readonly visible?: boolean
223 | readonly opacity?: number
224 | readonly blendMode?: BlendMode
225 | }
226 |
227 | interface GradientPaint {
228 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND"
229 | readonly gradientTransform: Transform
230 | readonly gradientStops: ReadonlyArray
231 |
232 | readonly visible?: boolean
233 | readonly opacity?: number
234 | readonly blendMode?: BlendMode
235 | }
236 |
237 | interface ImagePaint {
238 | readonly type: "IMAGE"
239 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE"
240 | readonly imageHash: string | null
241 | readonly imageTransform?: Transform // setting for "CROP"
242 | readonly scalingFactor?: number // setting for "TILE"
243 | readonly filters?: ImageFilters
244 |
245 | readonly visible?: boolean
246 | readonly opacity?: number
247 | readonly blendMode?: BlendMode
248 | }
249 |
250 | type Paint = SolidPaint | GradientPaint | ImagePaint
251 |
252 | interface Guide {
253 | readonly axis: "X" | "Y"
254 | readonly offset: number
255 | }
256 |
257 | interface RowsColsLayoutGrid {
258 | readonly pattern: "ROWS" | "COLUMNS"
259 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER"
260 | readonly gutterSize: number
261 |
262 | readonly count: number // Infinity when "Auto" is set in the UI
263 | readonly sectionSize?: number // Not set for alignment: "STRETCH"
264 | readonly offset?: number // Not set for alignment: "CENTER"
265 |
266 | readonly visible?: boolean
267 | readonly color?: RGBA
268 | }
269 |
270 | interface GridLayoutGrid {
271 | readonly pattern: "GRID"
272 | readonly sectionSize: number
273 |
274 | readonly visible?: boolean
275 | readonly color?: RGBA
276 | }
277 |
278 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid
279 |
280 | interface ExportSettingsConstraints {
281 | readonly type: "SCALE" | "WIDTH" | "HEIGHT"
282 | readonly value: number
283 | }
284 |
285 | interface ExportSettingsImage {
286 | readonly format: "JPG" | "PNG"
287 | readonly contentsOnly?: boolean // defaults to true
288 | readonly suffix?: string
289 | readonly constraint?: ExportSettingsConstraints
290 | }
291 |
292 | interface ExportSettingsSVG {
293 | readonly format: "SVG"
294 | readonly contentsOnly?: boolean // defaults to true
295 | readonly suffix?: string
296 | readonly svgOutlineText?: boolean // defaults to true
297 | readonly svgIdAttribute?: boolean // defaults to false
298 | readonly svgSimplifyStroke?: boolean // defaults to true
299 | }
300 |
301 | interface ExportSettingsPDF {
302 | readonly format: "PDF"
303 | readonly contentsOnly?: boolean // defaults to true
304 | readonly suffix?: string
305 | }
306 |
307 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF
308 |
309 | type WindingRule = "NONZERO" | "EVENODD"
310 |
311 | interface VectorVertex {
312 | readonly x: number
313 | readonly y: number
314 | readonly strokeCap?: StrokeCap
315 | readonly strokeJoin?: StrokeJoin
316 | readonly cornerRadius?: number
317 | readonly handleMirroring?: HandleMirroring
318 | }
319 |
320 | interface VectorSegment {
321 | readonly start: number
322 | readonly end: number
323 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 }
324 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 }
325 | }
326 |
327 | interface VectorRegion {
328 | readonly windingRule: WindingRule
329 | readonly loops: ReadonlyArray>
330 | }
331 |
332 | interface VectorNetwork {
333 | readonly vertices: ReadonlyArray
334 | readonly segments: ReadonlyArray
335 | readonly regions?: ReadonlyArray // Defaults to []
336 | }
337 |
338 | interface VectorPath {
339 | readonly windingRule: WindingRule | "NONE"
340 | readonly data: string
341 | }
342 |
343 | type VectorPaths = ReadonlyArray
344 |
345 | interface LetterSpacing {
346 | readonly value: number
347 | readonly unit: "PIXELS" | "PERCENT"
348 | }
349 |
350 | type LineHeight = {
351 | readonly value: number
352 | readonly unit: "PIXELS" | "PERCENT"
353 | } | {
354 | readonly unit: "AUTO"
355 | }
356 |
357 | type BlendMode =
358 | "PASS_THROUGH" |
359 | "NORMAL" |
360 | "DARKEN" |
361 | "MULTIPLY" |
362 | "LINEAR_BURN" |
363 | "COLOR_BURN" |
364 | "LIGHTEN" |
365 | "SCREEN" |
366 | "LINEAR_DODGE" |
367 | "COLOR_DODGE" |
368 | "OVERLAY" |
369 | "SOFT_LIGHT" |
370 | "HARD_LIGHT" |
371 | "DIFFERENCE" |
372 | "EXCLUSION" |
373 | "HUE" |
374 | "SATURATION" |
375 | "COLOR" |
376 | "LUMINOSITY"
377 |
378 | interface Font {
379 | fontName: FontName
380 | }
381 |
382 | type Reaction = { action: Action, trigger: Trigger }
383 |
384 | type Action =
385 | { readonly type: "BACK" | "CLOSE" } |
386 | { readonly type: "URL", url: string } |
387 | { readonly type: "NODE"
388 | readonly destinationId: string | null
389 | readonly navigation: Navigation
390 | readonly transition: Transition | null
391 | readonly preserveScrollPosition: boolean
392 |
393 | // Only present if navigation == "OVERLAY" and the destination uses
394 | // overlay position type "RELATIVE"
395 | readonly overlayRelativePosition?: Vector
396 | }
397 |
398 | interface SimpleTransition {
399 | readonly type: "DISSOLVE" | "SMART_ANIMATE"
400 | readonly easing: Easing
401 | readonly duration: number
402 | }
403 |
404 | interface DirectionalTransition {
405 | readonly type: "MOVE_IN" | "MOVE_OUT" | "PUSH" | "SLIDE_IN" | "SLIDE_OUT"
406 | readonly direction: "LEFT" | "RIGHT" | "TOP" | "BOTTOM"
407 | readonly matchLayers: boolean
408 |
409 | readonly easing: Easing
410 | readonly duration: number
411 | }
412 |
413 | type Transition = SimpleTransition | DirectionalTransition
414 |
415 | type Trigger =
416 | { readonly type: "ON_CLICK" | "ON_HOVER" | "ON_PRESS" | "ON_DRAG" } |
417 | { readonly type: "AFTER_TIMEOUT", readonly timeout: number } |
418 | { readonly type: "MOUSE_ENTER" | "MOUSE_LEAVE" | "MOUSE_UP" | "MOUSE_DOWN"
419 | readonly delay: number
420 | }
421 |
422 | type Navigation = "NAVIGATE" | "SWAP" | "OVERLAY"
423 |
424 | interface Easing {
425 | readonly type: "EASE_IN" | "EASE_OUT" | "EASE_IN_AND_OUT" | "LINEAR"
426 | }
427 |
428 | type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH"
429 |
430 | type OverlayPositionType = "CENTER" | "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_CENTER" | "BOTTOM_RIGHT" | "MANUAL"
431 |
432 | type OverlayBackground =
433 | { readonly type: "NONE" } |
434 | { readonly type: "SOLID_COLOR", readonly color: RGBA }
435 |
436 | type OverlayBackgroundInteraction = "NONE" | "CLOSE_ON_CLICK_OUTSIDE"
437 |
438 | ////////////////////////////////////////////////////////////////////////////////
439 | // Mixins
440 |
441 | interface BaseNodeMixin {
442 | readonly id: string
443 | readonly parent: (BaseNode & ChildrenMixin) | null
444 | name: string // Note: setting this also sets `autoRename` to false on TextNodes
445 | readonly removed: boolean
446 | toString(): string
447 | remove(): void
448 |
449 | getPluginData(key: string): string
450 | setPluginData(key: string, value: string): void
451 |
452 | // Namespace is a string that must be at least 3 alphanumeric characters, and should
453 | // be a name related to your plugin. Other plugins will be able to read this data.
454 | getSharedPluginData(namespace: string, key: string): string
455 | setSharedPluginData(namespace: string, key: string, value: string): void
456 | setRelaunchData(data: { [command: string]: /* description */ string }): void
457 | }
458 |
459 | interface SceneNodeMixin {
460 | visible: boolean
461 | locked: boolean
462 | }
463 |
464 | interface ChildrenMixin {
465 | readonly children: ReadonlyArray
466 |
467 | appendChild(child: SceneNode): void
468 | insertChild(index: number, child: SceneNode): void
469 |
470 | findChildren(callback?: (node: SceneNode) => boolean): SceneNode[]
471 | findChild(callback: (node: SceneNode) => boolean): SceneNode | null
472 |
473 | /**
474 | * If you only need to search immediate children, it is much faster
475 | * to call node.children.filter(callback) or node.findChildren(callback)
476 | */
477 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[]
478 |
479 | /**
480 | * If you only need to search immediate children, it is much faster
481 | * to call node.children.find(callback) or node.findChild(callback)
482 | */
483 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null
484 | }
485 |
486 | interface ConstraintMixin {
487 | constraints: Constraints
488 | }
489 |
490 | interface LayoutMixin {
491 | readonly absoluteTransform: Transform
492 | relativeTransform: Transform
493 | x: number
494 | y: number
495 | rotation: number // In degrees
496 |
497 | readonly width: number
498 | readonly height: number
499 | constrainProportions: boolean
500 |
501 | layoutAlign: "MIN" | "CENTER" | "MAX" | "STRETCH" // applicable only inside auto-layout frames
502 |
503 | resize(width: number, height: number): void
504 | resizeWithoutConstraints(width: number, height: number): void
505 | }
506 |
507 | interface BlendMixin {
508 | opacity: number
509 | blendMode: BlendMode
510 | isMask: boolean
511 | effects: ReadonlyArray
512 | effectStyleId: string
513 | }
514 |
515 | interface ContainerMixin {
516 | expanded: boolean
517 | backgrounds: ReadonlyArray // DEPRECATED: use 'fills' instead
518 | backgroundStyleId: string // DEPRECATED: use 'fillStyleId' instead
519 | }
520 |
521 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL"
522 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND"
523 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH"
524 |
525 | interface GeometryMixin {
526 | fills: ReadonlyArray | PluginAPI['mixed']
527 | strokes: ReadonlyArray
528 | strokeWeight: number
529 | strokeMiterLimit: number
530 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE"
531 | strokeCap: StrokeCap | PluginAPI['mixed']
532 | strokeJoin: StrokeJoin | PluginAPI['mixed']
533 | dashPattern: ReadonlyArray
534 | fillStyleId: string | PluginAPI['mixed']
535 | strokeStyleId: string
536 | outlineStroke(): VectorNode | null
537 | }
538 |
539 | interface CornerMixin {
540 | cornerRadius: number | PluginAPI['mixed']
541 | cornerSmoothing: number
542 | }
543 |
544 | interface RectangleCornerMixin {
545 | topLeftRadius: number
546 | topRightRadius: number
547 | bottomLeftRadius: number
548 | bottomRightRadius: number
549 | }
550 |
551 | interface ExportMixin {
552 | exportSettings: ReadonlyArray
553 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format
554 | }
555 |
556 | interface ReactionMixin {
557 | readonly reactions: ReadonlyArray
558 | }
559 |
560 | interface DefaultShapeMixin extends
561 | BaseNodeMixin, SceneNodeMixin, ReactionMixin,
562 | BlendMixin, GeometryMixin, LayoutMixin,
563 | ExportMixin {
564 | }
565 |
566 | interface DefaultFrameMixin extends
567 | BaseNodeMixin, SceneNodeMixin, ReactionMixin,
568 | ChildrenMixin, ContainerMixin,
569 | GeometryMixin, CornerMixin, RectangleCornerMixin,
570 | BlendMixin, ConstraintMixin, LayoutMixin,
571 | ExportMixin {
572 |
573 | layoutMode: "NONE" | "HORIZONTAL" | "VERTICAL"
574 | counterAxisSizingMode: "FIXED" | "AUTO" // applicable only if layoutMode != "NONE"
575 | horizontalPadding: number // applicable only if layoutMode != "NONE"
576 | verticalPadding: number // applicable only if layoutMode != "NONE"
577 | itemSpacing: number // applicable only if layoutMode != "NONE"
578 |
579 | layoutGrids: ReadonlyArray
580 | gridStyleId: string
581 | clipsContent: boolean
582 | guides: ReadonlyArray
583 |
584 | overflowDirection: OverflowDirection
585 | numberOfFixedChildren: number
586 |
587 | readonly overlayPositionType: OverlayPositionType
588 | readonly overlayBackground: OverlayBackground
589 | readonly overlayBackgroundInteraction: OverlayBackgroundInteraction
590 | }
591 |
592 | ////////////////////////////////////////////////////////////////////////////////
593 | // Nodes
594 |
595 | interface DocumentNode extends BaseNodeMixin {
596 | readonly type: "DOCUMENT"
597 |
598 | readonly children: ReadonlyArray
599 |
600 | appendChild(child: PageNode): void
601 | insertChild(index: number, child: PageNode): void
602 | findChildren(callback?: (node: PageNode) => boolean): Array
603 | findChild(callback: (node: PageNode) => boolean): PageNode | null
604 |
605 | /**
606 | * If you only need to search immediate children, it is much faster
607 | * to call node.children.filter(callback) or node.findChildren(callback)
608 | */
609 | findAll(callback?: (node: PageNode | SceneNode) => boolean): Array
610 |
611 | /**
612 | * If you only need to search immediate children, it is much faster
613 | * to call node.children.find(callback) or node.findChild(callback)
614 | */
615 | findOne(callback: (node: PageNode | SceneNode) => boolean): PageNode | SceneNode | null
616 | }
617 |
618 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin {
619 |
620 | readonly type: "PAGE"
621 | clone(): PageNode
622 |
623 | guides: ReadonlyArray
624 | selection: ReadonlyArray
625 | selectedTextRange: { node: TextNode, start: number, end: number } | null
626 |
627 | backgrounds: ReadonlyArray
628 |
629 | readonly prototypeStartNode: FrameNode | GroupNode | ComponentNode | InstanceNode | null
630 | }
631 |
632 | interface FrameNode extends DefaultFrameMixin {
633 | readonly type: "FRAME"
634 | clone(): FrameNode
635 | }
636 |
637 | interface GroupNode extends
638 | BaseNodeMixin, SceneNodeMixin, ReactionMixin,
639 | ChildrenMixin, ContainerMixin, BlendMixin,
640 | LayoutMixin, ExportMixin {
641 |
642 | readonly type: "GROUP"
643 | clone(): GroupNode
644 | }
645 |
646 | interface SliceNode extends
647 | BaseNodeMixin, SceneNodeMixin, LayoutMixin,
648 | ExportMixin {
649 |
650 | readonly type: "SLICE"
651 | clone(): SliceNode
652 | }
653 |
654 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin, RectangleCornerMixin {
655 | readonly type: "RECTANGLE"
656 | clone(): RectangleNode
657 | }
658 |
659 | interface LineNode extends DefaultShapeMixin, ConstraintMixin {
660 | readonly type: "LINE"
661 | clone(): LineNode
662 | }
663 |
664 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
665 | readonly type: "ELLIPSE"
666 | clone(): EllipseNode
667 | arcData: ArcData
668 | }
669 |
670 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
671 | readonly type: "POLYGON"
672 | clone(): PolygonNode
673 | pointCount: number
674 | }
675 |
676 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
677 | readonly type: "STAR"
678 | clone(): StarNode
679 | pointCount: number
680 | innerRadius: number
681 | }
682 |
683 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
684 | readonly type: "VECTOR"
685 | clone(): VectorNode
686 | vectorNetwork: VectorNetwork
687 | vectorPaths: VectorPaths
688 | handleMirroring: HandleMirroring | PluginAPI['mixed']
689 | }
690 |
691 | interface TextNode extends DefaultShapeMixin, ConstraintMixin {
692 | readonly type: "TEXT"
693 | clone(): TextNode
694 | readonly hasMissingFont: boolean
695 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED"
696 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM"
697 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT"
698 | paragraphIndent: number
699 | paragraphSpacing: number
700 | autoRename: boolean
701 |
702 | textStyleId: string | PluginAPI['mixed']
703 | fontSize: number | PluginAPI['mixed']
704 | fontName: FontName | PluginAPI['mixed']
705 | textCase: TextCase | PluginAPI['mixed']
706 | textDecoration: TextDecoration | PluginAPI['mixed']
707 | letterSpacing: LetterSpacing | PluginAPI['mixed']
708 | lineHeight: LineHeight | PluginAPI['mixed']
709 |
710 | characters: string
711 | insertCharacters(start: number, characters: string, useStyle?: "BEFORE" | "AFTER"): void
712 | deleteCharacters(start: number, end: number): void
713 |
714 | getRangeFontSize(start: number, end: number): number | PluginAPI['mixed']
715 | setRangeFontSize(start: number, end: number, value: number): void
716 | getRangeFontName(start: number, end: number): FontName | PluginAPI['mixed']
717 | setRangeFontName(start: number, end: number, value: FontName): void
718 | getRangeTextCase(start: number, end: number): TextCase | PluginAPI['mixed']
719 | setRangeTextCase(start: number, end: number, value: TextCase): void
720 | getRangeTextDecoration(start: number, end: number): TextDecoration | PluginAPI['mixed']
721 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void
722 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | PluginAPI['mixed']
723 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void
724 | getRangeLineHeight(start: number, end: number): LineHeight | PluginAPI['mixed']
725 | setRangeLineHeight(start: number, end: number, value: LineHeight): void
726 | getRangeFills(start: number, end: number): Paint[] | PluginAPI['mixed']
727 | setRangeFills(start: number, end: number, value: Paint[]): void
728 | getRangeTextStyleId(start: number, end: number): string | PluginAPI['mixed']
729 | setRangeTextStyleId(start: number, end: number, value: string): void
730 | getRangeFillStyleId(start: number, end: number): string | PluginAPI['mixed']
731 | setRangeFillStyleId(start: number, end: number, value: string): void
732 | }
733 |
734 | interface ComponentNode extends DefaultFrameMixin {
735 | readonly type: "COMPONENT"
736 | clone(): ComponentNode
737 |
738 | createInstance(): InstanceNode
739 | description: string
740 | readonly remote: boolean
741 | readonly key: string // The key to use with "importComponentByKeyAsync"
742 | }
743 |
744 | interface InstanceNode extends DefaultFrameMixin {
745 | readonly type: "INSTANCE"
746 | clone(): InstanceNode
747 | masterComponent: ComponentNode
748 | scaleFactor: number
749 | }
750 |
751 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin {
752 | readonly type: "BOOLEAN_OPERATION"
753 | clone(): BooleanOperationNode
754 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE"
755 |
756 | expanded: boolean
757 | }
758 |
759 | type BaseNode =
760 | DocumentNode |
761 | PageNode |
762 | SceneNode
763 |
764 | type SceneNode =
765 | SliceNode |
766 | FrameNode |
767 | GroupNode |
768 | ComponentNode |
769 | InstanceNode |
770 | BooleanOperationNode |
771 | VectorNode |
772 | StarNode |
773 | LineNode |
774 | EllipseNode |
775 | PolygonNode |
776 | RectangleNode |
777 | TextNode
778 |
779 | type NodeType =
780 | "DOCUMENT" |
781 | "PAGE" |
782 | "SLICE" |
783 | "FRAME" |
784 | "GROUP" |
785 | "COMPONENT" |
786 | "INSTANCE" |
787 | "BOOLEAN_OPERATION" |
788 | "VECTOR" |
789 | "STAR" |
790 | "LINE" |
791 | "ELLIPSE" |
792 | "POLYGON" |
793 | "RECTANGLE" |
794 | "TEXT"
795 |
796 | ////////////////////////////////////////////////////////////////////////////////
797 | // Styles
798 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID"
799 |
800 | interface BaseStyle {
801 | readonly id: string
802 | readonly type: StyleType
803 | name: string
804 | description: string
805 | remote: boolean
806 | readonly key: string // The key to use with "importStyleByKeyAsync"
807 | remove(): void
808 | }
809 |
810 | interface PaintStyle extends BaseStyle {
811 | type: "PAINT"
812 | paints: ReadonlyArray
813 | }
814 |
815 | interface TextStyle extends BaseStyle {
816 | type: "TEXT"
817 | fontSize: number
818 | textDecoration: TextDecoration
819 | fontName: FontName
820 | letterSpacing: LetterSpacing
821 | lineHeight: LineHeight
822 | paragraphIndent: number
823 | paragraphSpacing: number
824 | textCase: TextCase
825 | }
826 |
827 | interface EffectStyle extends BaseStyle {
828 | type: "EFFECT"
829 | effects: ReadonlyArray
830 | }
831 |
832 | interface GridStyle extends BaseStyle {
833 | type: "GRID"
834 | layoutGrids: ReadonlyArray
835 | }
836 |
837 | ////////////////////////////////////////////////////////////////////////////////
838 | // Other
839 |
840 | interface Image {
841 | readonly hash: string
842 | getBytesAsync(): Promise
843 | }
844 | } // declare global
845 |
846 | export {}
847 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 |
2 | function convertArrayToObject(array, value = undefined) {
3 | return array.reduce(function (obj, name) {
4 | obj[name] = value;
5 | return obj;
6 | }, {})
7 | }
8 |
9 | // Doesn't work because the component has already been added to the component list by the time the walker gets to the children nodes
10 | export function isInsideComponentThatAlreadyExists(node, allComponents) {
11 | if (node.type === "PAGE") return false
12 |
13 | if (allComponents.some((component) => JSON.stringify(component) === JSON.stringify(node.parent))) {
14 | return true
15 | }
16 | else {
17 | return isInsideComponentThatAlreadyExists(node.parent, allComponents)
18 | }
19 |
20 |
21 | }
22 |
23 | export function isTopLevelInstance(node) {
24 | if (node !== null) {
25 | if (isNestedInstance(node.parent)) {
26 | return true
27 | }
28 | else {
29 | return false
30 | }
31 | }
32 | }
33 |
34 | export function isNestedComponentSet(node) {
35 | if (node !== null) {
36 | if (node.type === "COMPONENT_SET") {
37 | return true
38 | }
39 | else if (node.type === "PAGE") {
40 | return false
41 | }
42 | else {
43 | return isNestedComponentSet(node.parent)
44 | }
45 | }
46 | }
47 |
48 | function isInsideComponent(node: SceneNode): boolean {
49 | const parent = node.parent
50 | if (parent.type === 'COMPONENT') {
51 | return true
52 | } else if (parent.type === 'PAGE') {
53 | return false
54 | } else {
55 | return isInsideComponent(parent as SceneNode)
56 | }
57 | }
58 |
59 | export function getComponentParent(node) {
60 | if (node?.type === "COMPONENT") return node
61 | if (node?.type === "PAGE") return null
62 | if (node?.parent?.type === "COMPONENT") {
63 | return node.parent
64 | }
65 | else {
66 | return getComponentParent(node?.parent)
67 | }
68 | }
69 |
70 | export function getParentInstances(node, instances = []) {
71 | if (node.type === "PAGE") return null
72 | if (node.type === "INSTANCE") {
73 | instances.push(node)
74 | }
75 | if (isInsideInstance(node)) {
76 | return getParentInstances(node.parent, instances)
77 | } else {
78 | return instances
79 | }
80 | }
81 |
82 | export function putValuesIntoArray(value) {
83 | return Array.isArray(value) ? value : [value]
84 | }
85 |
86 | function isFunction(functionToCheck) {
87 | return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
88 | }
89 |
90 | const nodeProps: string[] = [
91 | 'id',
92 | 'parent',
93 | 'name',
94 | 'removed',
95 | 'visible',
96 | 'locked',
97 | 'children',
98 | 'constraints',
99 | 'absoluteTransform',
100 | 'relativeTransform',
101 | 'x',
102 | 'y',
103 | 'rotation',
104 | 'width',
105 | 'height',
106 | 'constrainProportions',
107 | 'layoutAlign',
108 | 'layoutGrow',
109 | 'opacity',
110 | 'blendMode',
111 | 'isMask',
112 | 'effects',
113 | 'effectStyleId',
114 | 'expanded',
115 | 'backgrounds',
116 | 'backgroundStyleId',
117 | 'fills',
118 | 'strokes',
119 | 'strokeWeight',
120 | 'strokeMiterLimit',
121 | 'strokeAlign',
122 | 'strokeCap',
123 | 'strokeJoin',
124 | 'dashPattern',
125 | 'fillStyleId',
126 | 'strokeStyleId',
127 | 'cornerRadius',
128 | 'cornerSmoothing',
129 | 'topLeftRadius',
130 | 'topRightRadius',
131 | 'bottomLeftRadius',
132 | 'bottomRightRadius',
133 | 'exportSettings',
134 | 'overflowDirection',
135 | 'numberOfFixedChildren',
136 | 'overlayPositionType',
137 | 'overlayBackground',
138 | 'overlayBackgroundInteraction',
139 | 'reactions',
140 | 'description',
141 | 'remote',
142 | 'key',
143 | 'layoutMode',
144 | 'primaryAxisSizingMode',
145 | 'counterAxisSizingMode',
146 | 'primaryAxisAlignItems',
147 | 'counterAxisAlignItems',
148 | 'paddingLeft',
149 | 'paddingRight',
150 | 'paddingTop',
151 | 'paddingBottom',
152 | 'itemSpacing',
153 | // 'horizontalPadding',
154 | // 'verticalPadding',
155 | 'layoutGrids',
156 | 'gridStyleId',
157 | 'clipsContent',
158 | 'guides'
159 | ]
160 |
161 | const readOnly: string[] = [
162 | 'id',
163 | 'parent',
164 | 'removed',
165 | 'children',
166 | 'absoluteTransform',
167 | 'width',
168 | 'height',
169 | 'overlayPositionType',
170 | 'overlayBackground',
171 | 'overlayBackgroundInteraction',
172 | 'reactions',
173 | 'remote',
174 | 'key',
175 | 'type'
176 | ]
177 |
178 | const instanceProps: string[] = [
179 | 'rotation',
180 | 'constrainProportions'
181 | ]
182 |
183 | const defaults: string[] = [
184 | 'name',
185 | 'guides',
186 | 'description',
187 | 'remote',
188 | 'key',
189 | 'reactions',
190 | 'x',
191 | 'y',
192 | 'exportSettings',
193 | 'expanded',
194 | 'isMask',
195 | 'exportSettings',
196 | 'overflowDirection',
197 | 'numberOfFixedChildren',
198 | 'constraints',
199 | 'relativeTransform'
200 | ]
201 |
202 | const mixedProp = {
203 | cornerRadius: [
204 | 'topleftCornerRadius',
205 | 'topRightCornerRadius',
206 | 'bottomLeftCornerRadius',
207 | 'bottomRightCornerRadius']
208 | }
209 |
210 | function applyMixedValues(node, prop) {
211 |
212 |
213 | const obj = {};
214 |
215 | if (mixedProp[prop] && node[prop] === figma.mixed) {
216 | for (let prop of mixedProp[prop]) {
217 | obj[prop] = source[prop]
218 | }
219 | } else {
220 | obj[prop] = node[prop]
221 | }
222 | }
223 |
224 |
225 | export const nodeToObject = (node: any, withoutRelations?: boolean, removeConflicts?: boolean) => {
226 | const props = Object.entries(Object.getOwnPropertyDescriptors(node.__proto__))
227 | const blacklist = ['parent', 'children', 'removed', 'masterComponent', 'horizontalPadding', 'verticalPadding']
228 | const obj: any = { id: node.id, type: node.type }
229 | for (const [name, prop] of props) {
230 | if (prop.get && !blacklist.includes(name)) {
231 | try {
232 | if (typeof obj[name] === 'symbol') {
233 | obj[name] = 'Mixed'
234 | } else {
235 | obj[name] = prop.get.call(node)
236 | }
237 | } catch (err) {
238 | obj[name] = undefined
239 | }
240 | }
241 | }
242 | if (node.parent && !withoutRelations) {
243 | obj.parent = { id: node.parent.id, type: node.parent.type }
244 | }
245 | if (node.children && !withoutRelations) {
246 | obj.children = node.children.map((child: any) => nodeToObject(child, withoutRelations))
247 | }
248 | if (node.masterComponent && !withoutRelations) {
249 | obj.masterComponent = nodeToObject(node.masterComponent, withoutRelations)
250 | }
251 |
252 | if (!removeConflicts) {
253 | !obj.fillStyleId && obj.fills ? delete obj.fillStyleId : delete obj.fills
254 | !obj.strokeStyleId && obj.strokes ? delete obj.strokeStyleId : delete obj.strokes
255 | !obj.backgroundStyleId && obj.backgrounds ? delete obj.backgroundStyleId : delete obj.backgrounds
256 | !obj.effectStyleId && obj.effects ? delete obj.effectStyleId : delete obj.effects
257 | !obj.gridStyleId && obj.layoutGrids ? delete obj.gridStyleId : delete obj.layoutGrids
258 |
259 | if (obj.textStyleId) {
260 | delete obj.fontName
261 | delete obj.fontSize
262 | delete obj.letterSpacing
263 | delete obj.lineHeight
264 | delete obj.paragraphIndent
265 | delete obj.paragraphSpacing
266 | delete obj.textCase
267 | delete obj.textDecoration
268 | }
269 | else {
270 | delete obj.textStyleId
271 | }
272 |
273 | if (obj.cornerRadius !== figma.mixed) {
274 | delete obj.topLeftRadius
275 | delete obj.topRightRadius
276 | delete obj.bottomLeftRadius
277 | delete obj.bottomRightRadius
278 | }
279 | else {
280 | delete obj.cornerRadius
281 | }
282 | }
283 |
284 | return obj
285 | }
286 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './PluginUI';
2 |
3 | const app = new App({
4 | target: document.body,
5 | });
6 |
7 | export default app;
--------------------------------------------------------------------------------
/src/pluginGeneration.ts:
--------------------------------------------------------------------------------
1 | import v from "voca";
2 | import Str from "./str";
3 | import {
4 | getInstanceCounterpart,
5 | getTopInstance,
6 | getInstanceCounterpartUsingLocation,
7 | getOverrides,
8 | getNodeDepth,
9 | getParentInstance,
10 | getNoneGroupParent,
11 | isInsideInstance,
12 | } from "@fignite/helpers";
13 | import { putValuesIntoArray, nodeToObject } from "./helpers";
14 | import {
15 | allowedProps,
16 | defaultPropValues,
17 | textProps,
18 | styleProps,
19 | } from "./props";
20 |
21 | // TODO: Check for properties that can't be set on instances or nodes inside instances
22 | // TODO: walkNodes and string API could be improved
23 | // TODO: Fix mirror hangding null in vectors
24 | // TODO: Some issues with auto layout, grow 1. These need to be applied to children after all children have been created.
25 | // TODO: How to check for missing fonts
26 | // TODO: Add support for images
27 | // TODO: Find a way to handle exponential numbers better
28 |
29 | // function getParentInstances(node, instances = []) {
30 | // const parent = node.parent
31 | // if (node.type === "PAGE") return null
32 | // if (parent.type === "INSTANCE") {
33 | // instances.push(parent)
34 | // }
35 | // if (isInsideInstance(node)) {
36 | // return getParentInstances(node.parent, instances)
37 | // } else {
38 | // return instances
39 | // }
40 | // }
41 |
42 | function getParentInstances(node, instances = []) {
43 | if (node.type === "PAGE") return null;
44 | if (node.parent.type === "INSTANCE") {
45 | instances.push(node.parent);
46 | }
47 | if (isInsideInstance(node)) {
48 | return getParentInstances(node.parent, instances);
49 | } else {
50 | return instances;
51 | }
52 | }
53 |
54 | function getParentComponents(node, instances = []) {
55 | if (node.type === "PAGE") return null;
56 | if (node.parent.type === "INSTANCE") {
57 | instances.push(node.parent.mainComponent);
58 | }
59 | if (isInsideInstance(node)) {
60 | return getParentInstances(node.parent, instances);
61 | } else {
62 | return instances;
63 | }
64 | }
65 |
66 | function Utf8ArrayToStr(array) {
67 | var out, i, len, c;
68 | var char2, char3;
69 |
70 | out = "";
71 | len = array.length;
72 | i = 0;
73 | while (i < len) {
74 | c = array[i++];
75 | switch (c >> 4) {
76 | case 0:
77 | case 1:
78 | case 2:
79 | case 3:
80 | case 4:
81 | case 5:
82 | case 6:
83 | case 7:
84 | // 0xxxxxxx
85 | out += String.fromCharCode(c);
86 | break;
87 | case 12:
88 | case 13:
89 | // 110x xxxx 10xx xxxx
90 | char2 = array[i++];
91 | out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
92 | break;
93 | case 14:
94 | // 1110 xxxx 10xx xxxx 10xx xxxx
95 | char2 = array[i++];
96 | char3 = array[i++];
97 | out += String.fromCharCode(
98 | ((c & 0x0f) << 12) |
99 | ((char2 & 0x3f) << 6) |
100 | ((char3 & 0x3f) << 0)
101 | );
102 | break;
103 | }
104 | }
105 |
106 | return out;
107 | }
108 |
109 | function toArrayBuffer(buf) {
110 | var ab = new ArrayBuffer(buf.length);
111 | var view = new Uint8Array(ab);
112 | for (var i = 0; i < buf.length; ++i) {
113 | view[i] = buf[i];
114 | }
115 | console.log(ab);
116 | return ab;
117 | }
118 |
119 | function toBuffer(ab) {
120 | var buf = Buffer.alloc(ab.byteLength);
121 | var view = new Uint8Array(ab);
122 | for (var i = 0; i < buf.length; ++i) {
123 | buf[i] = view[i];
124 | }
125 | console.log(buf);
126 | return buf;
127 | }
128 |
129 | // FIXME: vectorNetwork and vectorPath cannot be over ridden on an instance
130 |
131 | function simpleClone(val) {
132 | return JSON.parse(JSON.stringify(val));
133 | }
134 |
135 | export async function genPluginStr(origSel, opts?) {
136 | var str = new Str();
137 |
138 | var fonts;
139 | var allComponents = [];
140 | var discardNodes = [];
141 |
142 | var styles = {};
143 | var images = [];
144 | // console.log(styles)
145 |
146 | // Provides a reference for the node when printed as a string
147 | function Ref(nodes) {
148 | var result = [];
149 |
150 | // TODO: Needs to somehow replace parent node references of selection with figma.currentPage
151 | // result.push(v.camelCase(node.type) + node.id.replace(/\:|\;/g, "_"))
152 |
153 | nodes = putValuesIntoArray(nodes);
154 |
155 | for (var i = 0; i < nodes.length; i++) {
156 | var node = nodes[i];
157 |
158 | // console.log("node", node)
159 |
160 | if (node) {
161 | // TODO: Needs to check if node exists inside selection
162 | // figma.currentPage.selection.some((item) => item === node)
163 | // function nodeExistsInSel(nodes = figma.currentPage.selection) {
164 | // for (var i = 0; i < nodes.length; i++) {
165 | // var sourceNode = nodes[i]
166 | // if (sourceNode.id === node.id) {
167 | // return true
168 | // }
169 | // else if (sourceNode.children) {
170 | // return nodeExistsInSel(sourceNode.children)
171 | // }
172 | // }
173 | // }
174 |
175 | // console.log(node.name, node.id, node.type, nodeExistsInSel())
176 |
177 | if (node.type === "PAGE") {
178 | result.push("figma.currentPage");
179 | } else {
180 | // If node is nested inside an instance it needs another reference
181 | // if (isInsideInstance(node)) {
182 | // result.push(`figma.getNodeById("I" + ${Ref(node.parent)}.id + ";" + ${Ref(node.parent.mainComponent.children[getNodeIndex(node)])}.id)`)
183 | // }
184 | // else {
185 | // result.push(v.camelCase(node.type) + "_" + node.id.replace(/\:|\;/g, "_") + "_" + node.name.replace(/\:|\;|\/|=/g, "_"))
186 | result.push(
187 | v.camelCase(node.type) +
188 | "_" +
189 | node.id.replace(/\:|\;/g, "_")
190 | );
191 | // }
192 | }
193 | }
194 | }
195 |
196 | if (result.length === 1) result = result[0];
197 |
198 | return result;
199 | }
200 |
201 | function StyleRef(style) {
202 | return (
203 | v.lowerCase(style.name.replace(/\s|\//g, "_").replace(/\./g, "")) +
204 | "_" +
205 | style.key.slice(-4)
206 | );
207 | }
208 |
209 | // A function that lets you loop through each node and their children, it provides callbacks to reference different parts of the loops life cycle, before, during, or after the loop.
210 |
211 | function walkNodes(nodes, callback?, parent?, selection?, level?) {
212 | let node;
213 |
214 | for (var i = 0; i < nodes.length; i++) {
215 | if (!parent) {
216 | selection = i;
217 | level = 0;
218 | }
219 |
220 | node = nodes[i];
221 |
222 | // console.log(node.type)
223 | // If main component doesn't exist in document then add it to list to be created
224 | // Don't think this does anything
225 | // if (node.type === "COMPONENT" && node.parent === null) {
226 | // // console.log(node.type, node.mainComponent, node.name, node)
227 | // // FIXME: Don't create a clone becuase this will give it a diffrent id. Instead add it to the page so it can be picked up? Need to then remove it again to clean up the document? Might be better to see where this parent is used and subsitute with `figma.currentPage`
228 | // // console.log(node.name)
229 |
230 | // // If component can't be added to page, then it is from an external library
231 | // // Why am I adding it to the page again?
232 | // try {
233 | // // figma.currentPage.appendChild(node)
234 | // }
235 | // catch (error) {
236 | // // node = node.clone()
237 | // }
238 |
239 | // // discardNodes.push(node)
240 | // }
241 |
242 | let sel = selection; // Index of which top level array the node lives in
243 | let ref = node.type?.toLowerCase() + (i + 1 + level - sel); // Trying to find a way to create a unique identifier based on where node lives in structure
244 |
245 | if (!parent) parent = "figma.currentPage";
246 |
247 | // These may not be needed now
248 | var obj = {
249 | ref,
250 | level, // How deep down the node lives in the array structure
251 | sel,
252 | parent,
253 | };
254 |
255 | var stop = false;
256 | if (callback.during) {
257 | // console.log("sibling node", node)
258 | // If boolean value of true returned from createBasic() then this sets a flag to stop iterating children in node
259 | // console.log("node being traversed", node)
260 |
261 | stop = callback.during(node, obj);
262 | }
263 |
264 | if (node.children) {
265 | ++level;
266 |
267 | if (stop && nodes[i + 1]) {
268 | // Iterate the next node
269 | // ++i
270 |
271 | walkNodes([nodes[i + 1]], callback, ref, selection, level);
272 | } else {
273 | walkNodes(node.children, callback, ref, selection, level);
274 | }
275 |
276 | if (callback.after) {
277 | callback.after(node, obj);
278 | }
279 | }
280 | }
281 | }
282 |
283 | function isInstanceDefaultVariant(node) {
284 | if (node.type === "INSTANCE") {
285 | var isInstanceDefaultVariant = true;
286 | var componentSet = node.mainComponent.parent;
287 | if (componentSet) {
288 | if (componentSet.type === "COMPONENT_SET") {
289 | if (
290 | componentSet !== null &&
291 | componentSet.type === "COMPONENT_SET"
292 | ) {
293 | var defaultVariant = componentSet.defaultVariant;
294 |
295 | if (
296 | defaultVariant &&
297 | defaultVariant.id !== node.mainComponent.id
298 | ) {
299 | isInstanceDefaultVariant = false;
300 | }
301 | }
302 |
303 | return isInstanceDefaultVariant;
304 | }
305 | } else {
306 | return false;
307 | }
308 | } else {
309 | // Returns true because is not an instance and therefor should pass
310 | // TODO: Consider changing function to hasComponentBeenSwapped or something similar
311 | return true;
312 | }
313 | }
314 |
315 | function componentHasBeenSwapped(node) {
316 | if (node.type === "INSTANCE") {
317 | if (node.mainComponent.parent) {
318 | if (node.mainComponent.parent.type === "COMPONENT_SET") {
319 | var componentBeenSwapped = false;
320 | var componentSet = node.mainComponent.parent;
321 | if (
322 | componentSet !== null &&
323 | componentSet.type === "COMPONENT_SET"
324 | ) {
325 | var defaultVariant = componentSet.defaultVariant;
326 |
327 | if (
328 | defaultVariant &&
329 | defaultVariant.id !== node.mainComponent.id
330 | ) {
331 | componentBeenSwapped = true;
332 | }
333 | }
334 |
335 | return componentBeenSwapped;
336 | }
337 | }
338 | }
339 | }
340 |
341 | function collectImageHash(node) {
342 | if ("fills" in node) {
343 | for (var i = 0; i < node.fills.length; i++) {
344 | var fill = node.fills[i];
345 | if (fill.type === "IMAGE") {
346 | images.push(fill.imageHash);
347 | }
348 | }
349 | }
350 | }
351 |
352 | function replaceImageHasWithRef(node) {
353 | if ("fills" in node) {
354 | var fills = simpleClone(node.fills);
355 | for (var i = 0; i < fills.length; i++) {
356 | var fill = fills[i];
357 | if (fill.type === "IMAGE") {
358 | images.push({ imageHash: fill.imageHash, node });
359 | fill.imageHash = `${Ref(node)}_image.imageHash`;
360 | }
361 | }
362 | return fills;
363 | }
364 | }
365 |
366 | // async function createImageHash(node) {
367 | // if ('fills' in node) {
368 | // for (var i = 0; i < node.fills.length; i++) {
369 | // var fill = node.fills[i]
370 | // if (fill.type === "IMAGE") {
371 | // // figma.getImageByHash(fill.imageHash).getBytesAsync().then((image) => {
372 | // // str`
373 | // // // Create IMAGE HASH
374 | // // var ${Ref(node)}_image_hash = ${image}\n
375 | // // `
376 | // // })
377 |
378 | // return figma.getImageByHash(fill.imageHash).getBytesAsync()
379 | // }
380 | // }
381 | // }
382 | // }
383 |
384 | // createImageHash(node).then((image) => {
385 | // str`
386 | // // // Create IMAGE HASH
387 | // // var ${Ref(node)}_image_hash = ${image}\n
388 | // // `
389 |
390 | // })
391 |
392 | function createProps(node, level, options = {}, mainComponent?) {
393 | var string = "";
394 | var staticPropsStr = "";
395 | var textPropsString = "";
396 | var fontsString = "";
397 | var hasText;
398 | var hasWidthOrHeight = true;
399 |
400 | // collectImageHash(node)
401 |
402 | var object = node.__proto__ ? nodeToObject(node) : node;
403 |
404 | for (let [name, value] of Object.entries(object)) {
405 | // }
406 |
407 | // copyPasteProps(nodeToObject(node), ({ obj, name, value }) => {
408 | if (
409 | JSON.stringify(value) !==
410 | JSON.stringify(defaultPropValues[node.type][name]) &&
411 | allowedProps.includes(name) &&
412 | // name !== "key" &&
413 | // name !== "mainComponent" &&
414 | // name !== "absoluteTransform" &&
415 | // name !== "type" &&
416 | // name !== "id" &&
417 | // name !== "parent" &&
418 | // name !== "children" &&
419 | // name !== "masterComponent" &&
420 | // name !== "mainComponent" &&
421 | // name !== "horizontalPadding" &&
422 | // name !== "verticalPadding" &&
423 | // name !== "reactions" &&
424 | // name !== "overlayPositionType" &&
425 | // name !== "overflowDirection" &&
426 | // name !== "numberOfFixedChildren" &&
427 | // name !== "overlayBackground" &&
428 | // name !== "overlayBackgroundInteraction" &&
429 | // name !== "remote" &&
430 | // name !== "defaultVariant" &&
431 | // name !== "hasMissingFont" &&
432 | // name !== "exportSettings" &&
433 | // name !== "variantProperties" &&
434 | // name !== "variantGroupProperties" &&
435 | // name !== "absoluteRenderBounds" &&
436 | // name !== "fillGeometry" &&
437 | // name !== "strokeGeometry" &&
438 | // name !== "stuckNodes" &&
439 | // name !== "componentPropertyReferences" &&
440 | // name !== "canUpgradeToNativeBidiSupport" &&
441 | // name !== "componentPropertyDefinitions" &&
442 | // name !== "componentProperties" &&
443 | // // Investigate these ones
444 | // name !== "itemReverseZIndex" &&
445 | // name !== "strokesIncludedInLayout" &&
446 | !(
447 | (isInsideInstance(node) || node.type === "INSTANCE") &&
448 | name === "vectorNetwork"
449 | ) &&
450 | !(
451 | (isInsideInstance(node) || node.type === "INSTANCE") &&
452 | name === "vectorPaths"
453 | )
454 | ) {
455 | // TODO: ^ Add some of these exclusions to nodeToObject()
456 |
457 | var overriddenProp = true;
458 | var counterSizingIsFixed = false;
459 | var primarySizingIsFixed = false;
460 | var shouldResizeWidth = false;
461 | var shouldResizeHeight = false;
462 |
463 | if (node.type === "INSTANCE" && !isInsideInstance(node)) {
464 | overriddenProp =
465 | JSON.stringify(node[name]) !==
466 | JSON.stringify(mainComponent[name]);
467 | }
468 |
469 | if (node.type === "INSTANCE") {
470 | if (node.width !== node.mainComponent?.width) {
471 | if (node.primaryAxisSizingMode === "FIXED") {
472 | shouldResizeWidth = true;
473 | }
474 | }
475 |
476 | if (node.height !== node.mainComponent?.height) {
477 | if (node.counterAxisSizingMode === "FIXED") {
478 | shouldResizeHeight = true;
479 | }
480 | }
481 | } else {
482 | shouldResizeHeight = true;
483 | shouldResizeWidth = true;
484 | }
485 |
486 | // Applies property overrides of instances (currently only activates characters)
487 | if (isInsideInstance(node)) {
488 | var parentInstance = getParentInstance(node);
489 | // var depthOfNode = getNodeDepth(node, parentInstance)
490 |
491 | // Add these exclusions to getOverrides helper
492 | // if (!('horizontalPadding' in node) || !('verticalPadding' in node)) {
493 |
494 | if (typeof getOverrides(node, name) !== "undefined") {
495 | } else {
496 | overriddenProp = false;
497 | }
498 | // }
499 | }
500 |
501 | if (overriddenProp) {
502 | // Can't override certain properties on nodes which are part of instance
503 | if (
504 | !(
505 | isInsideInstance(node) &&
506 | (name === "x" ||
507 | name === "y" ||
508 | name === "relativeTransform")
509 | )
510 | ) {
511 | // Add resize
512 |
513 | if (options?.resize !== false) {
514 | // FIXME: This is being ignored when default of node is true for width, but not for height
515 | if (
516 | (name === "width" || name === "height") &&
517 | hasWidthOrHeight
518 | ) {
519 | hasWidthOrHeight = false;
520 |
521 | // This checks if the instance is set to fixed sizing, if so it checks if it's different from the main component to determine if it should be resized
522 | if (shouldResizeHeight || shouldResizeWidth) {
523 | // Round widths/heights less than 0.001 to 0.01 because API does not accept less than 0.01 for frames/components/component sets
524 | // Need to round super high relative transform numbers
525 | var width = node.width.toFixed(10);
526 | var height = node.height.toFixed(10);
527 |
528 | // FIXME: Should this apply to all nodes types?
529 | if (
530 | (node.type === "FRAME" ||
531 | node.type === "COMPONENT" ||
532 | node.type === "RECTANGLE" ||
533 | node.type === "INSTANCE") &&
534 | node.width < 0.01
535 | )
536 | width = 0.01;
537 | if (
538 | (node.type === "FRAME" ||
539 | node.type === "COMPONENT" ||
540 | node.type === "RECTANGLE" ||
541 | node.type === "INSTANCE") &&
542 | node.height < 0.01
543 | )
544 | height = 0.01;
545 |
546 | // Lines have to have a height of 0
547 | if (node.type === "LINE") height = 0;
548 |
549 | if (
550 | (node.type === "FRAME" &&
551 | node.width < 0.01) ||
552 | node.height < 0.01
553 | ) {
554 | string += ` ${Ref(
555 | node
556 | )}.resizeWithoutConstraints(${width}, ${height})\n`;
557 | } else {
558 | string += ` ${Ref(
559 | node
560 | )}.resize(${width}, ${height})\n`;
561 | }
562 |
563 | // Need to check for sizing property first because not all nodes have this property eg TEXT, LINE, RECTANGLE
564 | // This is to reset the sizing of either the width of height because it has been overriden by the resize method
565 | if (
566 | node.primaryAxisSizingMode &&
567 | node.primaryAxisSizingMode !== "FIXED"
568 | ) {
569 | string += ` ${Ref(
570 | node
571 | )}.primaryAxisSizingMode = ${JSON.stringify(
572 | node.primaryAxisSizingMode
573 | )}\n`;
574 | }
575 |
576 | if (
577 | node.counterAxisSizingMode &&
578 | node.counterAxisSizingMode !== "FIXED"
579 | ) {
580 | string += ` ${Ref(
581 | node
582 | )}.counterAxisSizingMode = ${JSON.stringify(
583 | node.counterAxisSizingMode
584 | )}\n`;
585 | }
586 | }
587 | }
588 | }
589 |
590 | // If styles
591 |
592 | let style;
593 | if (styleProps.includes(name)) {
594 | var styleId = node[name];
595 | styles[name] = styles[name] || [];
596 |
597 | // Get the style
598 | style = figma.getStyleById(styleId);
599 |
600 | // Push to array if unique
601 | if (
602 | !styles[name].some(
603 | (item) =>
604 | JSON.stringify(item.id) ===
605 | JSON.stringify(style.id)
606 | )
607 | ) {
608 | styles[name].push(style);
609 | }
610 |
611 | // Assign style to node
612 | if (name !== "textStyleId") {
613 | string += ` ${Ref(node)}.${name} = ${StyleRef(
614 | style
615 | )}.id\n`;
616 | }
617 | }
618 |
619 | // If text prop
620 | if (textProps.includes(name)) {
621 | if (name === "textStyleId") {
622 | textPropsString += ` ${Ref(
623 | node
624 | )}.${name} = ${StyleRef(style)}.id\n`;
625 | } else {
626 | textPropsString += ` ${Ref(
627 | node
628 | )}.${name} = ${JSON.stringify(value)}\n`;
629 | }
630 | }
631 |
632 | // If a text node
633 | if (name === "characters") {
634 | hasText = true;
635 | fonts = fonts || [];
636 | if (
637 | !fonts.some(
638 | (item) =>
639 | JSON.stringify(item) ===
640 | JSON.stringify(node.fontName)
641 | )
642 | ) {
643 | fonts.push(node.fontName);
644 | }
645 |
646 | if (node.fontName) {
647 | fontsString += `\
648 | ${Ref(node)}.fontName = {
649 | family: ${JSON.stringify(node.fontName?.family)},
650 | style: ${JSON.stringify(node.fontName?.style)}
651 | }`;
652 | }
653 | }
654 |
655 | if (
656 | name !== "width" &&
657 | name !== "height" &&
658 | !textProps.includes(name) &&
659 | !styleProps.includes(name)
660 | ) {
661 | // FIXME: Need a less messy way to do this on all numbers
662 | // Need to round super high relative transform numbers
663 | if (name === "relativeTransform") {
664 | var newValue = [
665 | [0, 0, 0],
666 | [0, 0, 0],
667 | ];
668 | newValue[0][0] = +value[0][0].toFixed(10);
669 | newValue[0][1] = +value[0][1].toFixed(10);
670 | newValue[0][2] = +value[0][2].toFixed(10);
671 |
672 | newValue[1][0] = +value[1][0].toFixed(10);
673 | newValue[1][1] = +value[1][1].toFixed(10);
674 | newValue[1][2] = +value[1][2].toFixed(10);
675 |
676 | value = newValue;
677 | }
678 | if (options?.[name] !== false) {
679 | // Disabled for now because I'm not sure how to programmatically add images. I think might have to include function to convert bytes to array
680 | // if (name === "fills") {
681 | // var newValueX = JSON.stringify(replaceImageHasWithRef(node))
682 |
683 | // staticPropsStr += `${Ref(node)}.fills = ${newValueX}\n`
684 | // }
685 | // else {
686 | staticPropsStr += ` ${Ref(
687 | node
688 | )}.${name} = ${JSON.stringify(value)}\n`;
689 | // }
690 | }
691 | }
692 | }
693 | }
694 | }
695 | }
696 |
697 | var loadFontsString = "";
698 |
699 | if (hasText) {
700 | loadFontsString = `\n // Font properties
701 | ${fontsString}
702 | ${textPropsString}`;
703 | }
704 |
705 | string += `${staticPropsStr}`;
706 | string += ` ${loadFontsString}`;
707 |
708 | // TODO: Need to create another function for lifecylce of any node and add this to bottom
709 | // if (opts?.includeObject) {
710 | // if (level === 0) {
711 | // string += `nodes.push(${Ref(node)})\n`;
712 | // }
713 | // }
714 | str`${string}`;
715 | }
716 |
717 | function appendNode(node) {
718 | // If parent is a group type node then append to nearest none group parent
719 | if (node.parent) {
720 | if (
721 | node.parent?.type === "BOOLEAN_OPERATION" ||
722 | node.parent?.type === "GROUP"
723 | ) {
724 | str` ${Ref(getNoneGroupParent(node))}.appendChild(${Ref(
725 | node
726 | )})\n`;
727 | } else if (node.parent?.type === "COMPONENT_SET") {
728 | // Currently set to do nothing, but should it append to something? Is there a way?
729 | // str`${Ref(getNoneGroupParent(node))}.appendChild(${Ref(node)})\n`
730 | } else {
731 | str` ${Ref(node.parent)}.appendChild(${Ref(node)})\n`;
732 | }
733 | }
734 | }
735 |
736 | function createBasic(node, level, options = {}) {
737 | if (node.type === "COMPONENT") {
738 | // If node being visited matches a component already visited (ie, already created?), then set falg to true so loop stops traversing
739 | if (
740 | allComponents.some(
741 | (component) =>
742 | JSON.stringify(component) === JSON.stringify(node)
743 | )
744 | ) {
745 | return true;
746 | }
747 | }
748 |
749 | if (
750 | node.type !== "GROUP" &&
751 | node.type !== "INSTANCE" &&
752 | node.type !== "COMPONENT_SET" &&
753 | node.type !== "BOOLEAN_OPERATION" &&
754 | !isInsideInstance(node)
755 | ) {
756 | // If it's a component first check if it's been added to the list before creating, if not then create it and add it to the list (only creates frame)
757 |
758 | if (
759 | !allComponents.some(
760 | (component) =>
761 | JSON.stringify(component) === JSON.stringify(node)
762 | )
763 | ) {
764 | str`
765 |
766 | // Create ${node.type}
767 | var ${Ref(node)} = figma.create${v.titleCase(node.type)}()\n`;
768 |
769 | if (node.type !== "COMPONENT" || options?.append !== false) {
770 | appendNode(node);
771 | }
772 |
773 | createProps(node, level);
774 |
775 | // else if (options?.append !== false) {
776 | // if (node.type !== "COMPONENT") {
777 | // appendNode(node)
778 | // }
779 |
780 | // }
781 |
782 | allComponents.push(node);
783 | }
784 | }
785 |
786 | function createRefToInstanceNode(node) {
787 | // FIXME: I think this needs to include the ids of several nested instances. In order to do that, references need to be made for them even if there no overrides
788 | // This dynamically creates the reference to nodes nested inside instances. I consists of two parts. The first is the id of the parent instance. The second part is the id of the current instance counterpart node.
789 |
790 | // var childRef = ""
791 | // // if (getNodeDepth(node, getParentInstance(node)) > 0) {
792 |
793 | // // console.log("----")
794 | // // console.log("instanceNode", node)
795 | // // console.log("counterpart", getInstanceCounterpart(node))
796 | // // console.log("nodeDepth", getNodeDepth(node, findParentInstance(node)))
797 | // // console.log("instanceParent", findParentInstance(node))
798 |
799 | // // FIXME: In some cases counterpart is returned as undefined. I think because layer might be hidden?. Tried again with layer hidden and issue didn't happen again. Maybe a figma bug. Perhaps to workaround, unhide layer and hide again.
800 | // if (typeof getInstanceCounterpartUsingLocation(node) === 'undefined') {
801 | // console.warn("Can't get location of counterpart", node)
802 | // }
803 | // else {
804 | // childRef = ` + ";" + ${Ref(getInstanceCounterpartUsingLocation(node))}.id`
805 | // }
806 |
807 | // // }
808 |
809 | // var letterI = `"I" +`
810 |
811 | // if (getParentInstance(node).id.startsWith("I")) {
812 | // letterI = ``
813 | // }
814 |
815 | // TODO: Try getting all the ids of the parents
816 | // TODO: 1. Get all the nodes of the parent instannces
817 | // 2. Output the id
818 | // 3. output the id of the original component
819 |
820 | var letterI = `"I" + `;
821 |
822 | if (getParentInstance(node).id.startsWith("I")) {
823 | letterI = ``;
824 | }
825 |
826 | // // Does it only need the top instance?
827 | // var parentInstances = getParentInstances(node)
828 | // var string = ""
829 | // if (parentInstances) {
830 | // // parentInstances.shift()
831 | // console.log(parentInstances)
832 | // var array = []
833 | // for (var i = 0; i < parentInstances.length; i++) {
834 | // var instance = parentInstances[i]
835 |
836 | // array.push(`${Ref(instance)}.id`)
837 | // }
838 |
839 | // string = array.join(` + ";" + `)
840 | // }
841 |
842 | var child = `${Ref(
843 | getInstanceCounterpartUsingLocation(
844 | node,
845 | getParentInstance(node)
846 | )
847 | )}.id`;
848 | var ref = `${letterI}${Ref(
849 | getParentInstance(node)
850 | )}.id + ";" + ${child}`;
851 | // if (node.id === figma.currentPage.selection[0].id) {
852 | // console.log(">>>>>", figma.currentPage.selection[0].id, ref)
853 | // }
854 |
855 | // console.log(getParentInstances(node).join(";"))
856 |
857 | return `var ${Ref(node)} = figma.getNodeById(${ref})`;
858 | }
859 |
860 | // Create overides for nodes inside instances
861 |
862 | // if (!('horizontalPadding' in node) || !('verticalPadding' in node)) {
863 | // if (getOverrides(node)) {
864 | if (isInsideInstance(node)) {
865 | // if (node.type === "INSTANCE") {
866 | // if (isInstanceDefaultVariant(node)) {
867 | // str`
868 | // // Component wasn't swapped by user
869 | // var ${Ref(node)}`
870 | // }
871 | // }
872 |
873 | str`
874 | // Ref to SUB NODE
875 | ${createRefToInstanceNode(node)}\n`;
876 |
877 | if (getOverrides(node)) {
878 | // If overrides exist apply them
879 | createProps(node, level);
880 | }
881 | }
882 | // }
883 | // }
884 |
885 | // Swap instances if different from default variant
886 | if (node.type === "INSTANCE") {
887 | // console.log("node name", node.name)
888 | // Swap if not the default variant
889 | // if (!isInstanceDefaultVariant(node)) {
890 | // console.log("node name swapped", node.name)
891 |
892 | // NOTE: Cannot use node ref when instance/node nested inside instance because not created by plugin. Must use an alternative method to identify instance to swap. Cannot use getNodeById unless you know what the node id will be. So what we do here, is dynamically lookup the id by combining the dynamic ids of several node references. This might need to work for more than one level of instances nested inside an instance.
893 | // if (isInsideInstance(node)) {
894 | // str`
895 | // // Swap COMPONENT
896 | // ${createRefToInstanceNode(node)}\n`
897 | // }
898 | // if (node.id === figma.currentPage.selection[0].id) {
899 | // console.log(">>>>>", " has been swapped")
900 | // }
901 |
902 | // NOTE: Decided to always swap the component because can't know if it's correct or not.
903 | str`
904 |
905 | // Swap COMPONENT
906 | ${Ref(node)}.swapComponent(${Ref(node.mainComponent)})\n`;
907 |
908 | // }
909 | }
910 | }
911 |
912 | function createInstance(node, level) {
913 | var mainComponent;
914 |
915 | if (node.type === "INSTANCE") {
916 | mainComponent = node.mainComponent;
917 | }
918 |
919 | // console.log("node", node.type, node.mainComponent)
920 |
921 | if (node.type === "INSTANCE") {
922 | // If main component not selected by user
923 | // Grab all components and add to list
924 | // If main component of instance not already visited, ie (selected by the user), then create it and it's children
925 |
926 | if (!allComponents.includes(mainComponent)) {
927 | createNode(mainComponent, { append: false });
928 | }
929 | }
930 |
931 | if (node.type === "INSTANCE" && !isInsideInstance(node)) {
932 | str`
933 |
934 | // Create INSTANCE
935 | var ${Ref(node)} = ${Ref(mainComponent)}.createInstance()\n`;
936 |
937 | appendNode(node);
938 |
939 | // Need to reference main component so that createProps can check if props are overriden
940 | createProps(node, level, {}, mainComponent);
941 | }
942 |
943 | // Once component has been created add it to array of all components
944 | if (node.type === "INSTANCE") {
945 | if (
946 | !allComponents.some(
947 | (component) =>
948 | JSON.stringify(component) ===
949 | JSON.stringify(mainComponent)
950 | )
951 | ) {
952 | allComponents.push(mainComponent);
953 | }
954 | }
955 | }
956 |
957 | function createGroup(node, level) {
958 | if (node.type === "GROUP" && !isInsideInstance(node)) {
959 | var children: any = Ref(node.children);
960 | if (Array.isArray(children)) {
961 | children = Ref(node.children).join(", ");
962 | }
963 | var parent;
964 | if (
965 | node.parent?.type === "GROUP" ||
966 | node.parent?.type === "COMPONENT_SET" ||
967 | node.parent?.type === "BOOLEAN_OPERATION"
968 | ) {
969 | parent = `${Ref(getNoneGroupParent(node))}`;
970 | // parent = `figma.currentPage`
971 | } else {
972 | parent = `${Ref(node.parent)}`;
973 | }
974 | str`
975 |
976 | // Create GROUP
977 | var ${Ref(node)} = figma.group([${children}], ${parent})\n`;
978 | createProps(node, level, {
979 | resize: false,
980 | relativeTransform: false,
981 | x: false,
982 | y: false,
983 | rotation: false,
984 | });
985 | }
986 | }
987 |
988 | function createBooleanOperation(node, level) {
989 | // Boolean can not be created if inside instance
990 | // TODO: When boolean objects are created they loose their coordinates?
991 | // TODO: Don't resize boolean objects
992 | if (node.type === "BOOLEAN_OPERATION" && !isInsideInstance(node)) {
993 | var children: any = Ref(node.children);
994 | if (Array.isArray(children)) {
995 | children = Ref(node.children).join(", ");
996 | }
997 | var parent;
998 | if (
999 | node.parent?.type === "GROUP" ||
1000 | node.parent?.type === "COMPONENT_SET" ||
1001 | node.parent?.type === "BOOLEAN_OPERATION"
1002 | ) {
1003 | parent = `${Ref(getNoneGroupParent(node))}`;
1004 | } else {
1005 | parent = `${Ref(node.parent)}`;
1006 | }
1007 | str`
1008 |
1009 | // Create BOOLEAN_OPERATION
1010 | var ${Ref(node)} = figma.${v.lowerCase(
1011 | node.booleanOperation
1012 | )}([${children}], ${parent})\n`;
1013 |
1014 | var x = node.parent.x - node.x;
1015 | var y = node.parent.y - node.y;
1016 |
1017 | // TODO: Don't apply relativeTransform, x, y, or rotation to booleans
1018 | createProps(node, level, {
1019 | resize: false,
1020 | relativeTransform: false,
1021 | x: false,
1022 | y: false,
1023 | rotation: false,
1024 | });
1025 | }
1026 | }
1027 |
1028 | function createComponentSet(node, level) {
1029 | // FIXME: What should happen when the parent is a group? The component set can't be added to a appended to a group. It therefore must be added to the currentPage, and then grouped by the group function?
1030 | if (node.type === "COMPONENT_SET") {
1031 | var children: any = Ref(node.children);
1032 | if (Array.isArray(children)) {
1033 | children = Ref(node.children).join(", ");
1034 | }
1035 | var parent;
1036 | if (
1037 | node.parent?.type === "GROUP" ||
1038 | node.parent?.type === "COMPONENT_SET" ||
1039 | node.parent?.type === "BOOLEAN_OPERATION"
1040 | ) {
1041 | parent = `${Ref(getNoneGroupParent(node))}`;
1042 | } else {
1043 | parent = `${Ref(node.parent)}`;
1044 | }
1045 |
1046 | str`
1047 |
1048 | // Create COMPONENT_SET
1049 | var ${Ref(node)} = figma.combineAsVariants([${children}], ${parent})\n`;
1050 |
1051 | createProps(node, level);
1052 | }
1053 | }
1054 |
1055 | function createNode(nodes, options?) {
1056 | nodes = putValuesIntoArray(nodes);
1057 |
1058 | walkNodes(nodes, {
1059 | during(node, { ref, level, sel, parent }) {
1060 | createInstance(node, level);
1061 |
1062 | return createBasic(node, level, options);
1063 | },
1064 | after(node, { ref, level, sel, parent }) {
1065 | createGroup(node, level);
1066 | createBooleanOperation(node, level);
1067 | createComponentSet(node, level);
1068 | },
1069 | });
1070 | }
1071 |
1072 | // figma.showUI(__html__, { width: 320, height: 480 });
1073 |
1074 | var selection = origSel;
1075 |
1076 | // for (var i = 0; i < selection.length; i++) {
1077 | createNode(selection);
1078 | // }
1079 |
1080 | async function generateImages() {
1081 | var array = [];
1082 | if (images && images.length > 0) {
1083 | for (var i = 0; i < images.length; i++) {
1084 | var { imageHash } = images[i];
1085 |
1086 | var imageBytes = await figma
1087 | .getImageByHash(imageHash)
1088 | .getBytesAsync();
1089 |
1090 | // Commented for now because causing syntax highlighter to crash
1091 | array.push(`
1092 | // Create IMAGE HASH
1093 | // var image = figma.createImage(toBuffer(${imageBytes}))\n`);
1094 | }
1095 | }
1096 |
1097 | return array;
1098 | }
1099 |
1100 | // Create styles
1101 | if (styles) {
1102 | var styleString = "";
1103 | for (let [key, value] of Object.entries(styles)) {
1104 | for (let i = 0; i < value.length; i++) {
1105 | var style = value[i];
1106 |
1107 | if (
1108 | style.type === "PAINT" ||
1109 | style.type === "EFFECT" ||
1110 | style.type === "GRID"
1111 | ) {
1112 | let nameOfProperty;
1113 |
1114 | if (style.type === "GRID") {
1115 | nameOfProperty = "layoutGrids";
1116 | } else {
1117 | nameOfProperty = v.camelCase(style.type) + "s";
1118 | }
1119 |
1120 | styleString += `
1121 |
1122 | // Create STYLE
1123 | var ${StyleRef(style)} = figma.create${v.titleCase(style.type)}Style()
1124 | ${StyleRef(style)}.name = ${JSON.stringify(style.name)}
1125 | ${StyleRef(style)}.${nameOfProperty} = ${JSON.stringify(style[nameOfProperty])}
1126 | `;
1127 | }
1128 |
1129 | if (style.type === "TEXT") {
1130 | let nameOfProperty = "";
1131 |
1132 | styleString += `\
1133 |
1134 | // Create STYLE
1135 | var ${StyleRef(style)} = figma.create${v.titleCase(style.type)}Style()
1136 | ${StyleRef(style)}.name = ${JSON.stringify(style.name)}
1137 | ${StyleRef(style)}.fontName = ${JSON.stringify(style.fontName)}
1138 | ${StyleRef(style)}.fontSize = ${JSON.stringify(style.fontSize)}
1139 | ${StyleRef(style)}.letterSpacing = ${JSON.stringify(style.letterSpacing)}
1140 | ${StyleRef(style)}.lineHeight = ${JSON.stringify(style.lineHeight)}
1141 | ${StyleRef(style)}.paragraphIndent = ${JSON.stringify(style.paragraphIndent)}
1142 | ${StyleRef(style)}.paragraphSpacing = ${JSON.stringify(style.paragraphSpacing)}
1143 | ${StyleRef(style)}.textCase = ${JSON.stringify(style.textCase)}
1144 | ${StyleRef(style)}.textDecoration = ${JSON.stringify(style.textDecoration)}
1145 | `;
1146 | }
1147 | }
1148 | }
1149 | str.prepend`${styleString}`;
1150 | }
1151 |
1152 | if (fonts) {
1153 | str.prepend`
1154 | // Load FONTS
1155 | async function loadFonts() {
1156 | await Promise.all([
1157 | ${fonts.map((font) => {
1158 | return `figma.loadFontAsync({
1159 | family: ${JSON.stringify(font?.family)},
1160 | style: ${JSON.stringify(font?.style)}
1161 | })`;
1162 | })}
1163 | ])
1164 | }
1165 |
1166 | await loadFonts()`;
1167 | }
1168 |
1169 | // Remove nodes created for temporary purpose
1170 | for (var i = 0; i < discardNodes.length; i++) {
1171 | var node = discardNodes[i];
1172 | // console.log(node)
1173 |
1174 | // Cannot remove node. Is it because it is from another file?
1175 | // TEMP FIX: Check node exists before trying to remove
1176 |
1177 | if (figma.getNodeById(node.id) && node.parent !== null) node.remove();
1178 | }
1179 |
1180 | if (opts?.wrapInFunction) {
1181 | if (opts?.includeObject) {
1182 | str`
1183 | // Pass children to function
1184 | let nodes = figma.currentPage.children
1185 | figma.currentPage = oldPage
1186 |
1187 | for (let i = 0; i < nodes.length; i++) {
1188 | let node = nodes[i]
1189 | figma.currentPage.appendChild(node)
1190 | }
1191 |
1192 | newPage.remove()
1193 |
1194 | return nodes\n`;
1195 | }
1196 | // Wrap in function
1197 | str`
1198 | }\n
1199 | createNodes()
1200 | `;
1201 | }
1202 |
1203 | if (opts?.wrapInFunction) {
1204 | if (opts?.includeObject) {
1205 | str.prepend`
1206 | // Create temporary page to pass nodes to function
1207 | let oldPage = figma.currentPage
1208 | let newPage = figma.createPage()
1209 | figma.currentPage = newPage
1210 | `;
1211 | }
1212 |
1213 | // Wrap in function
1214 | str.prepend`
1215 | // Wrap in function
1216 | async function createNodes() {
1217 | `;
1218 | }
1219 |
1220 | // var imageArray = await generateImages()
1221 |
1222 | // var imageString = ""
1223 | // if (imageArray && imageArray.length > 0) {
1224 | // imageString = imageArray.join()
1225 | // }
1226 |
1227 | return [
1228 | ...str()
1229 | .replace(/^\n|\n$/g, "")
1230 | .match(/(?=[\s\S])(?:.*\n?){1,8}/g),
1231 | ];
1232 |
1233 | // result = result.join("").replace(/^\n|\n$/g, "")
1234 | }
1235 |
--------------------------------------------------------------------------------
/src/props.ts:
--------------------------------------------------------------------------------
1 | // These are the default values that nodes get when they are created using the API, not via the editor. They are then used to make sure that these props and values are added to nodes created using
2 |
3 | export const allowedProps = [
4 | "name",
5 | "visible",
6 | "locked",
7 | "opacity",
8 | "blendMode",
9 | "isMask",
10 | "effects",
11 | "relativeTransform",
12 | // "absoluteTransform",
13 | "x",
14 | "y",
15 | "width",
16 | "height",
17 | "rotation",
18 | "layoutAlign",
19 | "constrainProportions",
20 | "layoutGrow",
21 | "exportSettings",
22 | "fills",
23 | "strokes",
24 | "strokeWeight",
25 | "strokeAlign",
26 | "strokeCap",
27 | "strokeJoin",
28 | "strokeMiterLimit",
29 | "dashPattern",
30 | "cornerRadius",
31 | "cornerSmoothing",
32 | "topLeftRadius",
33 | "topRightRadius",
34 | "bottomLeftRadius",
35 | "bottomRightRadius",
36 | "paddingLeft",
37 | "paddingRight",
38 | "paddingTop",
39 | "paddingBottom",
40 | "primaryAxisAlignItems",
41 | "counterAxisAlignItems",
42 | "primaryAxisSizingMode",
43 | "layoutPositioning",
44 | "strokeTopWeight",
45 | "strokeBottomWeight",
46 | "strokeLeftWeight",
47 | "strokeRightWeight",
48 | "layoutGrids",
49 | "clipsContent",
50 | "guides",
51 | "expanded",
52 | "constraints",
53 | "layoutMode",
54 | "counterAxisSizingMode",
55 | "itemSpacing",
56 | "overflowDirection",
57 | "numberOfFixedChildren",
58 | // "overlayPositionType",
59 | // "overlayBackground",
60 | // "overlayBackgroundInteraction",
61 | "reactions",
62 | "hyperlink",
63 | "characters",
64 | "lineHeight",
65 | "listSpacing",
66 | "fontName",
67 | "textAutoResize",
68 | "autoRename",
69 | "paints",
70 | "textDecoration",
71 | "textCase",
72 | "paragraphSpacing",
73 | "paragraphIndent",
74 | "fontSize",
75 | "fillStyleId",
76 | "backgroundStyleId",
77 | "strokeStyleId",
78 | "documentationLinks",
79 | "description",
80 | "vectorNetwork",
81 | "vectorPaths",
82 | "strokesIncludedInLayout",
83 | "itemReverseZIndex",
84 | ];
85 |
86 | // name !== "key" &&
87 | // name !== "mainComponent" &&
88 | // name !== "absoluteTransform" &&
89 | // name !== "type" &&
90 | // name !== "id" &&
91 | // name !== "parent" &&
92 | // name !== "children" &&
93 | // name !== "masterComponent" &&
94 | // name !== "mainComponent" &&
95 | // name !== "horizontalPadding" &&
96 | // name !== "verticalPadding" &&
97 | // name !== "reactions" &&
98 | // name !== "overlayPositionType" &&
99 | // name !== "overflowDirection" &&
100 | // name !== "numberOfFixedChildren" &&
101 | // name !== "overlayBackground" &&
102 | // name !== "overlayBackgroundInteraction" &&
103 | // name !== "remote" &&
104 | // name !== "defaultVariant" &&
105 | // name !== "hasMissingFont" &&
106 | // name !== "exportSettings" &&
107 | // name !== "variantProperties" &&
108 | // name !== "variantGroupProperties" &&
109 | // name !== "absoluteRenderBounds" &&
110 | // name !== "fillGeometry" &&
111 | // name !== "strokeGeometry" &&
112 | // name !== "stuckNodes" &&
113 | // name !== "componentPropertyReferences" &&
114 | // name !== "canUpgradeToNativeBidiSupport" &&
115 | // name !== "componentPropertyDefinitions" &&
116 | // name !== "componentProperties" &&
117 | // // Investigate these ones
118 | // name !== "itemReverseZIndex" &&
119 | // name !== "strokesIncludedInLayout" &&
120 |
121 | const exportPropValues = {
122 | exportSettings: [],
123 | };
124 |
125 | const prototypingPropValues = {
126 | overflowDirection: "NONE",
127 | numberOfFixedChildren: 0,
128 | };
129 |
130 | const sceneNodePropValues = {
131 | visible: true,
132 | locked: false,
133 | };
134 |
135 | const containerPropValues = {
136 | expanded: true,
137 | backgrounds: [
138 | {
139 | type: "SOLID",
140 | visible: true,
141 | opacity: 1,
142 | blendMode: "NORMAL",
143 | color: {
144 | r: 1,
145 | g: 1,
146 | b: 1,
147 | },
148 | },
149 | ],
150 | };
151 |
152 | const cornerPropValues = {
153 | cornerRadius: 0,
154 | cornerSmoothing: 0,
155 | topLeftRadius: 0,
156 | topRightRadius: 0,
157 | bottomLeftRadius: 0,
158 | bottomRightRadius: 0,
159 | };
160 |
161 | const layoutPropValues = {
162 | absoluteTransform: [],
163 | relativeTransform: [],
164 | x: 0,
165 | y: 0,
166 | rotation: 0,
167 | width: 0,
168 | height: 0,
169 |
170 | constrainProportions: false,
171 | constraints: {
172 | horizontal: "MIN",
173 | vertical: "MIN",
174 | },
175 | layoutAlign: "INHERIT",
176 | layoutGrow: 0,
177 | };
178 |
179 | const geometryPropValues = {
180 | fills: [
181 | {
182 | type: "SOLID",
183 | visible: true,
184 | opacity: 1,
185 | blendMode: "NORMAL",
186 | color: {
187 | r: 1,
188 | g: 1,
189 | b: 1,
190 | },
191 | },
192 | ],
193 | // strokes: [], Despite being default, it's needed for vectors?
194 | strokeWeight: 1,
195 | strokeMiterLimit: 4,
196 | strokeAlign: "INSIDE",
197 | strokeCap: "NONE",
198 | strokeJoin: "MITER",
199 | dashPattern: [],
200 | fillStyleId: "",
201 | strokeStyleId: "",
202 | };
203 |
204 | const textPropValues = {
205 | fontSize: 12,
206 | hasMissingFont: false,
207 | paragraphIndent: 0,
208 | paragraphSpacing: 0,
209 | textAlignHorizontal: "LEFT",
210 | textAlignVertical: "TOP",
211 | textAutoResize: "WIDTH_AND_HEIGHT",
212 | textCase: "ORIGINAL",
213 | textDecoration: "NONE",
214 | textStyleId: "",
215 | letterSpacing: {
216 | unit: "PERCENT",
217 | value: 0,
218 | },
219 | characters: "",
220 | autoRename: true,
221 | };
222 |
223 | const baseFramePropValues = {
224 | ...containerPropValues,
225 | ...layoutPropValues,
226 | layoutMode: "NONE",
227 | primaryAxisSizingMode: "AUTO",
228 | counterAxisSizingMode: "FIXED",
229 |
230 | primaryAxisAlignItems: "MIN",
231 | counterAxisAlignItems: "MIN",
232 |
233 | paddingLeft: 0,
234 | paddingRight: 0,
235 | paddingTop: 0,
236 | paddingBottom: 0,
237 | itemSpacing: 0,
238 |
239 | // verticalPadding: 0,
240 | // horizontalPadding: 0,
241 |
242 | layoutGrids: [],
243 | gridStyleId: "",
244 | clipsContent: true,
245 | guides: [],
246 | };
247 |
248 | const blendPropValues = {
249 | opacity: 1,
250 | blendMode: "PASS_THROUGH",
251 | isMask: false,
252 | effects: [],
253 | };
254 |
255 | export const defaultPropValues: {} = {
256 | FRAME: {
257 | name: "Frame",
258 | visible: true,
259 | locked: false,
260 | opacity: 1,
261 | blendMode: "PASS_THROUGH",
262 | isMask: false,
263 | effects: [],
264 | relativeTransform: [
265 | [1, 0, 0],
266 | [0, 1, 0],
267 | ],
268 | absoluteTransform: [
269 | [1, 0, 0],
270 | [0, 1, 0],
271 | ],
272 | x: 0,
273 | y: 0,
274 | width: 100,
275 | height: 100,
276 | rotation: 0,
277 | layoutAlign: "INHERIT",
278 | constrainProportions: false,
279 | layoutGrow: 0,
280 | exportSettings: [],
281 | fills: [
282 | {
283 | type: "SOLID",
284 | visible: true,
285 | opacity: 1,
286 | blendMode: "NORMAL",
287 | color: {
288 | r: 1,
289 | g: 1,
290 | b: 1,
291 | },
292 | },
293 | ],
294 | strokes: [],
295 | strokeWeight: 1,
296 | strokeAlign: "INSIDE",
297 | strokeCap: "NONE",
298 | strokeJoin: "MITER",
299 | strokeMiterLimit: 4,
300 | dashPattern: [],
301 | cornerRadius: 0,
302 | cornerSmoothing: 0,
303 | topLeftRadius: 0,
304 | topRightRadius: 0,
305 | bottomLeftRadius: 0,
306 | bottomRightRadius: 0,
307 | paddingLeft: 0,
308 | paddingRight: 0,
309 | paddingTop: 0,
310 | paddingBottom: 0,
311 | primaryAxisAlignItems: "MIN",
312 | counterAxisAlignItems: "MIN",
313 | primaryAxisSizingMode: "AUTO",
314 | layoutGrids: [],
315 | backgrounds: [
316 | {
317 | type: "SOLID",
318 | visible: true,
319 | opacity: 1,
320 | blendMode: "NORMAL",
321 | color: {
322 | r: 1,
323 | g: 1,
324 | b: 1,
325 | },
326 | },
327 | ],
328 | clipsContent: true,
329 | guides: [],
330 | expanded: true,
331 | constraints: {
332 | horizontal: "MIN",
333 | vertical: "MIN",
334 | },
335 | layoutMode: "NONE",
336 | counterAxisSizingMode: "FIXED",
337 | itemSpacing: 0,
338 | overflowDirection: "NONE",
339 | numberOfFixedChildren: 0,
340 | overlayPositionType: "CENTER",
341 | overlayBackground: {
342 | type: "NONE",
343 | },
344 | overlayBackgroundInteraction: "NONE",
345 | reactions: [],
346 | layoutPositioning: "AUTO",
347 | itemReverseZIndex: false,
348 | strokesIncludedInLayout: false,
349 | },
350 | GROUP: {},
351 | SLICE: {
352 | name: "Slice",
353 | visible: true,
354 | locked: false,
355 | relativeTransform: [
356 | [1, 0, 0],
357 | [0, 1, 0],
358 | ],
359 | absoluteTransform: [
360 | [1, 0, 0],
361 | [0, 1, 0],
362 | ],
363 | x: 0,
364 | y: 0,
365 | width: 100,
366 | height: 100,
367 | rotation: 0,
368 | layoutAlign: "INHERIT",
369 | constrainProportions: false,
370 | layoutGrow: 0,
371 | exportSettings: [],
372 | },
373 | BOOLEAN_OPERATION: {},
374 | RECTANGLE: {
375 | name: "Rectangle",
376 | visible: true,
377 | locked: false,
378 | opacity: 1,
379 | blendMode: "PASS_THROUGH",
380 | isMask: false,
381 | effects: [],
382 | fills: [
383 | {
384 | type: "SOLID",
385 | visible: true,
386 | opacity: 1,
387 | blendMode: "NORMAL",
388 | color: {
389 | r: 0.7686274647712708,
390 | g: 0.7686274647712708,
391 | b: 0.7686274647712708,
392 | },
393 | },
394 | ],
395 | strokes: [],
396 | strokeWeight: 1,
397 | strokeAlign: "INSIDE",
398 | strokeCap: "NONE",
399 | strokeJoin: "MITER",
400 | strokeMiterLimit: 4,
401 | dashPattern: [],
402 | relativeTransform: [
403 | [1, 0, 0],
404 | [0, 1, 0],
405 | ],
406 | absoluteTransform: [
407 | [1, 0, 0],
408 | [0, 1, 0],
409 | ],
410 | x: 0,
411 | y: 0,
412 | width: 100,
413 | height: 100,
414 | rotation: 0,
415 | layoutAlign: "INHERIT",
416 | constrainProportions: false,
417 | layoutGrow: 0,
418 | exportSettings: [],
419 | constraints: {
420 | horizontal: "MIN",
421 | vertical: "MIN",
422 | },
423 | cornerRadius: 0,
424 | cornerSmoothing: 0,
425 | topLeftRadius: 0,
426 | topRightRadius: 0,
427 | bottomLeftRadius: 0,
428 | bottomRightRadius: 0,
429 | reactions: [],
430 | layoutPositioning: "AUTO",
431 | },
432 | LINE: {
433 | name: "Line",
434 | visible: true,
435 | locked: false,
436 | opacity: 1,
437 | blendMode: "PASS_THROUGH",
438 | isMask: false,
439 | effects: [],
440 | fills: [],
441 | strokes: [
442 | {
443 | type: "SOLID",
444 | visible: true,
445 | opacity: 1,
446 | blendMode: "NORMAL",
447 | color: {
448 | r: 0,
449 | g: 0,
450 | b: 0,
451 | },
452 | },
453 | ],
454 | strokeWeight: 1,
455 | strokeAlign: "CENTER",
456 | strokeCap: "NONE",
457 | strokeJoin: "MITER",
458 | strokeMiterLimit: 4,
459 | dashPattern: [],
460 | relativeTransform: [
461 | [1, 0, 0],
462 | [0, 1, 0],
463 | ],
464 | absoluteTransform: [
465 | [1, 0, 0],
466 | [0, 1, 0],
467 | ],
468 | x: 0,
469 | y: 0,
470 | width: 100,
471 | height: 0,
472 | rotation: 0,
473 | layoutAlign: "INHERIT",
474 | constrainProportions: false,
475 | layoutGrow: 0,
476 | exportSettings: [],
477 | constraints: {
478 | horizontal: "MIN",
479 | vertical: "MIN",
480 | },
481 | reactions: [],
482 | layoutPositioning: "AUTO",
483 | },
484 | ELLIPSE: {
485 | name: "Ellipse",
486 | visible: true,
487 | locked: false,
488 | opacity: 1,
489 | blendMode: "PASS_THROUGH",
490 | isMask: false,
491 | effects: [],
492 | fills: [
493 | {
494 | type: "SOLID",
495 | visible: true,
496 | opacity: 1,
497 | blendMode: "NORMAL",
498 | color: {
499 | r: 0.7686274647712708,
500 | g: 0.7686274647712708,
501 | b: 0.7686274647712708,
502 | },
503 | },
504 | ],
505 | strokes: [],
506 | strokeWeight: 1,
507 | strokeAlign: "INSIDE",
508 | strokeCap: "NONE",
509 | strokeJoin: "MITER",
510 | strokeMiterLimit: 4,
511 | dashPattern: [],
512 | relativeTransform: [
513 | [1, 0, 0],
514 | [0, 1, 0],
515 | ],
516 | absoluteTransform: [
517 | [1, 0, 0],
518 | [0, 1, 0],
519 | ],
520 | x: 0,
521 | y: 0,
522 | width: 100,
523 | height: 100,
524 | rotation: 0,
525 | layoutAlign: "INHERIT",
526 | constrainProportions: false,
527 | layoutGrow: 0,
528 | exportSettings: [],
529 | constraints: {
530 | horizontal: "MIN",
531 | vertical: "MIN",
532 | },
533 | cornerRadius: 0,
534 | cornerSmoothing: 0,
535 | arcData: {
536 | startingAngle: 0,
537 | endingAngle: 6.2831854820251465,
538 | innerRadius: 0,
539 | },
540 | reactions: [],
541 | layoutPositioning: "AUTO",
542 | },
543 | POLYGON: {
544 | name: "Polygon",
545 | visible: true,
546 | locked: false,
547 | opacity: 1,
548 | blendMode: "PASS_THROUGH",
549 | isMask: false,
550 | effects: [],
551 | fills: [
552 | {
553 | type: "SOLID",
554 | visible: true,
555 | opacity: 1,
556 | blendMode: "NORMAL",
557 | color: {
558 | r: 0.7686274647712708,
559 | g: 0.7686274647712708,
560 | b: 0.7686274647712708,
561 | },
562 | },
563 | ],
564 | strokes: [],
565 | strokeWeight: 1,
566 | strokeAlign: "INSIDE",
567 | strokeCap: "NONE",
568 | strokeJoin: "MITER",
569 | strokeMiterLimit: 4,
570 | dashPattern: [],
571 | relativeTransform: [
572 | [1, 0, 0],
573 | [0, 1, 0],
574 | ],
575 | absoluteTransform: [
576 | [1, 0, 0],
577 | [0, 1, 0],
578 | ],
579 | x: 0,
580 | y: 0,
581 | width: 100,
582 | height: 100,
583 | rotation: 0,
584 | layoutAlign: "INHERIT",
585 | constrainProportions: false,
586 | layoutGrow: 0,
587 | exportSettings: [],
588 | constraints: {
589 | horizontal: "MIN",
590 | vertical: "MIN",
591 | },
592 | cornerRadius: 0,
593 | cornerSmoothing: 0,
594 | pointCount: 3,
595 | reactions: [],
596 | layoutPositioning: "AUTO",
597 | },
598 | STAR: {
599 | name: "Star",
600 | visible: true,
601 | locked: false,
602 | opacity: 1,
603 | blendMode: "PASS_THROUGH",
604 | isMask: false,
605 | effects: [],
606 | fills: [
607 | {
608 | type: "SOLID",
609 | visible: true,
610 | opacity: 1,
611 | blendMode: "NORMAL",
612 | color: {
613 | r: 0.7686274647712708,
614 | g: 0.7686274647712708,
615 | b: 0.7686274647712708,
616 | },
617 | },
618 | ],
619 | strokes: [],
620 | strokeWeight: 1,
621 | strokeAlign: "INSIDE",
622 | strokeCap: "NONE",
623 | strokeJoin: "MITER",
624 | strokeMiterLimit: 4,
625 | dashPattern: [],
626 | relativeTransform: [
627 | [1, 0, 0],
628 | [0, 1, 0],
629 | ],
630 | absoluteTransform: [
631 | [1, 0, 0],
632 | [0, 1, 0],
633 | ],
634 | x: 0,
635 | y: 0,
636 | width: 100,
637 | height: 100,
638 | rotation: 0,
639 | layoutAlign: "INHERIT",
640 | constrainProportions: false,
641 | layoutGrow: 0,
642 | exportSettings: [],
643 | constraints: {
644 | horizontal: "MIN",
645 | vertical: "MIN",
646 | },
647 | cornerRadius: 0,
648 | cornerSmoothing: 0,
649 | pointCount: 5,
650 | innerRadius: 0.3819660246372223,
651 | reactions: [],
652 | layoutPositioning: "AUTO",
653 | },
654 | VECTOR: {
655 | name: "Vector",
656 | visible: true,
657 | locked: false,
658 | opacity: 1,
659 | blendMode: "PASS_THROUGH",
660 | isMask: false,
661 | effects: [],
662 | fills: [],
663 | strokes: [
664 | {
665 | type: "SOLID",
666 | visible: true,
667 | opacity: 1,
668 | blendMode: "NORMAL",
669 | color: {
670 | r: 0,
671 | g: 0,
672 | b: 0,
673 | },
674 | },
675 | ],
676 | strokeWeight: 1,
677 | strokeAlign: "CENTER",
678 | strokeCap: "NONE",
679 | strokeJoin: "MITER",
680 | strokeMiterLimit: 4,
681 | dashPattern: [],
682 | relativeTransform: [
683 | [1, 0, 0],
684 | [0, 1, 0],
685 | ],
686 | absoluteTransform: [
687 | [1, 0, 0],
688 | [0, 1, 0],
689 | ],
690 | x: 0,
691 | y: 0,
692 | width: 100,
693 | height: 100,
694 | rotation: 0,
695 | layoutAlign: "INHERIT",
696 | constrainProportions: false,
697 | layoutGrow: 0,
698 | exportSettings: [],
699 | constraints: {
700 | horizontal: "MIN",
701 | vertical: "MIN",
702 | },
703 | cornerRadius: 0,
704 | cornerSmoothing: 0,
705 | vectorNetwork: {
706 | regions: [],
707 | segments: [],
708 | vertices: [],
709 | },
710 | vectorPaths: [],
711 | handleMirroring: "NONE",
712 | reactions: [],
713 | layoutPositioning: "AUTO",
714 | },
715 | TEXT: {
716 | name: "Text",
717 | visible: true,
718 | locked: false,
719 | opacity: 1,
720 | blendMode: "PASS_THROUGH",
721 | isMask: false,
722 | effects: [],
723 | fills: [
724 | {
725 | type: "SOLID",
726 | visible: true,
727 | opacity: 1,
728 | blendMode: "NORMAL",
729 | color: {
730 | r: 0,
731 | g: 0,
732 | b: 0,
733 | },
734 | },
735 | ],
736 | strokes: [],
737 | strokeWeight: 1,
738 | strokeAlign: "OUTSIDE",
739 | strokeCap: "NONE",
740 | strokeJoin: "MITER",
741 | strokeMiterLimit: 4,
742 | dashPattern: [],
743 | relativeTransform: [
744 | [1, 0, 0],
745 | [0, 1, 0],
746 | ],
747 | absoluteTransform: [
748 | [1, 0, 0],
749 | [0, 1, 0],
750 | ],
751 | x: 0,
752 | y: 0,
753 | width: 0,
754 | height: 14,
755 | rotation: 0,
756 | layoutAlign: "INHERIT",
757 | constrainProportions: false,
758 | layoutGrow: 0,
759 | exportSettings: [],
760 | constraints: {
761 | horizontal: "MIN",
762 | vertical: "MIN",
763 | },
764 | hasMissingFont: false,
765 | autoRename: true,
766 | fontSize: 12,
767 | paragraphIndent: 0,
768 | paragraphSpacing: 0,
769 | textAlignHorizontal: "LEFT",
770 | textAlignVertical: "TOP",
771 | textCase: "ORIGINAL",
772 | textDecoration: "NONE",
773 | textAutoResize: "", // It's actually WIDTH_AND_HEIGHT but when node is resizes, it gets reset, so we add it anyway
774 | letterSpacing: {
775 | unit: "PERCENT",
776 | value: 0,
777 | },
778 | lineHeight: {
779 | unit: "AUTO",
780 | },
781 | fontName: {
782 | family: "Roboto",
783 | style: "Regular",
784 | },
785 | reactions: [],
786 | hyperlink: null,
787 | layoutPositioning: "AUTO",
788 | },
789 | COMPONENT: {
790 | name: "Component",
791 | visible: true,
792 | locked: false,
793 | opacity: 1,
794 | blendMode: "PASS_THROUGH",
795 | isMask: false,
796 | effects: [],
797 | relativeTransform: [
798 | [1, 0, 0],
799 | [0, 1, 0],
800 | ],
801 | absoluteTransform: [
802 | [1, 0, 0],
803 | [0, 1, 0],
804 | ],
805 | x: 0,
806 | y: 0,
807 | width: 100,
808 | height: 100,
809 | rotation: 0,
810 | layoutAlign: "INHERIT",
811 | constrainProportions: false,
812 | layoutGrow: 0,
813 | exportSettings: [],
814 | fills: [
815 | {
816 | type: "SOLID",
817 | visible: false,
818 | opacity: 1,
819 | blendMode: "NORMAL",
820 | color: {
821 | r: 1,
822 | g: 1,
823 | b: 1,
824 | },
825 | },
826 | ],
827 | strokes: [],
828 | strokeWeight: 1,
829 | strokeAlign: "INSIDE",
830 | strokeCap: "NONE",
831 | strokeJoin: "MITER",
832 | strokeMiterLimit: 4,
833 | dashPattern: [],
834 | cornerRadius: 0,
835 | cornerSmoothing: 0,
836 | topLeftRadius: 0,
837 | topRightRadius: 0,
838 | bottomLeftRadius: 0,
839 | bottomRightRadius: 0,
840 | paddingLeft: 0,
841 | paddingRight: 0,
842 | paddingTop: 0,
843 | paddingBottom: 0,
844 | primaryAxisAlignItems: "MIN",
845 | counterAxisAlignItems: "MIN",
846 | primaryAxisSizingMode: "AUTO",
847 | layoutGrids: [],
848 | backgrounds: [
849 | {
850 | type: "SOLID",
851 | visible: false,
852 | opacity: 1,
853 | blendMode: "NORMAL",
854 | color: {
855 | r: 1,
856 | g: 1,
857 | b: 1,
858 | },
859 | },
860 | ],
861 | clipsContent: false,
862 | guides: [],
863 | expanded: true,
864 | constraints: {
865 | horizontal: "MIN",
866 | vertical: "MIN",
867 | },
868 | layoutMode: "NONE",
869 | counterAxisSizingMode: "FIXED",
870 | itemSpacing: 0,
871 | overflowDirection: "NONE",
872 | numberOfFixedChildren: 0,
873 | overlayPositionType: "CENTER",
874 | overlayBackground: {
875 | type: "NONE",
876 | },
877 | overlayBackgroundInteraction: "NONE",
878 | remote: false,
879 | reactions: [],
880 | description: "",
881 | documentationLinks: [],
882 | layoutPositioning: "AUTO",
883 | itemReverseZIndex: false,
884 | strokesIncludedInLayout: false,
885 | },
886 | COMPONENT_SET: {},
887 | INSTANCE: {
888 | x: 0,
889 | y: 0,
890 | scaleFactor: 1,
891 | },
892 | };
893 |
894 | export const readOnlyProps: string[] = [
895 | "id",
896 | "parent",
897 | "removed",
898 | "children",
899 | "width",
900 | "height",
901 | "overlayPositionType",
902 | "overlayBackground",
903 | "overlayBackgroundInteraction",
904 | "reactions",
905 | "remote",
906 | "key",
907 | "type",
908 | "defaultVariant",
909 | "hasMissingFont",
910 | "characters", // Not a readonly prop
911 | // 'relativeTransform', // Need to check if same as default x y coordinates to avoid unnecessary code
912 | "absoluteTransform",
913 | // 'horizontalPadding', // Not a readonly prop, just want to ignore
914 | // 'verticalPadding', // Not a readonly prop, just want to ignore
915 | "mainComponent", // Not a readonly prop, just want to ignore
916 | "masterComponent", // Not a readonly prop, just want to ignore
917 | ];
918 |
919 | export const textProps: string[] = [
920 | "characters",
921 | "fontSize",
922 | "fontName",
923 | "textStyleId",
924 | "textCase",
925 | "textDecoration",
926 | "letterSpacing",
927 | "lineHeight",
928 | "textAlignVertical",
929 | "textAlignHorizontal",
930 | "textAutoResize",
931 | "listSpacing",
932 | ];
933 |
934 | export const styleProps: string[] = [
935 | "fillStyleId",
936 | "strokeStyleId",
937 | "textStyleId",
938 | "effectStyleId",
939 | "gridStyleId",
940 | "backgroundStyleId",
941 | ];
942 |
943 | export var dynamicProps = ["width", "height"];
944 |
--------------------------------------------------------------------------------
/src/setCharacters.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This helper will check font and set fallback before set characters to node. Useful to work with TextNode content changes.
3 | * For example:
4 | * ```diff
5 | * const text = "New text for label";
6 | * const labelNode = figma.currentPage.findOne((el) => el.type === "TEXT");
7 | * - await figma.loadFontAsync({
8 | * - family: labelNode.fontName.family,
9 | * - style: labelNode.fontName.style
10 | * - })
11 | * - labelNode.characters = text;
12 | * + await setCharacters(labelNode, text);
13 | * ```
14 | *
15 | * Provided example doesn't handle many annoying cases like, not existed or multiple fonts, which expand code a lot. `setCharacters` cover this cases and reducing noise.
16 | *
17 | * @param node Target node to set characters
18 | * @param characters String of characters to set
19 | * @param options Parser options
20 | * @param options.fallbackFont Font that will be applied to target node, if original will fail to load. By default is "Roboto Regular"
21 | * @param options.smartStrategy Parser stragtegy, that allows to set font family and styles to characters in more flexible way
22 | */
23 |
24 | import { uniqBy } from '../node_modules/lodash/lodash'
25 |
26 | interface FontLinearItem {
27 | family: string
28 | style: string
29 | start?: number
30 | delimiter: '\n' | ' '
31 | }
32 |
33 | export const setCharacters = async (
34 | node: TextNode,
35 | characters: string,
36 | options?: {
37 | smartStrategy?: 'prevail' | 'strict' | 'experimental'
38 | fallbackFont?: FontName
39 | }
40 | ): Promise => {
41 | const fallbackFont = options?.fallbackFont || {
42 | family: 'Roboto',
43 | style: 'Regular'
44 | }
45 | try {
46 | if (node.fontName === figma.mixed) {
47 | if (options?.smartStrategy === 'prevail') {
48 | const fontHashTree: { [key: string]: number } = {}
49 | for (let i = 1; i < node.characters.length; i++) {
50 | const charFont = node.getRangeFontName(i - 1, i) as FontName
51 | const key = `${charFont.family}::${charFont.style}`
52 | fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1
53 | }
54 | const prevailedTreeItem = Object.entries(fontHashTree).sort(
55 | (a, b) => b[1] - a[1]
56 | )[0]
57 | const [family, style] = prevailedTreeItem[0].split('::')
58 | const prevailedFont = {
59 | family,
60 | style
61 | } as FontName
62 | await figma.loadFontAsync(prevailedFont)
63 | node.fontName = prevailedFont
64 | } else if (options?.smartStrategy === 'strict') {
65 | return setCharactersWithStrictMatchFont(node, characters, fallbackFont)
66 | } else if (options?.smartStrategy === 'experimental') {
67 | return setCharactersWithSmartMatchFont(node, characters, fallbackFont)
68 | } else {
69 | const firstCharFont = node.getRangeFontName(0, 1) as FontName
70 | await figma.loadFontAsync(firstCharFont)
71 | node.fontName = firstCharFont
72 | }
73 | } else {
74 | await figma.loadFontAsync({
75 | family: node.fontName.family,
76 | style: node.fontName.style
77 | })
78 | }
79 | } catch (err) {
80 | console.warn(
81 | `Failed to load "${node.fontName['family']} ${node.fontName['style']}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
82 | err
83 | )
84 | await figma.loadFontAsync(fallbackFont)
85 | node.fontName = fallbackFont
86 | }
87 | try {
88 | node.characters = characters
89 | return true
90 | } catch (err) {
91 | console.warn(`Failed to set characters. Skipped.`, err)
92 | return false
93 | }
94 | }
95 |
96 | const setCharactersWithStrictMatchFont = async (
97 | node: TextNode,
98 | characters: string,
99 | fallbackFont?: FontName
100 | ): Promise => {
101 | const fontHashTree: { [key: string]: string } = {}
102 | for (let i = 1; i < node.characters.length; i++) {
103 | const startIdx = i - 1
104 | const startCharFont = node.getRangeFontName(startIdx, i) as FontName
105 | const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`
106 | while (i < node.characters.length) {
107 | i++
108 | const charFont = node.getRangeFontName(i - 1, i) as FontName
109 | if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
110 | break
111 | }
112 | }
113 | fontHashTree[`${startIdx}_${i}`] = startCharFontVal
114 | }
115 | await figma.loadFontAsync(fallbackFont)
116 | node.fontName = fallbackFont
117 | node.characters = characters
118 | console.log(fontHashTree)
119 | await Promise.all(
120 | Object.keys(fontHashTree).map(async (range) => {
121 | console.log(range, fontHashTree[range])
122 | const [start, end] = range.split('_')
123 | const [family, style] = fontHashTree[range].split('::')
124 | const matchedFont = {
125 | family,
126 | style
127 | } as FontName
128 | await figma.loadFontAsync(matchedFont)
129 | return node.setRangeFontName(Number(start), Number(end), matchedFont)
130 | })
131 | )
132 | return true
133 | }
134 |
135 | const getDelimiterPos = (
136 | str: string,
137 | delimiter: string,
138 | startIdx = 0,
139 | endIdx: number = str.length
140 | ): [number, number][] => {
141 | const indices = []
142 | let temp = startIdx
143 | for (let i = startIdx; i < endIdx; i++) {
144 | if (str[i] === delimiter && i + startIdx !== endIdx && temp !== i + startIdx) {
145 | indices.push([temp, i + startIdx])
146 | temp = i + startIdx + 1
147 | }
148 | }
149 | temp !== endIdx && indices.push([temp, endIdx])
150 | return indices.filter(Boolean)
151 | }
152 |
153 | const buildLinearOrder = (node: TextNode) => {
154 | const fontTree: FontLinearItem[] = []
155 | const newLinesPos = getDelimiterPos(node.characters, '\n')
156 | newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
157 | const newLinesRangeFont = node.getRangeFontName(newLinesRangeStart, newLinesRangeEnd)
158 | if (newLinesRangeFont === figma.mixed) {
159 | const spacesPos = getDelimiterPos(
160 | node.characters,
161 | ' ',
162 | newLinesRangeStart,
163 | newLinesRangeEnd
164 | )
165 | spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
166 | const spacesRangeFont = node.getRangeFontName(spacesRangeStart, spacesRangeEnd)
167 | if (spacesRangeFont === figma.mixed) {
168 | const spacesRangeFont = node.getRangeFontName(
169 | spacesRangeStart,
170 | spacesRangeStart[0]
171 | ) as FontName
172 | fontTree.push({
173 | start: spacesRangeStart,
174 | delimiter: ' ',
175 | family: spacesRangeFont.family,
176 | style: spacesRangeFont.style
177 | })
178 | } else {
179 | fontTree.push({
180 | start: spacesRangeStart,
181 | delimiter: ' ',
182 | family: spacesRangeFont.family,
183 | style: spacesRangeFont.style
184 | })
185 | }
186 | })
187 | } else {
188 | fontTree.push({
189 | start: newLinesRangeStart,
190 | delimiter: '\n',
191 | family: newLinesRangeFont.family,
192 | style: newLinesRangeFont.style
193 | })
194 | }
195 | })
196 | return fontTree
197 | .sort((a, b) => +a.start - +b.start)
198 | .map(({ family, style, delimiter }) => ({ family, style, delimiter }))
199 | }
200 |
201 | const setCharactersWithSmartMatchFont = async (
202 | node: TextNode,
203 | characters: string,
204 | fallbackFont?: FontName
205 | ): Promise => {
206 | const rangeTree = buildLinearOrder(node)
207 | const fontsToLoad = uniqBy(rangeTree, ({ family, style }) => `${family}::${style}`).map(
208 | ({ family, style }): FontName => ({
209 | family,
210 | style
211 | })
212 | )
213 |
214 | await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync))
215 |
216 | node.fontName = fallbackFont
217 | node.characters = characters
218 |
219 | let prevPos = 0
220 | rangeTree.forEach(({ family, style, delimiter }) => {
221 | if (prevPos < node.characters.length) {
222 | const delimeterPos = node.characters.indexOf(delimiter, prevPos)
223 | const endPos = delimeterPos > prevPos ? delimeterPos : node.characters.length
224 | const matchedFont = {
225 | family,
226 | style
227 | }
228 | node.setRangeFontName(prevPos, endPos, matchedFont)
229 | prevPos = endPos + 1
230 | }
231 | })
232 | return true
233 | }
--------------------------------------------------------------------------------
/src/str.ts:
--------------------------------------------------------------------------------
1 | import { stripIndent } from 'common-tags/dist/common-tags.min.js'
2 |
3 | function trim(str) {
4 | // return str.replace(/\n$/g, "");
5 | return str
6 | };
7 |
8 | function removeIndent(str) {
9 | var indentMatches = /\s*\n(\s+)/.exec(str);
10 | if (indentMatches) {
11 | var indent = indentMatches[1];
12 | str = str.replace(new RegExp("^" + indent, "mg"), "");
13 | }
14 | // Remove new line at start of string
15 | str = str.replace(/^\n/, "")
16 | return str;
17 | }
18 |
19 |
20 |
21 | // function Str(this: any) {
22 | export class Str {
23 | constructor() {
24 | var output = "";
25 |
26 | function init(strings?: any, ...values: any): any {
27 |
28 | if (Array.isArray(strings)) {
29 | let str = '';
30 |
31 | strings.forEach((string, a) => {
32 | // Avoids zeros being squished
33 | if (values[a] === 0) values[a] = values[a].toString()
34 |
35 | str += string + (values[a] || '');
36 |
37 | });
38 |
39 | output += str
40 |
41 | }
42 |
43 | if (!strings) {
44 | return output
45 | }
46 | }
47 |
48 | init.prepend = function (strings?: any, ...values: any): any {
49 |
50 | if (Array.isArray(strings)) {
51 | let str = '';
52 |
53 | strings.forEach((string, a) => {
54 | // Avoids zeros being squished
55 | if (values[a] === 0) values[a] = values[a].toString()
56 |
57 | str += string + (values[a] || '');
58 |
59 | });
60 |
61 | // output = removeIndent(str) + output
62 | output = str + output
63 |
64 | }
65 | }
66 |
67 | return init
68 | // }
69 | }
70 | }
71 |
72 |
73 | export default Str
74 |
--------------------------------------------------------------------------------
/src/syntax-theme.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Github-like theme for Prism.js
3 | * @author Luke Askew http://github.com/lukeaskew
4 | */
5 | code,
6 | code[class*='language-'],
7 | pre[class*='language-'] {
8 | color: #333;
9 | text-align: left;
10 | white-space: pre;
11 | word-spacing: normal;
12 | tab-size: 4;
13 | hyphens: none;
14 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
15 | line-height: 1.4;
16 | direction: ltr;
17 | cursor: text;
18 | }
19 |
20 | pre[class*='language-'] {
21 | overflow: auto;
22 | margin: 1em 0;
23 | padding: 1.2em;
24 | border-radius: 3px;
25 | font-size: 85%;
26 | }
27 |
28 | p code,
29 | li code,
30 | table code {
31 | margin: 0;
32 | border-radius: 3px;
33 | padding: 0.2em 0;
34 | font-size: 85%;
35 | }
36 | p code:before, p code:after,
37 | li code:before,
38 | li code:after,
39 | table code:before,
40 | table code:after {
41 | letter-spacing: -0.2em;
42 | content: '\00a0';
43 | }
44 |
45 | code,
46 | :not(pre) > code[class*='language-'],
47 | pre[class*='language-'] {
48 | background: #f7f7f7;
49 | }
50 |
51 | :not(pre) > code[class*='language-'] {
52 | padding: 0.1em;
53 | border-radius: 0.3em;
54 | }
55 |
56 | .token.comment, .token.prolog, .token.doctype, .token.cdata {
57 | color: #969896;
58 | }
59 | .token.punctuation, .token.string, .token.atrule, .token.attr-value {
60 | color: #183691;
61 | }
62 | .token.property, .token.tag {
63 | color: #63a35c;
64 | }
65 | .token.boolean, .token.number {
66 | color: #0086b3;
67 | }
68 | .token.selector, .token.attr-name, .token.attr-value .punctuation:first-child, .token.keyword, .token.regex, .token.important {
69 | color: #795da3;
70 | }
71 | .token.operator, .token.entity, .token.url, .language-css .token.string {
72 | color: #a71d5d;
73 | }
74 | .token.entity {
75 | cursor: help;
76 | }
77 |
78 | .namespace {
79 | opacity: 0.7;
80 | }
--------------------------------------------------------------------------------
/src/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/widgetGeneration.ts:
--------------------------------------------------------------------------------
1 | import { isArray } from "lodash";
2 | import v from "voca";
3 |
4 | // function isObj(value) {
5 | // return typeof value === 'object' &&
6 | // !Array.isArray(value) &&
7 | // value !== null
8 | // }
9 |
10 | function Utf8ArrayToStr(array) {
11 | var out, i, len, c;
12 | var char2, char3;
13 |
14 | out = "";
15 | len = array.length;
16 | i = 0;
17 | while (i < len) {
18 | c = array[i++];
19 | switch (c >> 4) {
20 | case 0:
21 | case 1:
22 | case 2:
23 | case 3:
24 | case 4:
25 | case 5:
26 | case 6:
27 | case 7:
28 | // 0xxxxxxx
29 | out += String.fromCharCode(c);
30 | break;
31 | case 12:
32 | case 13:
33 | // 110x xxxx 10xx xxxx
34 | char2 = array[i++];
35 | out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
36 | break;
37 | case 14:
38 | // 1110 xxxx 10xx xxxx 10xx xxxx
39 | char2 = array[i++];
40 | char3 = array[i++];
41 | out += String.fromCharCode(
42 | ((c & 0x0f) << 12) |
43 | ((char2 & 0x3f) << 6) |
44 | ((char3 & 0x3f) << 0)
45 | );
46 | break;
47 | }
48 | }
49 |
50 | return out;
51 | }
52 |
53 | function isSymbol(x) {
54 | return (
55 | typeof x === "symbol" ||
56 | (typeof x === "object" &&
57 | Object.prototype.toString.call(x) === "[object Symbol]")
58 | );
59 | }
60 |
61 | function isObj(val) {
62 | if (val === null) {
63 | return false;
64 | }
65 | return typeof val === "function" || typeof val === "object";
66 | }
67 |
68 | function isStr(val) {
69 | if (typeof val === "string" || val instanceof String) return val;
70 | }
71 |
72 | function simpleClone(val) {
73 | return JSON.parse(JSON.stringify(val));
74 | }
75 |
76 | function componentToHex(c) {
77 | c = Math.floor(c * 255);
78 | var hex = c.toString(16);
79 | return hex.length == 1 ? "0" + hex : hex;
80 | }
81 |
82 | function rgbToHex(rgb) {
83 | if (rgb) {
84 | let { r, g, b } = rgb;
85 | return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
86 | }
87 | }
88 |
89 | function hexToRgb(hex) {
90 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
91 | return result
92 | ? {
93 | r: parseInt(result[1], 16) / 255,
94 | g: parseInt(result[2], 16) / 255,
95 | b: parseInt(result[3], 16) / 255,
96 | }
97 | : null;
98 | }
99 |
100 | async function walkNodes(nodes, callback) {
101 | var string = "";
102 | var depth = 0;
103 | var count = 0;
104 | let tab = `\t`;
105 |
106 | function* processNodes(nodes) {
107 | const len = nodes.length;
108 | if (len === 0) {
109 | return;
110 | }
111 |
112 | for (var i = 0; i < len; i++) {
113 | var node = nodes[i];
114 | let {
115 | before,
116 | during,
117 | after,
118 | stop = false,
119 | skip = false,
120 | } = yield node;
121 |
122 | if (skip) {
123 | } else {
124 | let children = node.children;
125 |
126 | if (before) {
127 | // console.log("before", before(node))
128 | string += tab.repeat(depth) + before();
129 | }
130 |
131 | if (!stop) {
132 | if (children) {
133 | if (during && typeof during() !== "undefined") {
134 | // console.log(during())
135 | string += tab.repeat(depth) + during();
136 | }
137 |
138 | yield* processNodes(children);
139 | } else if (node.characters) {
140 | if (during) {
141 | string += tab.repeat(depth + 1) + during();
142 | }
143 | }
144 | }
145 |
146 | if (after) {
147 | // console.log("after", after(node))
148 | string += tab.repeat(depth) + after();
149 | depth--;
150 | }
151 | }
152 | }
153 | }
154 |
155 | console.log("Generating widget code...");
156 |
157 | var tree = processNodes(nodes);
158 | var res = tree.next();
159 |
160 | while (!res.done) {
161 | // console.log(res.value);
162 | let node = res.value;
163 | let component;
164 |
165 | function sanitiseValue(value) {
166 | if (typeof value !== "undefined") {
167 | function doThingOnValue(value) {
168 | // Convert snakeCase and upperCase to kebabCase and lowercase
169 | if (isStr(value)) {
170 | value = v.lowerCase(v.kebabCase(value));
171 | }
172 |
173 | if (value === "min") value = "start";
174 | if (value === "max") value = "end";
175 | // Set to undefined to remove, as this is space = "auto" in widget land
176 | if (value === "space-between") value = undefined;
177 |
178 | return value;
179 | }
180 |
181 | var newValue;
182 |
183 | if (isObj(value)) {
184 | var cloneValue = simpleClone(value);
185 | for (let [key, value] of Object.entries(cloneValue)) {
186 | cloneValue[key] = doThingOnValue(value);
187 |
188 | // if (key === "opacity") {
189 |
190 | // // cloneValue['color']['a'] = "test"
191 | // // console.log(cloneValue)
192 | // Object.defineProperty(cloneValue['color'], 'a', Object.getOwnPropertyDescriptor(cloneValue, 'opacity'));
193 | // // console.log(cloneValue)
194 | // }
195 |
196 | // Convert radius to blur for effects
197 | if (key === "radius") {
198 | // Use this to rename property
199 | Object.defineProperty(
200 | cloneValue,
201 | "blur",
202 | Object.getOwnPropertyDescriptor(
203 | cloneValue,
204 | "radius"
205 | )
206 | );
207 | delete cloneValue["radius"];
208 | }
209 |
210 | // console.log(key, value)
211 | }
212 |
213 | newValue = cloneValue;
214 | }
215 |
216 | if (Array.isArray(value)) {
217 | var cloneValue = simpleClone(value);
218 | for (var i = 0; i < cloneValue.length; i++) {
219 | var item = cloneValue[i];
220 | for (let [key, value] of Object.entries(item)) {
221 | item[key] = doThingOnValue(value);
222 |
223 | // if (key === "opacity") {
224 | // console.log(item[key])
225 | // }
226 |
227 | // Convert radius to blur for effects
228 | if (key === "radius") {
229 | // Use this to rename property
230 | Object.defineProperty(
231 | item,
232 | "blur",
233 | Object.getOwnPropertyDescriptor(
234 | item,
235 | "radius"
236 | )
237 | );
238 | delete item["radius"];
239 | }
240 |
241 | // console.log(key, value)
242 | }
243 | cloneValue[i] = item;
244 | }
245 | newValue = cloneValue;
246 | }
247 |
248 | if (isStr(value)) {
249 | newValue = doThingOnValue(value);
250 | }
251 |
252 | if (!isNaN(value)) {
253 | newValue = value;
254 | }
255 |
256 | return newValue;
257 | }
258 | }
259 |
260 | function genWidthHeightProps(node) {
261 | var width = node.width;
262 | var height = node.height;
263 |
264 | width = (() => {
265 | if (node.width < 0.01) {
266 | return 0.01;
267 | } else {
268 | return node.width;
269 | }
270 | })();
271 |
272 | height = (() => {
273 | if (node.height < 0.01) {
274 | return 0.01;
275 | } else {
276 | return node.height;
277 | }
278 | })();
279 |
280 | // console.log({
281 | // parentLayoutMode: node.parent.layoutMode,
282 | // layoutMode: node.layoutMode,
283 | // counterAxisSizingMode: node.counterAxisSizingMode,
284 | // primaryAxisSizingMode: node.primaryAxisSizingMode,
285 | // layoutAlign: node.layoutAlign,
286 | // layoutGrow: node.layoutGrow
287 | // })
288 |
289 | // if (node.layoutMode && node.layoutMode !== "NONE") {
290 | if (
291 | (node.layoutMode === "HORIZONTAL" &&
292 | node.primaryAxisSizingMode === "AUTO") ||
293 | (node.layoutMode === "VERTICAL" &&
294 | node.counterAxisSizingMode === "AUTO") ||
295 | ((node.parent.layoutMode === "NONE" ||
296 | !node.parent.layoutMode) &&
297 | node.layoutMode === "HORIZONTAL" &&
298 | node.primaryAxisSizingMode === "AUTO" &&
299 | node.layoutGrow === 0) ||
300 | ((node.parent.layoutMode === "NONE" ||
301 | !node.parent.layoutMode) &&
302 | node.layoutMode === "VERTICAL" &&
303 | node.counterAxisSizingMode === "AUTO" &&
304 | node.layoutGrow === 0)
305 | ) {
306 | width = "hug-contents";
307 | }
308 | if (
309 | (node.layoutMode === "HORIZONTAL" &&
310 | node.counterAxisSizingMode === "AUTO") ||
311 | (node.layoutMode === "VERTICAL" &&
312 | node.primaryAxisSizingMode === "AUTO") ||
313 | ((node.parent.layoutMode === "NONE" ||
314 | !node.parent.layoutMode) &&
315 | node.layoutMode === "VERTICAL" &&
316 | node.primaryAxisSizingMode === "AUTO" &&
317 | node.layoutGrow === 0) ||
318 | ((node.parent.layoutMode === "NONE" ||
319 | !node.parent.layoutMode) &&
320 | node.layoutMode === "HORIZONTAL" &&
321 | node.counterAxisSizingMode === "AUTO" &&
322 | node.layoutGrow === 0)
323 | ) {
324 | height = "hug-contents";
325 | }
326 |
327 | if (
328 | (node.parent.layoutMode === "HORIZONTAL" &&
329 | node.layoutGrow === 1) ||
330 | (node.parent.layoutMode === "VERTICAL" &&
331 | node.layoutAlign === "STRETCH")
332 | ) {
333 | width = "fill-parent";
334 | }
335 |
336 | if (
337 | (node.parent.layoutMode === "HORIZONTAL" &&
338 | node.layoutAlign === "STRETCH") ||
339 | (node.parent.layoutMode === "VERTICAL" && node.layoutGrow === 1)
340 | ) {
341 | height = "fill-parent";
342 | }
343 |
344 | // FIXME: Add rules to prevent width and height being added to text unless fixed
345 |
346 | if (node.textAutoResize === "HEIGHT") {
347 | height = "hug-contents";
348 | }
349 |
350 | if (node.textAutoResize === "WIDTH_AND_HEIGHT") {
351 | height = "hug-contents";
352 | width = "hug-contents";
353 | }
354 | // }
355 |
356 | var obj = {
357 | width,
358 | height,
359 | };
360 |
361 | // console.log(obj)
362 |
363 | return obj;
364 | }
365 |
366 | var props = {
367 | ...genWidthHeightProps(node),
368 | name: node.name,
369 | hidden: !node.visible,
370 | x: (() => {
371 | // if (node.constraints?.horizontal) {
372 | // return sanitiseValue(node.constraints?.horizontal)
373 | // }
374 | // else {
375 | return node.x;
376 | // }
377 | })(),
378 | y: (() => {
379 | // if (node.constraints?.vertical) {
380 | // return sanitiseValue(node.constraints?.vertical)
381 | // }
382 | // else {
383 | return node.y;
384 | // }
385 | })(),
386 | blendMode: sanitiseValue(node.blendMode),
387 | opacity: node.opacity,
388 | // effect: Effect,
389 | fill: (() => {
390 | if (node.fills && node.fills.length > 0) {
391 | if (node.fills[0].visible) {
392 | return sanitiseValue(node.fills[0]);
393 | }
394 |
395 | // if (node.fills[0].opacity === 1) {
396 | // return rgbToHex(node.fills[0]?.color)
397 | // }
398 | // else {
399 | // console.log("Fill cannot have opacity")
400 | // return undefined
401 | // }
402 | }
403 | })(),
404 | // stroke: rgbToHex(node.strokes[0]?.color), // Will support GradientPaint in future
405 | stroke: (() => {
406 | if (node.strokes && node.strokes.length > 0) {
407 | if (node.strokes[0].visible) {
408 | return sanitiseValue(node.strokes[0]);
409 | }
410 |
411 | // if (node.strokes[0].opacity === 1) {
412 | // return rgbToHex(node.strokes[0]?.color)
413 | // }
414 | // else {
415 | // console.log("Stroke cannot have opacity")
416 | // return undefined
417 | // }
418 | }
419 | })(),
420 | strokeWidth: node.strokeWeight,
421 | strokeAlign: sanitiseValue(node.strokeAlign),
422 | rotation: node.rotation,
423 | cornerRadius: {
424 | topLeft: node.topLeftRadius,
425 | topRight: node.topRightRadius,
426 | bottomLeft: node.bottomLeftRadius,
427 | bottomRight: node.bottomRightRadius,
428 | },
429 | padding: {
430 | top: node.paddingBottom,
431 | right: node.paddingRight,
432 | bottom: node.paddingBottom,
433 | left: node.paddingLeft,
434 | },
435 | spacing: (() => {
436 | if (
437 | node.primaryAxisAlignItems === "SPACE_BETWEEN" ||
438 | node.counterAxisAlignItems === "SPACE_BETWEEN"
439 | ) {
440 | return "auto";
441 | } else {
442 | return node.itemSpacing;
443 | }
444 | })(),
445 | effect: (() => {
446 | if (node.effects && node.effects.length > 0) {
447 | return sanitiseValue(node.effects);
448 | }
449 | })(),
450 |
451 | // effect: sanitiseValue(node.effects[0]),
452 | direction: sanitiseValue(node.layoutMode),
453 | fontSize: node.fontSize,
454 | fontFamily: node.fontName?.family,
455 | fontWeight: (() => {
456 | if (node.fontName) return sanitiseValue(node.fontName.style);
457 | // switch (node.fontName?.style) {
458 |
459 | // case "Thin":
460 | // return 100
461 | // break
462 | // case "ExtraLight":
463 | // return 200
464 | // break
465 | // case "Medium":
466 | // return 300
467 | // break
468 | // case "Normal":
469 | // return 400
470 | // break
471 | // case "Medium":
472 | // return 500
473 | // break
474 | // case "SemiBold" && "Semi Bold":
475 | // return 600
476 | // break
477 | // case "Bold":
478 | // return 700
479 | // break
480 | // case "ExtraBold":
481 | // return 800
482 | // break
483 | // case "Black" && "Heavy":
484 | // return 900
485 | // break
486 | // default: 400
487 | // }
488 | })(),
489 | textDecoration: sanitiseValue(node.textDecoration),
490 | horizontalAlignText: sanitiseValue(node.textAlignHorizontal),
491 | verticalAlignText: sanitiseValue(node.textAlignVertical),
492 | lineHeight: (() => {
493 | if (node.lineHeight) {
494 | return sanitiseValue(node.lineHeight.value);
495 | }
496 | })(),
497 | letterSpacing: (() => {
498 | if (node.letterSpacing?.unit) {
499 | var unit;
500 | if (node.letterSpacing.unit === "PERCENT") {
501 | unit = "%";
502 | }
503 | if (node.letterSpacing.unit === "PIXELS") {
504 | unit = "px";
505 | }
506 | return node.letterSpacing.value + unit;
507 | }
508 | })(),
509 | textCase: sanitiseValue(node.textCase),
510 | horizontalAlignItems: (() => {
511 | if (node.layoutMode === "HORIZONTAL") {
512 | return sanitiseValue(node.primaryAxisAlignItems);
513 | }
514 | if (node.layoutMode === "VERTICAL") {
515 | return sanitiseValue(node.counterAxisAlignItems);
516 | }
517 | })(),
518 | verticalAlignItems: (() => {
519 | if (node.layoutMode === "HORIZONTAL") {
520 | return sanitiseValue(node.counterAxisAlignItems);
521 | }
522 | if (node.layoutMode === "VERTICAL") {
523 | return sanitiseValue(node.primaryAxisAlignItems);
524 | }
525 | })(),
526 | overflow: (() => {
527 | if (node.clipsContent) {
528 | return "hidden";
529 | } else {
530 | return "visible";
531 | }
532 | })(),
533 | };
534 |
535 | var defaultPropValues = {
536 | Frame: {
537 | name: "",
538 | hidden: false,
539 | x: 0,
540 | y: 0,
541 | blendMode: "normal",
542 | opacity: 1,
543 | effect: [],
544 | fill: [],
545 | stroke: [],
546 | strokeWidth: 1,
547 | strokeAlign: "inside",
548 | rotation: 0,
549 | cornerRadius: 0,
550 | overflow: "scroll",
551 | width: 100,
552 | height: 100,
553 | },
554 | AutoLayout: {
555 | name: "",
556 | hidden: false,
557 | x: 0,
558 | y: 0,
559 | blendMode: "normal",
560 | opacity: 1,
561 | effect: [],
562 | fill: [],
563 | stroke: [],
564 | strokeWidth: 1,
565 | strokeAlign: "inside",
566 | rotation: 0,
567 | flipVertical: false,
568 | cornerRadius: 0,
569 | overflow: "scroll",
570 | width: "hug-contents",
571 | height: "hug-contents",
572 | direction: "horizontal",
573 | spacing: 0,
574 | padding: 0,
575 | horizontalAlignItems: "start",
576 | verticalAlignItems: "start",
577 | },
578 | Text: {
579 | name: "",
580 | hidden: false,
581 | x: 0,
582 | y: 0,
583 | blendMode: "normal",
584 | opacity: 1,
585 | effect: [],
586 | width: "hug-contents",
587 | height: "hug-contents",
588 | rotation: 0,
589 | flipVertical: false,
590 | fontFamily: "Roboto",
591 | horizontalAlignText: "left",
592 | verticalAlignText: "top",
593 | letterSpacing: 0,
594 | lineHeight: "auto",
595 | textDecoration: "none",
596 | textCase: "original",
597 | fontSize: 16,
598 | italic: false,
599 | fill: {
600 | type: "solid",
601 | color: "#000000",
602 | blendMode: "normal",
603 | },
604 | fontWeight: 400,
605 | paragraphIndent: 0,
606 | paragraphSpacing: 0,
607 | },
608 | Rectangle: {
609 | name: "",
610 | hidden: false,
611 | x: 0,
612 | y: 0,
613 | blendMode: "normal",
614 | opacity: 1,
615 | effect: [],
616 | fill: [],
617 | stroke: [],
618 | strokeWidth: 1,
619 | strokeAlign: "inside",
620 | rotation: 0,
621 | flipVertical: false,
622 | cornerRadius: 0,
623 | width: 100,
624 | height: 100,
625 | },
626 | Ellipse: {
627 | name: "",
628 | hidden: false,
629 | x: 0,
630 | y: 0,
631 | blendMode: "normal",
632 | opacity: 1,
633 | effect: [],
634 | fill: [],
635 | stroke: [],
636 | strokeWidth: 1,
637 | strokeAlign: "inside",
638 | rotation: 0,
639 | flipVertical: false,
640 | width: 100,
641 | height: 100,
642 | },
643 | SVG: {
644 | width: 100,
645 | height: 100,
646 | x: 0,
647 | y: 0,
648 | },
649 | };
650 |
651 | if (
652 | node.type === "FRAME" ||
653 | node.type === "GROUP" ||
654 | node.type === "INSTANCE" ||
655 | node.type === "COMPONENT"
656 | ) {
657 | if (node.layoutMode && node.layoutMode !== "NONE") {
658 | component = "AutoLayout";
659 | } else {
660 | component = "Frame";
661 | }
662 | }
663 | if (node.type === "TEXT") {
664 | component = "Text";
665 | }
666 |
667 | if (node.type === "ELLIPSE") {
668 | component = "Ellipse";
669 | }
670 |
671 | if (node.type === "RECTANGLE" || node.type === "LINE") {
672 | component = "Rectangle";
673 | }
674 |
675 | if (
676 | node.type === "VECTOR" ||
677 | node.type === "BOOLEAN_OPERATION" ||
678 | node.type === "POLYGON" ||
679 | node.type === "STAR" ||
680 | (node.exportSettings && node.exportSettings[0]?.format === "SVG")
681 | ) {
682 | component = "SVG";
683 | }
684 |
685 | var svg,
686 | stop = false;
687 |
688 | if (component === "SVG") {
689 | if (node.visible) {
690 | svg = await node.exportAsync({ format: "SVG" });
691 | } else {
692 | // Skip component
693 | component = "skip";
694 | }
695 |
696 | // Don't iterate children
697 | stop = true;
698 | }
699 |
700 | if (!node.visible) {
701 | // Skip component
702 | component = "skip";
703 | }
704 |
705 | function genProps() {
706 | var array = [];
707 | for (let [key, value] of Object.entries(props) as any) {
708 | // If default props for component
709 | if (component && defaultPropValues[component]) {
710 | // Ignore undefined values
711 | if (typeof value !== "undefined") {
712 | // Check property exists for component
713 | if (key in defaultPropValues[component]) {
714 | if (
715 | JSON.stringify(
716 | defaultPropValues[component][key]
717 | ) !== JSON.stringify(value)
718 | ) {
719 | // Certain values need to be wrapped in curly braces
720 | if (!isSymbol(value)) {
721 | if (isNaN(value)) {
722 | if (
723 | typeof value === "object" &&
724 | value !== null
725 | ) {
726 | value = `{${JSON.stringify(
727 | value
728 | )}}`;
729 | } else {
730 | value = `${JSON.stringify(value)}`;
731 | }
732 | } else {
733 | value = `{${value}}`;
734 | }
735 |
736 | // Don't add tabs on first prop
737 | if (array.length === 0) {
738 | array.push(`${key}=${value}`);
739 | } else {
740 | array.push(
741 | `${tab.repeat(
742 | depth
743 | )}${key}=${value}`
744 | );
745 | }
746 | }
747 | }
748 | }
749 | }
750 | }
751 | }
752 | return array.join("\n");
753 | }
754 |
755 | // res = callback({ tree, res, node })
756 |
757 | // Need to use express `callback() || {}` incase the calback returns a nullish value
758 |
759 | // if (node.type === "VECTOR") {
760 | // node.exportAsync({
761 | // format: "SVG"
762 | // }).then((svg) => {
763 | // res = tree.next(callback(node, component, genProps(), svg) || {})
764 | // })
765 |
766 | // }
767 | // else {
768 |
769 | // if (component === "SVG") {
770 | // res = tree.next(await callback(node, component, genProps()) || {})
771 | // console.log(res)
772 | // }
773 | // else {
774 |
775 | if (component !== "skip") {
776 | res = tree.next(
777 | await callback(node, component, genProps(), stop, svg)
778 | );
779 | } else {
780 | res = tree.next({ skip: true });
781 | }
782 |
783 | // }
784 |
785 | count++;
786 | depth++;
787 | }
788 |
789 | if (string === "") {
790 | throw "No output generated from selection";
791 | }
792 |
793 | return string;
794 | }
795 |
796 | export async function genWidgetStr(origSel) {
797 | return walkNodes(origSel, async (node, component, props, stop, svg) => {
798 | if (component) {
799 | return {
800 | stop,
801 | before() {
802 | if (component === "SVG") {
803 | // await new Promise((resolve) => {
804 | // figma.showUI(`
805 | //
824 | // `, { visible: false })
825 |
826 | // figma.ui.postMessage({ type: 'decode-svg', value: svg })
827 |
828 | // figma.ui.onmessage = (msg) => {
829 | // if (msg.type === "encoded-image") {
830 | // console.log(msg.value)
831 | // }
832 |
833 | // }
834 | // resolve()
835 |
836 | // })
837 |
838 | return `<${component} ${props} overflow="visible" src={\`${Utf8ArrayToStr(
839 | svg
840 | )}\`} />\n`;
841 | } else {
842 | return `<${component} ${props}>\n`;
843 | }
844 | },
845 | during() {
846 | if (component === "Text") {
847 | return `${node.characters}\n`;
848 | }
849 | },
850 | after() {
851 | if (component !== "SVG") {
852 | return `${component}>\n`;
853 | } else {
854 | return ``;
855 | }
856 | },
857 | };
858 | } else {
859 | console.log("Node doesn't exist as a React component");
860 | return `Node doesn't exist as a React component`;
861 | }
862 | });
863 | }
864 |
--------------------------------------------------------------------------------
/stylup.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | classes: [
3 | {
4 | name: 'spacing',
5 | class: ['p', 'm'],
6 | children: [
7 | 't',
8 | 'r',
9 | 'b',
10 | 'l'
11 | ],
12 | style({ rule, args, str }) {
13 | if (args) {
14 | let values = args;
15 |
16 | switch (values.length) {
17 | case 1:
18 | values.push(values[0]);
19 | case 2:
20 | values.push(values[0]);
21 | case 3:
22 | values.push(values[1]);
23 | }
24 |
25 | for (let [index, side] of rule.children.entries()) {
26 | str`--${rule.class}${side}: ${values[index]};\n`
27 | }
28 |
29 | return str()
30 | }
31 | }
32 | },
33 | {
34 | name: 'colour',
35 | class: 'c',
36 | style({ rule, str }) {
37 | if (rule.args) {
38 |
39 | str`--${rule.class}: ${rule.args[0]};\n`
40 |
41 | return str()
42 | }
43 | }
44 | },
45 | {
46 | name: 'background color',
47 | class: 'bgc',
48 | style({ rule, str }) {
49 | if (rule.args) {
50 |
51 | str`--${rule.class}: ${rule.args[0]};\n`
52 |
53 | return str()
54 | }
55 | }
56 | },
57 | {
58 | name: 'width',
59 | class: 'w',
60 | style({ rule, str }) {
61 | if (rule.args) {
62 | str`--${rule.class}: ${rule.args[0]};\n`
63 |
64 | return str()
65 | }
66 | }
67 | },
68 | ]
69 | }
70 |
--------------------------------------------------------------------------------
/stylup.js:
--------------------------------------------------------------------------------
1 | import stylupProcessor from 'stylup';
2 |
3 | var matchRecursive = function () {
4 | var htmlOnly = /(?<=\<[^>]*)/gmi
5 |
6 | var formatParts = /^([\S\s]+?)\.\.\.([\S\s]+)/,
7 | metaChar = /[-[\]{}()*+?.\\^$|,]/g,
8 | escape = function (str) {
9 | return str.replace(metaChar, "\\$&");
10 | };
11 |
12 | return function (str, format) {
13 | var p = formatParts.exec(format);
14 | if (!p) throw new Error("format must include start and end tokens separated by '...'");
15 | if (p[1] == p[2]) throw new Error("start and end format tokens cannot be identical");
16 |
17 | var opener = p[1],
18 | closer = p[2],
19 |
20 | /* Use an optimized regex when opener and closer are one character each */
21 | iterator = new RegExp(format.length == 5 ? "[" + escape(opener + closer) + "]" : escape(opener) + "|" + escape(closer), "g"),
22 | results = [],
23 | openTokens, matchStartIndex, match;
24 |
25 | // console.log(iterator.toString())
26 | var endOfLast = 0
27 | var lengthOfStr = str.length
28 |
29 | do {
30 | openTokens = 0;
31 | while (match = iterator.exec(str)) {
32 | var matchEndIndex = match.index + 1
33 | if (match[0] == opener) {
34 | if (!openTokens)
35 | matchStartIndex = iterator.lastIndex - 1;
36 | openTokens++;
37 | } else if (openTokens) {
38 | openTokens--;
39 | if (!openTokens) {
40 | results.push(str.slice(endOfLast, matchStartIndex));
41 | if (str[matchStartIndex - 1] === "=") {
42 | results.push(`"` + str.slice(matchStartIndex, matchEndIndex) + `"`);
43 | }
44 | else {
45 | results.push(str.slice(matchStartIndex, matchEndIndex));
46 | }
47 | endOfLast = matchEndIndex
48 | }
49 | }
50 | }
51 | results.push(str.slice(endOfLast, lengthOfStr));
52 | } while (openTokens && (iterator.lastIndex = matchStartIndex));
53 |
54 | return results;
55 | };
56 | }();
57 |
58 | var example = ` {
66 | if (contenteditable !== undefined && contenteditable !== 'false') {
67 | code = target.innerText;
68 | if (block.querySelector('code') == null) {
69 | block.innerHTML = '
';
70 | }
71 | }
72 | }}>`
73 |
74 | function processForSvelte(content) {
75 | return matchRecursive(content, "{...}").join("")
76 | }
77 |
78 | export const stylup = {
79 | markup({ content, filename }) {
80 | // phtml trips over sveltes markup attribute={handlerbars}. So this replaces those occurances with attribute="{handlebars}"
81 | content = processForSvelte(content)
82 | return stylupProcessor.process(content, { from: filename }).then(result => ({ code: result.html, map: null }));
83 | }
84 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "typeRoots": [
5 | "./node_modules/@types",
6 | "./node_modules/@figma"
7 | ],
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "allowSyntheticDefaultImports": true
11 | }
12 | }
--------------------------------------------------------------------------------