@clovis500c/figma-bridge 0.0.0-stage → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -2
- package/dist/server.js +31012 -0
- package/package.json +61 -4
- package/plugin/code.js +4189 -0
- package/plugin/manifest.json +15 -0
- package/plugin/ui.html +1206 -0
package/plugin/code.js
ADDED
|
@@ -0,0 +1,4189 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// plugin/lib/util.ts
|
|
3
|
+
function round(n) {
|
|
4
|
+
return Math.round(n * 100) / 100;
|
|
5
|
+
}
|
|
6
|
+
function codeError(message, code) {
|
|
7
|
+
const e = new Error(message);
|
|
8
|
+
e.code = code;
|
|
9
|
+
return e;
|
|
10
|
+
}
|
|
11
|
+
var cancelled = {};
|
|
12
|
+
function markCancelled(id) {
|
|
13
|
+
cancelled[id] = true;
|
|
14
|
+
}
|
|
15
|
+
function clearCancelled(id) {
|
|
16
|
+
delete cancelled[id];
|
|
17
|
+
}
|
|
18
|
+
function checkCancelled(id) {
|
|
19
|
+
if (id && cancelled[id])
|
|
20
|
+
throw codeError("Cancelled from the Figma plugin", "CANCELLED");
|
|
21
|
+
}
|
|
22
|
+
function parseHex(hex) {
|
|
23
|
+
let h = String(hex).trim().replace(/^#/, "");
|
|
24
|
+
if (h.length === 3 || h.length === 4) {
|
|
25
|
+
h = h.split("").map(function(c) {
|
|
26
|
+
return c + c;
|
|
27
|
+
}).join("");
|
|
28
|
+
}
|
|
29
|
+
if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/i.test(h))
|
|
30
|
+
throw codeError("Invalid hex color: " + hex, "BAD_ARGS");
|
|
31
|
+
const n = function(i) {
|
|
32
|
+
return parseInt(h.slice(i, i + 2), 16) / 255;
|
|
33
|
+
};
|
|
34
|
+
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
|
|
35
|
+
}
|
|
36
|
+
function toHex(c, opacity) {
|
|
37
|
+
const part = function(v) {
|
|
38
|
+
const s = Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16);
|
|
39
|
+
return s.length === 1 ? "0" + s : s;
|
|
40
|
+
};
|
|
41
|
+
const a = c.a === undefined ? 1 : c.a;
|
|
42
|
+
const alpha = a * (opacity === undefined ? 1 : opacity);
|
|
43
|
+
return ("#" + part(c.r) + part(c.g) + part(c.b) + (alpha < 0.999 ? part(alpha) : "")).toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
function luminance(c) {
|
|
46
|
+
const ch = function(v) {
|
|
47
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
48
|
+
};
|
|
49
|
+
return 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b);
|
|
50
|
+
}
|
|
51
|
+
function contrastRatio(a, b) {
|
|
52
|
+
const la = luminance(a);
|
|
53
|
+
const lb = luminance(b);
|
|
54
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
55
|
+
}
|
|
56
|
+
async function getNode(id) {
|
|
57
|
+
const node = await figma.getNodeByIdAsync(String(id));
|
|
58
|
+
if (!node)
|
|
59
|
+
throw codeError("Node not found: " + id, "NOT_FOUND");
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
async function parentFor(parentId) {
|
|
63
|
+
if (!parentId)
|
|
64
|
+
return figma.currentPage;
|
|
65
|
+
const parent = await getNode(parentId);
|
|
66
|
+
if (!("appendChild" in parent))
|
|
67
|
+
throw codeError("Node " + parentId + " (" + parent.type + ") cannot have children", "BAD_ARGS");
|
|
68
|
+
return parent;
|
|
69
|
+
}
|
|
70
|
+
function place(node, x, y) {
|
|
71
|
+
const c = figma.viewport.center;
|
|
72
|
+
node.x = typeof x === "number" ? x : Math.round(c.x - node.width / 2);
|
|
73
|
+
node.y = typeof y === "number" ? y : Math.round(c.y - node.height / 2);
|
|
74
|
+
}
|
|
75
|
+
function isAutoLayout(node) {
|
|
76
|
+
return !!node && "layoutMode" in node && node.layoutMode !== "NONE";
|
|
77
|
+
}
|
|
78
|
+
function isNode(v) {
|
|
79
|
+
return v && typeof v === "object" && typeof v.id === "string" && typeof v.type === "string" && "removed" in v;
|
|
80
|
+
}
|
|
81
|
+
function toSafe(v, depth, seen) {
|
|
82
|
+
if (v === null || v === undefined)
|
|
83
|
+
return null;
|
|
84
|
+
const t = typeof v;
|
|
85
|
+
if (t === "number")
|
|
86
|
+
return isFinite(v) ? v : String(v);
|
|
87
|
+
if (t === "string" || t === "boolean")
|
|
88
|
+
return v;
|
|
89
|
+
if (t === "bigint" || t === "symbol")
|
|
90
|
+
return String(v);
|
|
91
|
+
if (t === "function")
|
|
92
|
+
return "[Function]";
|
|
93
|
+
if (v instanceof Uint8Array)
|
|
94
|
+
return { type: "Uint8Array", length: v.length };
|
|
95
|
+
if (isNode(v))
|
|
96
|
+
return v.removed ? { id: v.id, removed: true } : { id: v.id, name: v.name, type: v.type };
|
|
97
|
+
if (v instanceof Error)
|
|
98
|
+
return { error: v.message };
|
|
99
|
+
if (depth > 12)
|
|
100
|
+
return "[MaxDepth]";
|
|
101
|
+
if (seen.indexOf(v) !== -1)
|
|
102
|
+
return "[Circular]";
|
|
103
|
+
seen.push(v);
|
|
104
|
+
let out;
|
|
105
|
+
if (Array.isArray(v)) {
|
|
106
|
+
out = v.slice(0, 5000).map(function(x) {
|
|
107
|
+
return toSafe(x, depth + 1, seen);
|
|
108
|
+
});
|
|
109
|
+
if (v.length > 5000)
|
|
110
|
+
out.push("… " + (v.length - 5000) + " more");
|
|
111
|
+
} else if (v instanceof Map) {
|
|
112
|
+
out = {};
|
|
113
|
+
v.forEach(function(val, key) {
|
|
114
|
+
out[String(key)] = toSafe(val, depth + 1, seen);
|
|
115
|
+
});
|
|
116
|
+
} else if (v instanceof Set) {
|
|
117
|
+
out = toSafe(Array.from(v), depth + 1, seen);
|
|
118
|
+
} else {
|
|
119
|
+
out = {};
|
|
120
|
+
const keys = Object.keys(v);
|
|
121
|
+
for (let i = 0;i < keys.length; i++)
|
|
122
|
+
out[keys[i]] = toSafe(v[keys[i]], depth + 1, seen);
|
|
123
|
+
}
|
|
124
|
+
seen.pop();
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function safeStringify(v) {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(toSafe(v, 0, []));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return String(v);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
var WEIGHTS = {
|
|
135
|
+
"100": "Thin",
|
|
136
|
+
"200": "Extra Light",
|
|
137
|
+
"300": "Light",
|
|
138
|
+
"400": "Regular",
|
|
139
|
+
"500": "Medium",
|
|
140
|
+
"600": "Semi Bold",
|
|
141
|
+
"700": "Bold",
|
|
142
|
+
"800": "Extra Bold",
|
|
143
|
+
"900": "Black"
|
|
144
|
+
};
|
|
145
|
+
function parseFont(font, weight, fallbackFamily) {
|
|
146
|
+
let family = fallbackFamily || "Inter";
|
|
147
|
+
let style = "Regular";
|
|
148
|
+
if (typeof font === "string" && font) {
|
|
149
|
+
const i = font.indexOf(":");
|
|
150
|
+
family = i === -1 ? font : font.slice(0, i);
|
|
151
|
+
if (i !== -1)
|
|
152
|
+
style = font.slice(i + 1);
|
|
153
|
+
} else if (font && typeof font.family === "string") {
|
|
154
|
+
family = font.family;
|
|
155
|
+
style = font.style || "Regular";
|
|
156
|
+
}
|
|
157
|
+
if (weight !== undefined && weight !== null)
|
|
158
|
+
style = WEIGHTS[String(weight)] || String(weight);
|
|
159
|
+
return { family, style };
|
|
160
|
+
}
|
|
161
|
+
var fontLoads = {};
|
|
162
|
+
function loadFont(n) {
|
|
163
|
+
const key = n.family + "\x00" + n.style;
|
|
164
|
+
if (!fontLoads[key]) {
|
|
165
|
+
fontLoads[key] = figma.loadFontAsync(n).catch(function(e) {
|
|
166
|
+
delete fontLoads[key];
|
|
167
|
+
throw codeError('Font "' + n.family + " " + n.style + '" is not available: ' + (e && e.message ? e.message : e), "FONT");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return fontLoads[key];
|
|
171
|
+
}
|
|
172
|
+
function fontNamesOf(f) {
|
|
173
|
+
if (typeof f === "string" || f && typeof f.family === "string")
|
|
174
|
+
return [parseFont(f)];
|
|
175
|
+
if (isNode(f) && f.type === "TEXT") {
|
|
176
|
+
const text = f;
|
|
177
|
+
if (text.characters.length)
|
|
178
|
+
return text.getRangeAllFontNames(0, text.characters.length);
|
|
179
|
+
return text.fontName === figma.mixed ? [] : [text.fontName];
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(f)) {
|
|
182
|
+
let all = [];
|
|
183
|
+
for (let i = 0;i < f.length; i++)
|
|
184
|
+
all = all.concat(fontNamesOf(f[i]));
|
|
185
|
+
return all;
|
|
186
|
+
}
|
|
187
|
+
throw codeError("loadFonts: expected 'Family:Style', FontName or TextNode, got " + safeStringify(f), "BAD_ARGS");
|
|
188
|
+
}
|
|
189
|
+
var styleCache = null;
|
|
190
|
+
var variableCache = null;
|
|
191
|
+
var CACHE_MS = 5000;
|
|
192
|
+
async function localStyles() {
|
|
193
|
+
if (!styleCache || Date.now() - styleCache.at > CACHE_MS) {
|
|
194
|
+
const res = await Promise.all([figma.getLocalPaintStylesAsync(), figma.getLocalTextStylesAsync(), figma.getLocalEffectStylesAsync()]);
|
|
195
|
+
styleCache = { at: Date.now(), paint: res[0], text: res[1], effect: res[2] };
|
|
196
|
+
}
|
|
197
|
+
return styleCache;
|
|
198
|
+
}
|
|
199
|
+
async function localVariables() {
|
|
200
|
+
if (!variableCache || Date.now() - variableCache.at > CACHE_MS) {
|
|
201
|
+
const res = await Promise.all([figma.variables.getLocalVariablesAsync(), figma.variables.getLocalVariableCollectionsAsync()]);
|
|
202
|
+
variableCache = { at: Date.now(), vars: res[0], collections: res[1] };
|
|
203
|
+
}
|
|
204
|
+
return variableCache;
|
|
205
|
+
}
|
|
206
|
+
function invalidateCaches() {
|
|
207
|
+
styleCache = null;
|
|
208
|
+
variableCache = null;
|
|
209
|
+
}
|
|
210
|
+
async function findVariable(ref) {
|
|
211
|
+
const cache = await localVariables();
|
|
212
|
+
const byId = {};
|
|
213
|
+
for (let i = 0;i < cache.collections.length; i++)
|
|
214
|
+
byId[cache.collections[i].id] = cache.collections[i].name;
|
|
215
|
+
for (let i = 0;i < cache.vars.length; i++) {
|
|
216
|
+
const v = cache.vars[i];
|
|
217
|
+
if (v.name === ref || byId[v.variableCollectionId] + "/" + v.name === ref)
|
|
218
|
+
return v;
|
|
219
|
+
}
|
|
220
|
+
throw codeError('Variable not found: "' + ref + '". Use get_design_system to list them.', "NOT_FOUND");
|
|
221
|
+
}
|
|
222
|
+
async function findStyle(kind, name) {
|
|
223
|
+
const cache = await localStyles();
|
|
224
|
+
const list = kind === "paint" ? cache.paint : kind === "text" ? cache.text : cache.effect;
|
|
225
|
+
for (let i = 0;i < list.length; i++)
|
|
226
|
+
if (list[i].name === name || list[i].id === name)
|
|
227
|
+
return list[i];
|
|
228
|
+
throw codeError("No local " + kind + ' style named "' + name + '". Use get_design_system to list them.', "NOT_FOUND");
|
|
229
|
+
}
|
|
230
|
+
var styleNames = {};
|
|
231
|
+
async function styleName(id) {
|
|
232
|
+
if (!id || typeof id !== "string")
|
|
233
|
+
return null;
|
|
234
|
+
if (styleNames[id] === undefined) {
|
|
235
|
+
const s = await figma.getStyleByIdAsync(id);
|
|
236
|
+
styleNames[id] = s ? s.name : "";
|
|
237
|
+
}
|
|
238
|
+
return styleNames[id] || null;
|
|
239
|
+
}
|
|
240
|
+
var componentCache = null;
|
|
241
|
+
async function localComponents() {
|
|
242
|
+
if (!componentCache || Date.now() - componentCache.at > 15000) {
|
|
243
|
+
await figma.loadAllPagesAsync();
|
|
244
|
+
const all = figma.root.findAllWithCriteria({ types: ["COMPONENT_SET", "COMPONENT"] });
|
|
245
|
+
const list = all.filter(function(n) {
|
|
246
|
+
return !(n.type === "COMPONENT" && n.parent && n.parent.type === "COMPONENT_SET");
|
|
247
|
+
});
|
|
248
|
+
componentCache = { at: Date.now(), list };
|
|
249
|
+
}
|
|
250
|
+
return componentCache.list;
|
|
251
|
+
}
|
|
252
|
+
function pageOf(node) {
|
|
253
|
+
let p = node;
|
|
254
|
+
while (p && p.type !== "PAGE")
|
|
255
|
+
p = p.parent;
|
|
256
|
+
return p;
|
|
257
|
+
}
|
|
258
|
+
function toLab(c) {
|
|
259
|
+
const lin = function(v) {
|
|
260
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
261
|
+
};
|
|
262
|
+
const r = lin(c.r);
|
|
263
|
+
const g = lin(c.g);
|
|
264
|
+
const b = lin(c.b);
|
|
265
|
+
const x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
|
|
266
|
+
const y = 0.2126729 * r + 0.7151522 * g + 0.072175 * b;
|
|
267
|
+
const z = (0.0193339 * r + 0.119192 * g + 0.9503041 * b) / 1.08883;
|
|
268
|
+
const f = function(t) {
|
|
269
|
+
return t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
|
|
270
|
+
};
|
|
271
|
+
return [116 * f(y) - 16, 500 * (f(x) - f(y)), 200 * (f(y) - f(z))];
|
|
272
|
+
}
|
|
273
|
+
function deltaE(a, b) {
|
|
274
|
+
const l1 = toLab(a);
|
|
275
|
+
const l2 = toLab(b);
|
|
276
|
+
const rad = Math.PI / 180;
|
|
277
|
+
const c1 = Math.sqrt(l1[1] * l1[1] + l1[2] * l1[2]);
|
|
278
|
+
const c2 = Math.sqrt(l2[1] * l2[1] + l2[2] * l2[2]);
|
|
279
|
+
const cm = (c1 + c2) / 2;
|
|
280
|
+
const g = 0.5 * (1 - Math.sqrt(Math.pow(cm, 7) / (Math.pow(cm, 7) + Math.pow(25, 7))));
|
|
281
|
+
const a1 = l1[1] * (1 + g);
|
|
282
|
+
const a2 = l2[1] * (1 + g);
|
|
283
|
+
const cp1 = Math.sqrt(a1 * a1 + l1[2] * l1[2]);
|
|
284
|
+
const cp2 = Math.sqrt(a2 * a2 + l2[2] * l2[2]);
|
|
285
|
+
const hue = function(x, y) {
|
|
286
|
+
if (x === 0 && y === 0)
|
|
287
|
+
return 0;
|
|
288
|
+
const h = Math.atan2(y, x) / rad;
|
|
289
|
+
return h < 0 ? h + 360 : h;
|
|
290
|
+
};
|
|
291
|
+
const h1 = hue(a1, l1[2]);
|
|
292
|
+
const h2 = hue(a2, l2[2]);
|
|
293
|
+
const dl = l2[0] - l1[0];
|
|
294
|
+
const dc = cp2 - cp1;
|
|
295
|
+
let dh = 0;
|
|
296
|
+
if (cp1 * cp2 !== 0) {
|
|
297
|
+
dh = h2 - h1;
|
|
298
|
+
if (dh > 180)
|
|
299
|
+
dh -= 360;
|
|
300
|
+
else if (dh < -180)
|
|
301
|
+
dh += 360;
|
|
302
|
+
}
|
|
303
|
+
const dH = 2 * Math.sqrt(cp1 * cp2) * Math.sin(dh / 2 * rad);
|
|
304
|
+
const lm = (l1[0] + l2[0]) / 2;
|
|
305
|
+
const cpm = (cp1 + cp2) / 2;
|
|
306
|
+
let hm = h1 + h2;
|
|
307
|
+
if (cp1 * cp2 !== 0) {
|
|
308
|
+
if (Math.abs(h1 - h2) > 180)
|
|
309
|
+
hm += h1 + h2 < 360 ? 360 : -360;
|
|
310
|
+
hm /= 2;
|
|
311
|
+
}
|
|
312
|
+
const t = 1 - 0.17 * Math.cos((hm - 30) * rad) + 0.24 * Math.cos(2 * hm * rad) + 0.32 * Math.cos((3 * hm + 6) * rad) - 0.2 * Math.cos((4 * hm - 63) * rad);
|
|
313
|
+
const sl = 1 + 0.015 * Math.pow(lm - 50, 2) / Math.sqrt(20 + Math.pow(lm - 50, 2));
|
|
314
|
+
const sc = 1 + 0.045 * cpm;
|
|
315
|
+
const sh = 1 + 0.015 * cpm * t;
|
|
316
|
+
const rt = -2 * Math.sqrt(Math.pow(cpm, 7) / (Math.pow(cpm, 7) + Math.pow(25, 7))) * Math.sin(60 * Math.exp(-Math.pow((hm - 275) / 25, 2)) * rad);
|
|
317
|
+
return Math.sqrt(Math.pow(dl / sl, 2) + Math.pow(dc / sc, 2) + Math.pow(dH / sh, 2) + rt * (dc / sc) * (dH / sh));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// plugin/lib/annotate.ts
|
|
321
|
+
var COLORS = ["yellow", "orange", "red", "pink", "violet", "blue", "teal", "green"];
|
|
322
|
+
async function categoryId(label, color, created) {
|
|
323
|
+
const api = figma.annotations;
|
|
324
|
+
if (!api || typeof api.getAnnotationCategoriesAsync !== "function")
|
|
325
|
+
return;
|
|
326
|
+
const list = await api.getAnnotationCategoriesAsync();
|
|
327
|
+
for (let i = 0;i < list.length; i++)
|
|
328
|
+
if (list[i].label.toLowerCase() === label.toLowerCase())
|
|
329
|
+
return list[i].id;
|
|
330
|
+
const c = COLORS.indexOf(String(color)) !== -1 ? color : "blue";
|
|
331
|
+
const cat = await api.addAnnotationCategoryAsync({ label, color: c });
|
|
332
|
+
created.push(label);
|
|
333
|
+
return cat.id;
|
|
334
|
+
}
|
|
335
|
+
function supports(node) {
|
|
336
|
+
return "annotations" in node;
|
|
337
|
+
}
|
|
338
|
+
async function annotate(p) {
|
|
339
|
+
const action = String(p.action || (p.label || p.properties ? "add" : "list"));
|
|
340
|
+
if (action === "list")
|
|
341
|
+
return list(p);
|
|
342
|
+
if (!p.nodeId)
|
|
343
|
+
throw codeError("`nodeId` is required to " + action + " annotations", "BAD_ARGS");
|
|
344
|
+
const node = await getNode(p.nodeId);
|
|
345
|
+
if (!supports(node))
|
|
346
|
+
throw codeError("A " + node.type + " node cannot have annotations", "BAD_ARGS");
|
|
347
|
+
if (action === "clear") {
|
|
348
|
+
const n = node.annotations.length;
|
|
349
|
+
node.annotations = [];
|
|
350
|
+
return { nodeId: node.id, removed: n };
|
|
351
|
+
}
|
|
352
|
+
if (action !== "add")
|
|
353
|
+
throw codeError('Unknown action "' + action + '" (add, list, clear)', "BAD_ARGS");
|
|
354
|
+
if (!p.label && !(p.properties && p.properties.length))
|
|
355
|
+
throw codeError("Give a label and/or properties", "BAD_ARGS");
|
|
356
|
+
const annotation = {};
|
|
357
|
+
if (p.label)
|
|
358
|
+
annotation.labelMarkdown = String(p.label);
|
|
359
|
+
if (Array.isArray(p.properties) && p.properties.length) {
|
|
360
|
+
annotation.properties = p.properties.map(function(t) {
|
|
361
|
+
return { type: String(t) };
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const createdCategories = [];
|
|
365
|
+
if (p.category) {
|
|
366
|
+
const id = await categoryId(String(p.category), p.color, createdCategories);
|
|
367
|
+
if (id)
|
|
368
|
+
annotation.categoryId = id;
|
|
369
|
+
}
|
|
370
|
+
const current = p.replace ? [] : node.annotations.slice();
|
|
371
|
+
try {
|
|
372
|
+
node.annotations = current.concat([annotation]);
|
|
373
|
+
} catch (e) {
|
|
374
|
+
throw codeError("Figma rejected the annotation: " + (e.message || e) + " (check the property names)", "BAD_ARGS");
|
|
375
|
+
}
|
|
376
|
+
const out = { nodeId: node.id, annotations: node.annotations.length };
|
|
377
|
+
if (createdCategories.length)
|
|
378
|
+
out.createdCategory = createdCategories[0];
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
async function list(p) {
|
|
382
|
+
const root = p.nodeId ? await getNode(p.nodeId) : p.pageId ? await getNode(p.pageId) : figma.currentPage;
|
|
383
|
+
if (root.type === "PAGE")
|
|
384
|
+
await root.loadAsync();
|
|
385
|
+
const nodes = (root.type === "PAGE" ? [] : [root]).concat("findAll" in root ? root.findAll(function(n) {
|
|
386
|
+
return supports(n) && n.annotations.length > 0;
|
|
387
|
+
}) : []);
|
|
388
|
+
const categories = {};
|
|
389
|
+
const api = figma.annotations;
|
|
390
|
+
if (api && typeof api.getAnnotationCategoriesAsync === "function") {
|
|
391
|
+
const cats = await api.getAnnotationCategoriesAsync();
|
|
392
|
+
for (let i = 0;i < cats.length; i++)
|
|
393
|
+
categories[cats[i].id] = cats[i].label;
|
|
394
|
+
}
|
|
395
|
+
const out = [];
|
|
396
|
+
for (let i = 0;i < nodes.length && out.length < 300; i++) {
|
|
397
|
+
const n = nodes[i];
|
|
398
|
+
if (!supports(n) || !n.annotations.length)
|
|
399
|
+
continue;
|
|
400
|
+
out.push({
|
|
401
|
+
nodeId: n.id,
|
|
402
|
+
name: n.name,
|
|
403
|
+
annotations: n.annotations.map(function(a) {
|
|
404
|
+
const item = {};
|
|
405
|
+
if (a.labelMarkdown || a.label)
|
|
406
|
+
item.label = a.labelMarkdown || a.label;
|
|
407
|
+
if (a.properties && a.properties.length) {
|
|
408
|
+
item.properties = a.properties.map(function(x) {
|
|
409
|
+
return x.type;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (a.categoryId)
|
|
413
|
+
item.category = categories[a.categoryId] || a.categoryId;
|
|
414
|
+
return item;
|
|
415
|
+
})
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return { count: out.length, nodes: out };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// plugin/lib/health.ts
|
|
422
|
+
var MAX_NODES = 20000;
|
|
423
|
+
var MAX_ISSUES = 100;
|
|
424
|
+
var WEIGHTS2 = { tokens: 25, contrast: 20, typography: 15, components: 15, styles: 15, naming: 10 };
|
|
425
|
+
var NUMBER_FIELDS = ["itemSpacing", "counterAxisSpacing", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"];
|
|
426
|
+
var RADIUS_FIELDS = ["topLeftRadius", "topRightRadius", "bottomRightRadius", "bottomLeftRadius"];
|
|
427
|
+
async function scopeNodes(p, wholeFile, requestId) {
|
|
428
|
+
let roots;
|
|
429
|
+
let label;
|
|
430
|
+
if (p.nodeId) {
|
|
431
|
+
const n = await getNode(p.nodeId);
|
|
432
|
+
roots = [n];
|
|
433
|
+
label = n.name;
|
|
434
|
+
} else if (wholeFile) {
|
|
435
|
+
await figma.loadAllPagesAsync();
|
|
436
|
+
roots = figma.root.children.slice();
|
|
437
|
+
label = "the whole file";
|
|
438
|
+
} else if (figma.currentPage.selection.length) {
|
|
439
|
+
roots = figma.currentPage.selection.slice();
|
|
440
|
+
label = "the selection";
|
|
441
|
+
} else {
|
|
442
|
+
roots = figma.currentPage.children.slice();
|
|
443
|
+
label = "page " + figma.currentPage.name;
|
|
444
|
+
}
|
|
445
|
+
const nodes = [];
|
|
446
|
+
let truncated = false;
|
|
447
|
+
const walk = function(n) {
|
|
448
|
+
if (nodes.length >= MAX_NODES) {
|
|
449
|
+
truncated = true;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (n.type !== "PAGE" && n.type !== "DOCUMENT")
|
|
453
|
+
nodes.push(n);
|
|
454
|
+
if (n.type === "INSTANCE" || !("children" in n))
|
|
455
|
+
return;
|
|
456
|
+
if (n.type === "PAGE" && n.name === "⟲ Bridge checkpoints")
|
|
457
|
+
return;
|
|
458
|
+
for (let i = 0;i < n.children.length; i++)
|
|
459
|
+
walk(n.children[i]);
|
|
460
|
+
};
|
|
461
|
+
for (let i = 0;i < roots.length; i++) {
|
|
462
|
+
checkCancelled(requestId);
|
|
463
|
+
walk(roots[i]);
|
|
464
|
+
}
|
|
465
|
+
return { nodes, truncated, label };
|
|
466
|
+
}
|
|
467
|
+
function visibleSolids(paints) {
|
|
468
|
+
if (!paints || paints === figma.mixed)
|
|
469
|
+
return [];
|
|
470
|
+
return paints.filter(function(pt) {
|
|
471
|
+
return pt.visible !== false && pt.type === "SOLID";
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function score(good, total) {
|
|
475
|
+
return total ? Math.round(good / total * 100) : 100;
|
|
476
|
+
}
|
|
477
|
+
function convention(name) {
|
|
478
|
+
const seg = name.split("/").pop() || name;
|
|
479
|
+
if (/^[a-z0-9]+(-[a-z0-9]+)+$/.test(seg))
|
|
480
|
+
return "kebab-case";
|
|
481
|
+
if (/^[a-z0-9]+(_[a-z0-9]+)+$/.test(seg))
|
|
482
|
+
return "snake_case";
|
|
483
|
+
if (/^[a-z]+[A-Z][A-Za-z0-9]*$/.test(seg))
|
|
484
|
+
return "camelCase";
|
|
485
|
+
if (/^[A-Z][a-z0-9]*( [A-Z0-9][a-z0-9]*)*$/.test(seg))
|
|
486
|
+
return "Title Case";
|
|
487
|
+
if (/^[a-z0-9]+$/.test(seg))
|
|
488
|
+
return "lowercase";
|
|
489
|
+
return "mixed";
|
|
490
|
+
}
|
|
491
|
+
function namingConsistency(names, what, issues) {
|
|
492
|
+
const counts = {};
|
|
493
|
+
const multiWord = [];
|
|
494
|
+
for (let i = 0;i < names.length; i++) {
|
|
495
|
+
const c = convention(names[i]);
|
|
496
|
+
if (c === "lowercase")
|
|
497
|
+
continue;
|
|
498
|
+
counts[c] = (counts[c] || 0) + 1;
|
|
499
|
+
multiWord.push(names[i]);
|
|
500
|
+
}
|
|
501
|
+
const keys = Object.keys(counts).sort(function(a, b) {
|
|
502
|
+
return counts[b] - counts[a];
|
|
503
|
+
});
|
|
504
|
+
const main = keys[0] || "lowercase";
|
|
505
|
+
const off = multiWord.filter(function(n) {
|
|
506
|
+
return convention(n) !== main;
|
|
507
|
+
});
|
|
508
|
+
for (let i = 0;i < Math.min(off.length, 5); i++)
|
|
509
|
+
issues.push({ category: "naming", message: what + ' "' + off[i] + '" is not ' + main + " like most others" });
|
|
510
|
+
return { score: score(multiWord.length - off.length, multiWord.length), convention: main };
|
|
511
|
+
}
|
|
512
|
+
function paintKey(paints) {
|
|
513
|
+
return JSON.stringify(paints.map(function(pt) {
|
|
514
|
+
if (pt.type === "SOLID")
|
|
515
|
+
return toHex(pt.color, pt.opacity) + (pt.boundVariables && pt.boundVariables.color ? "@" + pt.boundVariables.color.id : "");
|
|
516
|
+
if (pt.gradientStops)
|
|
517
|
+
return pt.type + pt.gradientStops.map(function(s) {
|
|
518
|
+
return toHex(s.color) + round(s.position);
|
|
519
|
+
});
|
|
520
|
+
return pt.type + (pt.imageHash || "");
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
async function designSystemHealth(p, requestId) {
|
|
524
|
+
const scope = await scopeNodes(p, !p.nodeId, requestId);
|
|
525
|
+
const nodes = scope.nodes;
|
|
526
|
+
const issues = [];
|
|
527
|
+
const add = function(category, message, n) {
|
|
528
|
+
if (issues.length < MAX_ISSUES * 3)
|
|
529
|
+
issues.push(n ? { category, message, nodeId: n.id, name: n.name } : { category, message });
|
|
530
|
+
};
|
|
531
|
+
const styles = await localStyles();
|
|
532
|
+
const vars = await localVariables();
|
|
533
|
+
const components = await localComponents();
|
|
534
|
+
const componentNames = {};
|
|
535
|
+
for (let i = 0;i < components.length; i++)
|
|
536
|
+
componentNames[components[i].name] = true;
|
|
537
|
+
const used = {};
|
|
538
|
+
const use = function(id) {
|
|
539
|
+
if (typeof id === "string" && id)
|
|
540
|
+
used[id] = (used[id] || 0) + 1;
|
|
541
|
+
};
|
|
542
|
+
let colorTotal = 0;
|
|
543
|
+
let colorBound = 0;
|
|
544
|
+
let numberTotal = 0;
|
|
545
|
+
let numberBound = 0;
|
|
546
|
+
let textTotal = 0;
|
|
547
|
+
let textStyled = 0;
|
|
548
|
+
let contrastTotal = 0;
|
|
549
|
+
let contrastOk = 0;
|
|
550
|
+
let layerTotal = 0;
|
|
551
|
+
let layerDefault = 0;
|
|
552
|
+
let instanceCount = 0;
|
|
553
|
+
let overridden = 0;
|
|
554
|
+
let detached = 0;
|
|
555
|
+
const pageBg = pageBackground();
|
|
556
|
+
for (let i = 0;i < nodes.length; i++) {
|
|
557
|
+
if (i % 500 === 0)
|
|
558
|
+
checkCancelled(requestId);
|
|
559
|
+
const n = nodes[i];
|
|
560
|
+
const bound = n.boundVariables || {};
|
|
561
|
+
const keys = Object.keys(bound);
|
|
562
|
+
for (let k = 0;k < keys.length; k++) {
|
|
563
|
+
const b = bound[keys[k]];
|
|
564
|
+
if (Array.isArray(b))
|
|
565
|
+
for (let j = 0;j < b.length; j++)
|
|
566
|
+
use(b[j] && b[j].id);
|
|
567
|
+
else if (b && b.id)
|
|
568
|
+
use(b.id);
|
|
569
|
+
}
|
|
570
|
+
["fillStyleId", "strokeStyleId", "effectStyleId", "textStyleId"].forEach(function(f) {
|
|
571
|
+
if (f in n)
|
|
572
|
+
use(n[f]);
|
|
573
|
+
});
|
|
574
|
+
if (n.type === "INSTANCE") {
|
|
575
|
+
instanceCount++;
|
|
576
|
+
const fields = (n.overrides || []).reduce(function(sum, o) {
|
|
577
|
+
return sum + o.overriddenFields.length;
|
|
578
|
+
}, 0);
|
|
579
|
+
if (fields > 12) {
|
|
580
|
+
overridden++;
|
|
581
|
+
add("components", "Instance with " + fields + " overridden properties: consider a variant", n);
|
|
582
|
+
}
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if ((n.type === "FRAME" || n.type === "GROUP") && (componentNames[n.name] || /^[^=,]+=[^=,]+(, [^=,]+=[^=,]+)*$/.test(n.name))) {
|
|
586
|
+
detached++;
|
|
587
|
+
add("components", 'Frame named like a component ("' + n.name + '"): probably a detached instance', n);
|
|
588
|
+
}
|
|
589
|
+
const fillStyled = typeof n.fillStyleId === "string" && n.fillStyleId !== "";
|
|
590
|
+
const solids = fillStyled ? [] : visibleSolids(n.fills);
|
|
591
|
+
for (let k = 0;k < solids.length; k++) {
|
|
592
|
+
colorTotal++;
|
|
593
|
+
if (solids[k].boundVariables && solids[k].boundVariables.color)
|
|
594
|
+
colorBound++;
|
|
595
|
+
else
|
|
596
|
+
add("tokens", "Hard-coded color " + toHex(solids[k].color, solids[k].opacity), n);
|
|
597
|
+
}
|
|
598
|
+
const strokeStyled = typeof n.strokeStyleId === "string" && n.strokeStyleId !== "";
|
|
599
|
+
const strokes = strokeStyled ? [] : visibleSolids(n.strokes);
|
|
600
|
+
for (let k = 0;k < strokes.length; k++) {
|
|
601
|
+
colorTotal++;
|
|
602
|
+
if (strokes[k].boundVariables && strokes[k].boundVariables.color)
|
|
603
|
+
colorBound++;
|
|
604
|
+
}
|
|
605
|
+
if (fillStyled) {
|
|
606
|
+
colorTotal++;
|
|
607
|
+
colorBound++;
|
|
608
|
+
}
|
|
609
|
+
if (n.layoutMode && n.layoutMode !== "NONE") {
|
|
610
|
+
for (let k = 0;k < NUMBER_FIELDS.length; k++) {
|
|
611
|
+
const f = NUMBER_FIELDS[k];
|
|
612
|
+
if (f === "counterAxisSpacing" && n.layoutWrap !== "WRAP")
|
|
613
|
+
continue;
|
|
614
|
+
if (!n[f])
|
|
615
|
+
continue;
|
|
616
|
+
numberTotal++;
|
|
617
|
+
if (bound[f])
|
|
618
|
+
numberBound++;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if ("topLeftRadius" in n && n.type !== "TEXT") {
|
|
622
|
+
for (let k = 0;k < RADIUS_FIELDS.length; k++) {
|
|
623
|
+
if (!n[RADIUS_FIELDS[k]])
|
|
624
|
+
continue;
|
|
625
|
+
numberTotal++;
|
|
626
|
+
if (bound[RADIUS_FIELDS[k]])
|
|
627
|
+
numberBound++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (n.type === "TEXT") {
|
|
631
|
+
textTotal++;
|
|
632
|
+
if (typeof n.textStyleId === "string" && n.textStyleId)
|
|
633
|
+
textStyled++;
|
|
634
|
+
else if (n.textStyleId === figma.mixed)
|
|
635
|
+
textStyled += 0.5;
|
|
636
|
+
else
|
|
637
|
+
add("typography", "Text without a text style (" + (n.fontName !== figma.mixed ? n.fontName.family + " " + n.fontName.style : "mixed fonts") + ", " + (n.fontSize !== figma.mixed ? round(n.fontSize) : "mixed") + " px)", n);
|
|
638
|
+
const c = n.visible ? textContrast(n, pageBg) : null;
|
|
639
|
+
if (c) {
|
|
640
|
+
contrastTotal++;
|
|
641
|
+
if (c.ratio >= c.min)
|
|
642
|
+
contrastOk++;
|
|
643
|
+
else
|
|
644
|
+
add("contrast", "Contrast " + round(c.ratio) + ":1 is below " + c.min + ":1", n);
|
|
645
|
+
}
|
|
646
|
+
} else if (n.type !== "COMPONENT" && n.type !== "COMPONENT_SET" && n.type !== "SECTION") {
|
|
647
|
+
layerTotal++;
|
|
648
|
+
if (DEFAULT_NAME.test(n.name)) {
|
|
649
|
+
layerDefault++;
|
|
650
|
+
add("naming", "Default layer name", n);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const styleTotal = styles.paint.length + styles.text.length + styles.effect.length;
|
|
655
|
+
let styleProblems = 0;
|
|
656
|
+
const seen = {};
|
|
657
|
+
const dup = function(key, name, kind) {
|
|
658
|
+
if (seen[key]) {
|
|
659
|
+
styleProblems++;
|
|
660
|
+
add("styles", kind + ' style "' + name + '" duplicates "' + seen[key] + '"');
|
|
661
|
+
} else
|
|
662
|
+
seen[key] = name;
|
|
663
|
+
};
|
|
664
|
+
for (let i = 0;i < styles.paint.length; i++)
|
|
665
|
+
dup("p" + paintKey(styles.paint[i].paints), styles.paint[i].name, "Color");
|
|
666
|
+
for (let i = 0;i < styles.text.length; i++) {
|
|
667
|
+
const s = styles.text[i];
|
|
668
|
+
dup("t" + JSON.stringify([s.fontName, s.fontSize, s.lineHeight, s.letterSpacing, s.textCase, s.textDecoration]), s.name, "Text");
|
|
669
|
+
}
|
|
670
|
+
for (let i = 0;i < styles.effect.length; i++)
|
|
671
|
+
dup("e" + JSON.stringify(styles.effect[i].effects), styles.effect[i].name, "Effect");
|
|
672
|
+
const wholeFile = !p.nodeId;
|
|
673
|
+
if (wholeFile) {
|
|
674
|
+
const all = styles.paint.concat(styles.text, styles.effect);
|
|
675
|
+
for (let i = 0;i < all.length; i++) {
|
|
676
|
+
if (used[all[i].id])
|
|
677
|
+
continue;
|
|
678
|
+
styleProblems++;
|
|
679
|
+
add("styles", 'Style "' + all[i].name + '" is not used in this file');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
for (let i = 0;i < vars.vars.length; i++) {
|
|
683
|
+
const v = vars.vars[i];
|
|
684
|
+
const modes = Object.keys(v.valuesByMode);
|
|
685
|
+
for (let k = 0;k < modes.length; k++) {
|
|
686
|
+
const val = v.valuesByMode[modes[k]];
|
|
687
|
+
if (val && val.type === "VARIABLE_ALIAS")
|
|
688
|
+
use(val.id);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (let i = 0;i < styles.paint.length; i++) {
|
|
692
|
+
const ps = styles.paint[i].paints;
|
|
693
|
+
for (let k = 0;k < ps.length; k++)
|
|
694
|
+
if (ps[k].boundVariables && ps[k].boundVariables.color)
|
|
695
|
+
use(ps[k].boundVariables.color.id);
|
|
696
|
+
}
|
|
697
|
+
let unusedVars = 0;
|
|
698
|
+
if (wholeFile) {
|
|
699
|
+
for (let i = 0;i < vars.vars.length; i++) {
|
|
700
|
+
if (used[vars.vars[i].id])
|
|
701
|
+
continue;
|
|
702
|
+
unusedVars++;
|
|
703
|
+
if (unusedVars <= 10)
|
|
704
|
+
add("styles", 'Variable "' + vars.vars[i].name + '" is not used in this file');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
let unusedComponents = 0;
|
|
708
|
+
if (wholeFile) {
|
|
709
|
+
for (let i = 0;i < Math.min(components.length, 300); i++) {
|
|
710
|
+
const c = components[i];
|
|
711
|
+
const parts = c.type === "COMPONENT_SET" ? c.children : [c];
|
|
712
|
+
let count = 0;
|
|
713
|
+
for (let k = 0;k < parts.length && !count; k++)
|
|
714
|
+
count += (await parts[k].getInstancesAsync()).length;
|
|
715
|
+
if (!count) {
|
|
716
|
+
unusedComponents++;
|
|
717
|
+
add("styles", 'Component "' + c.name + '" has no instances');
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const varNames = vars.vars.map(function(v) {
|
|
722
|
+
return v.name;
|
|
723
|
+
});
|
|
724
|
+
const styleNames = styles.paint.concat(styles.text, styles.effect).map(function(s) {
|
|
725
|
+
return s.name;
|
|
726
|
+
});
|
|
727
|
+
const naming = namingConsistency(varNames.concat(styleNames), "Token", issues);
|
|
728
|
+
const categories = {
|
|
729
|
+
tokens: { score: score(colorBound + numberBound, colorTotal + numberTotal), colors: { bound: colorBound, total: colorTotal }, numbers: { bound: numberBound, total: numberTotal } },
|
|
730
|
+
contrast: { score: score(contrastOk, contrastTotal), passing: contrastOk, total: contrastTotal },
|
|
731
|
+
typography: { score: score(textStyled, textTotal), styled: Math.floor(textStyled), total: textTotal },
|
|
732
|
+
components: {
|
|
733
|
+
score: Math.max(0, 100 - Math.round((detached + overridden * 0.5) / Math.max(1, instanceCount + detached) * 100)),
|
|
734
|
+
instances: instanceCount,
|
|
735
|
+
detachedSuspects: detached,
|
|
736
|
+
heavilyOverridden: overridden,
|
|
737
|
+
unused: wholeFile ? unusedComponents : undefined
|
|
738
|
+
},
|
|
739
|
+
styles: {
|
|
740
|
+
score: score(styleTotal + vars.vars.length + components.length - styleProblems - unusedVars - unusedComponents, styleTotal + vars.vars.length + components.length),
|
|
741
|
+
styles: styleTotal,
|
|
742
|
+
variables: vars.vars.length,
|
|
743
|
+
problems: styleProblems + unusedVars + unusedComponents
|
|
744
|
+
},
|
|
745
|
+
naming: {
|
|
746
|
+
score: Math.round((score(layerTotal - layerDefault, layerTotal) + naming.score) / 2),
|
|
747
|
+
defaultLayerNames: layerDefault,
|
|
748
|
+
layers: layerTotal,
|
|
749
|
+
tokenConvention: naming.convention
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
let total = 0;
|
|
753
|
+
let weight = 0;
|
|
754
|
+
const names = Object.keys(WEIGHTS2);
|
|
755
|
+
for (let i = 0;i < names.length; i++) {
|
|
756
|
+
total += categories[names[i]].score * WEIGHTS2[names[i]];
|
|
757
|
+
weight += WEIGHTS2[names[i]];
|
|
758
|
+
}
|
|
759
|
+
const picked = [];
|
|
760
|
+
for (let pass = 0;picked.length < MAX_ISSUES && pass < MAX_ISSUES; pass++) {
|
|
761
|
+
let added = false;
|
|
762
|
+
for (let c = 0;c < names.length && picked.length < MAX_ISSUES; c++) {
|
|
763
|
+
const list = issues.filter(function(x) {
|
|
764
|
+
return x.category === names[c];
|
|
765
|
+
});
|
|
766
|
+
if (list[pass]) {
|
|
767
|
+
picked.push(list[pass]);
|
|
768
|
+
added = true;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!added)
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
scope: scope.label,
|
|
776
|
+
score: Math.round(total / weight),
|
|
777
|
+
categories,
|
|
778
|
+
issues: picked,
|
|
779
|
+
totalIssues: issues.length,
|
|
780
|
+
nodesChecked: nodes.length,
|
|
781
|
+
truncated: scope.truncated,
|
|
782
|
+
hint: "audit {fix:true} binds raw colors and numbers to matching variables, applies matching text styles and renames default layer names."
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function resolved(v, byId, collections, depth) {
|
|
786
|
+
const col = collections[v.variableCollectionId];
|
|
787
|
+
if (!col)
|
|
788
|
+
return null;
|
|
789
|
+
const raw = v.valuesByMode[col.defaultModeId];
|
|
790
|
+
if (raw && raw.type === "VARIABLE_ALIAS") {
|
|
791
|
+
const t = byId[raw.id];
|
|
792
|
+
return t && (depth || 0) < 10 ? resolved(t, byId, collections, (depth || 0) + 1) : null;
|
|
793
|
+
}
|
|
794
|
+
return raw;
|
|
795
|
+
}
|
|
796
|
+
function scoped(v, wanted) {
|
|
797
|
+
const s = v.scopes || [];
|
|
798
|
+
if (!s.length || s.indexOf("ALL_SCOPES") !== -1)
|
|
799
|
+
return true;
|
|
800
|
+
for (let i = 0;i < wanted.length; i++)
|
|
801
|
+
if (s.indexOf(wanted[i]) !== -1)
|
|
802
|
+
return true;
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
var FILL_SCOPES = { TEXT: ["ALL_FILLS", "TEXT_FILL"], FRAME: ["ALL_FILLS", "FRAME_FILL"], COMPONENT: ["ALL_FILLS", "FRAME_FILL"], SECTION: ["ALL_FILLS", "FRAME_FILL"] };
|
|
806
|
+
async function autoFix(p, requestId) {
|
|
807
|
+
const all = ["colors", "numbers", "textStyles", "names"];
|
|
808
|
+
const fixes = Array.isArray(p.fixes) && p.fixes.length ? p.fixes : all;
|
|
809
|
+
for (let i = 0;i < fixes.length; i++)
|
|
810
|
+
if (all.indexOf(fixes[i]) === -1)
|
|
811
|
+
throw codeError('Unknown fix "' + fixes[i] + '" (use ' + all.join(", ") + ")", "BAD_ARGS");
|
|
812
|
+
const scope = await scopeNodes(p, false, requestId);
|
|
813
|
+
const vars = await localVariables();
|
|
814
|
+
const byId = {};
|
|
815
|
+
const collections = {};
|
|
816
|
+
for (let i = 0;i < vars.vars.length; i++)
|
|
817
|
+
byId[vars.vars[i].id] = vars.vars[i];
|
|
818
|
+
for (let i = 0;i < vars.collections.length; i++)
|
|
819
|
+
collections[vars.collections[i].id] = vars.collections[i];
|
|
820
|
+
const colorVars = [];
|
|
821
|
+
const numberVars = [];
|
|
822
|
+
for (let i = 0;i < vars.vars.length; i++) {
|
|
823
|
+
const v = vars.vars[i];
|
|
824
|
+
const value = resolved(v, byId, collections);
|
|
825
|
+
if (v.resolvedType === "COLOR" && value && typeof value === "object")
|
|
826
|
+
colorVars.push({ v, rgba: value });
|
|
827
|
+
if (v.resolvedType === "FLOAT" && typeof value === "number")
|
|
828
|
+
numberVars.push({ v, num: value });
|
|
829
|
+
}
|
|
830
|
+
const changes = [];
|
|
831
|
+
const counts = { colors: 0, numbers: 0, textStyles: 0, names: 0 };
|
|
832
|
+
const record = function(n, fix, from, to) {
|
|
833
|
+
counts[fix]++;
|
|
834
|
+
if (changes.length < 200)
|
|
835
|
+
changes.push({ nodeId: n.id, name: n.name, fix, from, to });
|
|
836
|
+
};
|
|
837
|
+
const matchColor = function(n, paint, scopes) {
|
|
838
|
+
const alpha = paint.opacity === undefined ? 1 : paint.opacity;
|
|
839
|
+
const ranked = colorVars.filter(function(c) {
|
|
840
|
+
return scoped(c.v, scopes) && Math.abs((c.rgba.a === undefined ? 1 : c.rgba.a) - alpha) < 0.02;
|
|
841
|
+
}).map(function(c) {
|
|
842
|
+
return { c, d: deltaE(c.rgba, paint.color) };
|
|
843
|
+
}).filter(function(x) {
|
|
844
|
+
return x.d < 2;
|
|
845
|
+
}).sort(function(a, b) {
|
|
846
|
+
return a.d - b.d;
|
|
847
|
+
});
|
|
848
|
+
for (let i = 0;i < ranked.length; i++) {
|
|
849
|
+
try {
|
|
850
|
+
const r = ranked[i].c.v.resolveForConsumer(n).value;
|
|
851
|
+
if (r && typeof r === "object" && deltaE(r, paint.color) < 2)
|
|
852
|
+
return ranked[i].c.v;
|
|
853
|
+
} catch (e) {}
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
856
|
+
};
|
|
857
|
+
const matchNumber = function(value, scopes, hint) {
|
|
858
|
+
const hits = numberVars.filter(function(c) {
|
|
859
|
+
return Math.abs(c.num - value) < 0.01 && scoped(c.v, scopes);
|
|
860
|
+
});
|
|
861
|
+
hits.sort(function(a, b) {
|
|
862
|
+
return (hint.test(b.v.name) ? 1 : 0) - (hint.test(a.v.name) ? 1 : 0);
|
|
863
|
+
});
|
|
864
|
+
return hits.length ? hits[0].v : null;
|
|
865
|
+
};
|
|
866
|
+
const textStyles = fixes.indexOf("textStyles") !== -1 ? (await localStyles()).text : [];
|
|
867
|
+
for (let i = 0;i < scope.nodes.length; i++) {
|
|
868
|
+
if (i % 200 === 0)
|
|
869
|
+
checkCancelled(requestId);
|
|
870
|
+
const n = scope.nodes[i];
|
|
871
|
+
if (n.type === "INSTANCE" || n.locked)
|
|
872
|
+
continue;
|
|
873
|
+
const bound = n.boundVariables || {};
|
|
874
|
+
if (fixes.indexOf("colors") !== -1) {
|
|
875
|
+
const fields = ["fills", "strokes"];
|
|
876
|
+
for (let f = 0;f < fields.length; f++) {
|
|
877
|
+
const field = fields[f];
|
|
878
|
+
if (!(field in n) || n[field] === figma.mixed)
|
|
879
|
+
continue;
|
|
880
|
+
if (field === "fills" && n.fillStyleId || field === "strokes" && n.strokeStyleId)
|
|
881
|
+
continue;
|
|
882
|
+
const paints = n[field].slice();
|
|
883
|
+
let changed = false;
|
|
884
|
+
for (let k = 0;k < paints.length; k++) {
|
|
885
|
+
const pt = paints[k];
|
|
886
|
+
if (pt.type !== "SOLID" || pt.visible === false || pt.boundVariables && pt.boundVariables.color)
|
|
887
|
+
continue;
|
|
888
|
+
const v = matchColor(n, pt, field === "strokes" ? ["STROKE_COLOR"] : FILL_SCOPES[n.type] || ["ALL_FILLS", "SHAPE_FILL"]);
|
|
889
|
+
if (!v)
|
|
890
|
+
continue;
|
|
891
|
+
paints[k] = figma.variables.setBoundVariableForPaint(pt, "color", v);
|
|
892
|
+
changed = true;
|
|
893
|
+
record(n, "colors", field + " " + toHex(pt.color, pt.opacity), "var:" + v.name);
|
|
894
|
+
}
|
|
895
|
+
if (changed)
|
|
896
|
+
n[field] = paints;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (fixes.indexOf("numbers") !== -1) {
|
|
900
|
+
const fields = [];
|
|
901
|
+
if (n.layoutMode && n.layoutMode !== "NONE") {
|
|
902
|
+
for (let k = 0;k < NUMBER_FIELDS.length; k++)
|
|
903
|
+
if (NUMBER_FIELDS[k] !== "counterAxisSpacing" || n.layoutWrap === "WRAP")
|
|
904
|
+
fields.push(NUMBER_FIELDS[k]);
|
|
905
|
+
}
|
|
906
|
+
if ("topLeftRadius" in n && n.type !== "TEXT")
|
|
907
|
+
for (let k = 0;k < RADIUS_FIELDS.length; k++)
|
|
908
|
+
fields.push(RADIUS_FIELDS[k]);
|
|
909
|
+
for (let k = 0;k < fields.length; k++) {
|
|
910
|
+
const f = fields[k];
|
|
911
|
+
const value = n[f];
|
|
912
|
+
if (typeof value !== "number" || !value || bound[f])
|
|
913
|
+
continue;
|
|
914
|
+
const radius = f.indexOf("Radius") !== -1;
|
|
915
|
+
const v = matchNumber(value, radius ? ["CORNER_RADIUS"] : ["GAP"], radius ? /radius|corner|round/i : /spac|gap|pad/i);
|
|
916
|
+
if (!v)
|
|
917
|
+
continue;
|
|
918
|
+
try {
|
|
919
|
+
n.setBoundVariable(f, v);
|
|
920
|
+
record(n, "numbers", f + " " + round(value), "var:" + v.name);
|
|
921
|
+
} catch (e) {}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (n.type === "TEXT" && textStyles.length && !n.textStyleId && n.fontName !== figma.mixed && n.fontSize !== figma.mixed && n.lineHeight !== figma.mixed && n.letterSpacing !== figma.mixed) {
|
|
925
|
+
for (let k = 0;k < textStyles.length; k++) {
|
|
926
|
+
const s = textStyles[k];
|
|
927
|
+
if (s.fontName.family === n.fontName.family && s.fontName.style === n.fontName.style && Math.abs(s.fontSize - n.fontSize) < 0.01 && JSON.stringify(s.lineHeight) === JSON.stringify(n.lineHeight) && s.letterSpacing.unit === n.letterSpacing.unit && Math.abs(s.letterSpacing.value - n.letterSpacing.value) < 0.01) {
|
|
928
|
+
try {
|
|
929
|
+
await loadFont(s.fontName);
|
|
930
|
+
await n.setTextStyleIdAsync(s.id);
|
|
931
|
+
record(n, "textStyles", n.fontName.family + " " + n.fontName.style + " " + round(n.fontSize), "style:" + s.name);
|
|
932
|
+
} catch (e) {}
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (fixes.indexOf("names") !== -1 && n.type !== "TEXT" && DEFAULT_NAME.test(n.name)) {
|
|
938
|
+
const name = nameFromContent(n);
|
|
939
|
+
if (name && name !== n.name) {
|
|
940
|
+
const from = n.name;
|
|
941
|
+
n.name = name;
|
|
942
|
+
record(n, "names", from, name);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return { scope: scope.label, fixed: counts, changes, truncated: scope.truncated || changes.length >= 200 };
|
|
947
|
+
}
|
|
948
|
+
function firstText(n, depth) {
|
|
949
|
+
if (n.type === "TEXT")
|
|
950
|
+
return n.characters;
|
|
951
|
+
if (depth > 3 || !("children" in n))
|
|
952
|
+
return "";
|
|
953
|
+
for (let i = 0;i < n.children.length; i++) {
|
|
954
|
+
if (n.children[i].visible === false)
|
|
955
|
+
continue;
|
|
956
|
+
const t = firstText(n.children[i], depth + 1);
|
|
957
|
+
if (t.trim())
|
|
958
|
+
return t;
|
|
959
|
+
}
|
|
960
|
+
return "";
|
|
961
|
+
}
|
|
962
|
+
function hasImage(n) {
|
|
963
|
+
return "fills" in n && n.fills !== figma.mixed && n.fills.some(function(f) {
|
|
964
|
+
return f.type === "IMAGE" && f.visible !== false;
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
function vectorOnly(n) {
|
|
968
|
+
if (n.type === "VECTOR" || n.type === "BOOLEAN_OPERATION" || n.type === "STAR" || n.type === "POLYGON")
|
|
969
|
+
return true;
|
|
970
|
+
if (!("children" in n) || !n.children.length || n.type === "TEXT")
|
|
971
|
+
return false;
|
|
972
|
+
for (let i = 0;i < n.children.length; i++)
|
|
973
|
+
if (!vectorOnly(n.children[i]))
|
|
974
|
+
return false;
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
function nameFromContent(n) {
|
|
978
|
+
if (hasImage(n))
|
|
979
|
+
return "Image";
|
|
980
|
+
if (n.type === "VECTOR" || (n.type === "GROUP" || n.type === "FRAME") && vectorOnly(n))
|
|
981
|
+
return "Icon";
|
|
982
|
+
const text = firstText(n, 0).split(`
|
|
983
|
+
`)[0].trim();
|
|
984
|
+
if (text)
|
|
985
|
+
return text.length > 32 ? text.slice(0, 31) + "…" : text;
|
|
986
|
+
if (n.layoutMode === "HORIZONTAL")
|
|
987
|
+
return "Row";
|
|
988
|
+
if (n.layoutMode === "VERTICAL")
|
|
989
|
+
return "Column";
|
|
990
|
+
if (n.layoutMode === "GRID")
|
|
991
|
+
return "Grid";
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// plugin/lib/audit.ts
|
|
996
|
+
var MAX_NODES2 = 5000;
|
|
997
|
+
var MAX_ISSUES2 = 120;
|
|
998
|
+
var DEFAULT_NAME = /^(Frame|Rectangle|Ellipse|Group|Vector|Text|Line|Polygon|Star|Component|Instance|Section|Image)( \d+)?$/;
|
|
999
|
+
async function audit(p, _timeoutMs, requestId) {
|
|
1000
|
+
if (p.fix === true || Array.isArray(p.fixes) && p.fixes.length) {
|
|
1001
|
+
const fixed = await autoFix(p, requestId);
|
|
1002
|
+
invalidateCaches();
|
|
1003
|
+
const after = p.scope === "design-system" ? await designSystemHealth(p, requestId) : await lint(p);
|
|
1004
|
+
return Object.assign({ fixed: fixed.fixed, changes: fixed.changes, fixScope: fixed.scope }, after);
|
|
1005
|
+
}
|
|
1006
|
+
if (p.scope === "design-system")
|
|
1007
|
+
return designSystemHealth(p, requestId);
|
|
1008
|
+
return lint(p);
|
|
1009
|
+
}
|
|
1010
|
+
async function lint(p) {
|
|
1011
|
+
let roots;
|
|
1012
|
+
if (p.nodeId)
|
|
1013
|
+
roots = [await getNode(p.nodeId)];
|
|
1014
|
+
else if (figma.currentPage.selection.length)
|
|
1015
|
+
roots = figma.currentPage.selection.slice();
|
|
1016
|
+
else
|
|
1017
|
+
roots = figma.currentPage.children.slice();
|
|
1018
|
+
const only = Array.isArray(p.rules) && p.rules.length ? p.rules : null;
|
|
1019
|
+
const issues = [];
|
|
1020
|
+
const counts = {};
|
|
1021
|
+
const families = {};
|
|
1022
|
+
const sizes = {};
|
|
1023
|
+
let visited = 0;
|
|
1024
|
+
const pageBg = pageBackground();
|
|
1025
|
+
const report = function(rule, severity, n, message) {
|
|
1026
|
+
if (only && only.indexOf(rule) === -1)
|
|
1027
|
+
return;
|
|
1028
|
+
counts[rule] = (counts[rule] || 0) + 1;
|
|
1029
|
+
if (issues.length < MAX_ISSUES2)
|
|
1030
|
+
issues.push({ rule, severity, nodeId: n.id, name: n.name, message });
|
|
1031
|
+
};
|
|
1032
|
+
const visit = function(n, clipBox) {
|
|
1033
|
+
if (visited >= MAX_NODES2)
|
|
1034
|
+
return;
|
|
1035
|
+
visited++;
|
|
1036
|
+
if (n.visible === false)
|
|
1037
|
+
return;
|
|
1038
|
+
const box = n.absoluteBoundingBox || null;
|
|
1039
|
+
if (DEFAULT_NAME.test(n.name) && n.type !== "TEXT")
|
|
1040
|
+
report("default-name", "info", n, "Layer has a default name");
|
|
1041
|
+
if (clipBox && box && !inside(box, clipBox)) {
|
|
1042
|
+
const fully = box.x >= clipBox.x + clipBox.width || box.y >= clipBox.y + clipBox.height || box.x + box.width <= clipBox.x || box.y + box.height <= clipBox.y;
|
|
1043
|
+
report("clipped", fully ? "warning" : "info", n, fully ? "Completely hidden by a clipping parent" : "Partly cut off by a clipping parent");
|
|
1044
|
+
}
|
|
1045
|
+
if (n.type === "TEXT")
|
|
1046
|
+
checkText(n, report, families, sizes, pageBg);
|
|
1047
|
+
if (isAutoLayout(n)) {
|
|
1048
|
+
const vals = [n.itemSpacing, n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft];
|
|
1049
|
+
if (vals.some(function(v) {
|
|
1050
|
+
return typeof v === "number" && v % 4 !== 0;
|
|
1051
|
+
})) {
|
|
1052
|
+
report("off-grid", "info", n, "Spacing/padding not on a 4 px grid (" + vals.map(round).join(",") + ")");
|
|
1053
|
+
}
|
|
1054
|
+
} else if ((n.type === "FRAME" || n.type === "COMPONENT") && n.parent && n.parent.type !== "PAGE" && n.parent.type !== "SECTION") {
|
|
1055
|
+
const visibleKids = (n.children || []).filter(function(c) {
|
|
1056
|
+
return c.visible;
|
|
1057
|
+
});
|
|
1058
|
+
if (visibleKids.length >= 2)
|
|
1059
|
+
report("no-auto-layout", "info", n, "Frame with " + visibleKids.length + " children but no auto-layout");
|
|
1060
|
+
}
|
|
1061
|
+
if ((n.type === "FRAME" || n.type === "GROUP") && n.children && !n.children.length) {
|
|
1062
|
+
const hasFill = n.fills && n.fills !== figma.mixed && n.fills.length;
|
|
1063
|
+
if (!hasFill)
|
|
1064
|
+
report("empty", "info", n, "Empty layer with no fill");
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof n.width === "number" && n.type !== "TEXT" && n.type !== "VECTOR" && n.type !== "LINE" && n.type !== "BOOLEAN_OPERATION" && n.type !== "STAR" && n.type !== "POLYGON") {
|
|
1067
|
+
if (Math.abs(n.width - Math.round(n.width)) > 0.01 || Math.abs(n.height - Math.round(n.height)) > 0.01) {
|
|
1068
|
+
report("fractional", "info", n, "Fractional size " + round(n.width) + "×" + round(n.height));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const kids = n.children || [];
|
|
1072
|
+
const nextClip = n.clipsContent && box ? box : clipBox;
|
|
1073
|
+
for (let i = 0;i < kids.length; i++)
|
|
1074
|
+
visit(kids[i], nextClip);
|
|
1075
|
+
};
|
|
1076
|
+
for (let i = 0;i < roots.length; i++)
|
|
1077
|
+
visit(roots[i], null);
|
|
1078
|
+
const familyList = Object.keys(families);
|
|
1079
|
+
const sizeList = Object.keys(sizes).map(Number).sort(function(a, b) {
|
|
1080
|
+
return a - b;
|
|
1081
|
+
});
|
|
1082
|
+
if (familyList.length > 3)
|
|
1083
|
+
report("font-sprawl", "warning", roots[0], familyList.length + " font families: " + familyList.join(", "));
|
|
1084
|
+
if (sizeList.length > 8)
|
|
1085
|
+
report("type-scale", "warning", roots[0], sizeList.length + " different font sizes: " + sizeList.join(", "));
|
|
1086
|
+
const summary = { error: 0, warning: 0, info: 0 };
|
|
1087
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
1088
|
+
issues.sort(function(a, b) {
|
|
1089
|
+
return order[a.severity] - order[b.severity];
|
|
1090
|
+
});
|
|
1091
|
+
const bySeverity = {};
|
|
1092
|
+
for (let i = 0;i < issues.length; i++)
|
|
1093
|
+
bySeverity[issues[i].severity] = (bySeverity[issues[i].severity] || 0) + 1;
|
|
1094
|
+
summary.error = bySeverity.error || 0;
|
|
1095
|
+
summary.warning = bySeverity.warning || 0;
|
|
1096
|
+
summary.info = bySeverity.info || 0;
|
|
1097
|
+
return {
|
|
1098
|
+
summary,
|
|
1099
|
+
countsByRule: counts,
|
|
1100
|
+
nodesChecked: visited,
|
|
1101
|
+
fonts: familyList,
|
|
1102
|
+
fontSizes: sizeList,
|
|
1103
|
+
issues,
|
|
1104
|
+
truncated: visited >= MAX_NODES2 || issues.length >= MAX_ISSUES2
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function checkText(n, report, families, sizes, pageBg) {
|
|
1108
|
+
if (n.hasMissingFont)
|
|
1109
|
+
report("missing-font", "error", n, "Uses a font that is not installed");
|
|
1110
|
+
if (n.fontName !== figma.mixed)
|
|
1111
|
+
families[n.fontName.family] = true;
|
|
1112
|
+
if (n.fontSize !== figma.mixed) {
|
|
1113
|
+
sizes[round(n.fontSize)] = true;
|
|
1114
|
+
if (n.fontSize < 10)
|
|
1115
|
+
report("tiny-text", "warning", n, "Font size " + n.fontSize + " px is hard to read");
|
|
1116
|
+
}
|
|
1117
|
+
if (n.textTruncation === "DISABLED" && n.textAutoResize === "NONE" && n.characters.length > 0 && n.fontSize !== figma.mixed) {
|
|
1118
|
+
const lh = n.lineHeight !== figma.mixed && n.lineHeight.unit === "PIXELS" ? n.lineHeight.value : n.fontSize * 1.2;
|
|
1119
|
+
if (n.height + 0.5 < lh)
|
|
1120
|
+
report("text-overflow", "warning", n, "Fixed text box is shorter than one line");
|
|
1121
|
+
}
|
|
1122
|
+
const parent = n.parent;
|
|
1123
|
+
if (parent && parent.absoluteBoundingBox && n.absoluteBoundingBox && parent.type !== "PAGE" && parent.type !== "SECTION") {
|
|
1124
|
+
const pb = parent.absoluteBoundingBox;
|
|
1125
|
+
const tb = n.absoluteBoundingBox;
|
|
1126
|
+
if (tb.x + tb.width > pb.x + pb.width + 1 || tb.x < pb.x - 1)
|
|
1127
|
+
report("text-overflow", "warning", n, "Text extends outside its container");
|
|
1128
|
+
}
|
|
1129
|
+
const c = textContrast(n, pageBg);
|
|
1130
|
+
if (c && c.ratio < c.min) {
|
|
1131
|
+
report("contrast", c.ratio < c.min - 1.5 ? "error" : "warning", n, "Contrast " + round(c.ratio) + ":1 is below " + c.min + ":1 (WCAG AA" + (c.large ? ", large text" : "") + ")");
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function textContrast(n, pageBg) {
|
|
1135
|
+
const fg = solidOf(n.fills);
|
|
1136
|
+
if (!fg || n.fontSize === figma.mixed)
|
|
1137
|
+
return null;
|
|
1138
|
+
const bg = backgroundOf(n, pageBg);
|
|
1139
|
+
if (!bg)
|
|
1140
|
+
return null;
|
|
1141
|
+
const bold = n.fontName !== figma.mixed && /bold|black|heavy|semi/i.test(n.fontName.style);
|
|
1142
|
+
const large = n.fontSize >= 24 || bold && n.fontSize >= 18.66;
|
|
1143
|
+
return { ratio: contrastRatio(fg, bg), min: large ? 3 : 4.5, large };
|
|
1144
|
+
}
|
|
1145
|
+
function solidOf(paints) {
|
|
1146
|
+
if (!paints || paints === figma.mixed)
|
|
1147
|
+
return null;
|
|
1148
|
+
for (let i = paints.length - 1;i >= 0; i--) {
|
|
1149
|
+
const p = paints[i];
|
|
1150
|
+
if (p.visible !== false && p.type === "SOLID" && (p.opacity === undefined || p.opacity > 0.9))
|
|
1151
|
+
return p.color;
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function backgroundOf(n, pageBg) {
|
|
1156
|
+
let p = n.parent;
|
|
1157
|
+
while (p && p.type !== "PAGE") {
|
|
1158
|
+
if (p.fills && p.fills !== figma.mixed) {
|
|
1159
|
+
const images = p.fills.some(function(f) {
|
|
1160
|
+
return f.visible !== false && (f.type === "IMAGE" || f.type.indexOf("GRADIENT") === 0);
|
|
1161
|
+
});
|
|
1162
|
+
if (images)
|
|
1163
|
+
return null;
|
|
1164
|
+
const c = solidOf(p.fills);
|
|
1165
|
+
if (c)
|
|
1166
|
+
return c;
|
|
1167
|
+
}
|
|
1168
|
+
p = p.parent;
|
|
1169
|
+
}
|
|
1170
|
+
return pageBg;
|
|
1171
|
+
}
|
|
1172
|
+
function pageBackground() {
|
|
1173
|
+
const bg = solidOf(figma.currentPage.backgrounds);
|
|
1174
|
+
return bg || { r: 1, g: 1, b: 1 };
|
|
1175
|
+
}
|
|
1176
|
+
function inside(a, b) {
|
|
1177
|
+
return a.x >= b.x - 0.5 && a.y >= b.y - 0.5 && a.x + a.width <= b.x + b.width + 0.5 && a.y + a.height <= b.y + b.height + 0.5;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// plugin/lib/prototype.ts
|
|
1181
|
+
var TRIGGERS = {
|
|
1182
|
+
click: "ON_CLICK",
|
|
1183
|
+
tap: "ON_CLICK",
|
|
1184
|
+
hover: "ON_HOVER",
|
|
1185
|
+
press: "ON_PRESS",
|
|
1186
|
+
drag: "ON_DRAG",
|
|
1187
|
+
after: "AFTER_TIMEOUT",
|
|
1188
|
+
timeout: "AFTER_TIMEOUT",
|
|
1189
|
+
"mouse-enter": "MOUSE_ENTER",
|
|
1190
|
+
"mouse-leave": "MOUSE_LEAVE",
|
|
1191
|
+
"mouse-down": "MOUSE_DOWN",
|
|
1192
|
+
"mouse-up": "MOUSE_UP"
|
|
1193
|
+
};
|
|
1194
|
+
var NAVIGATION = {
|
|
1195
|
+
navigate: "NAVIGATE",
|
|
1196
|
+
overlay: "OVERLAY",
|
|
1197
|
+
swap: "SWAP",
|
|
1198
|
+
"swap-overlay": "SWAP",
|
|
1199
|
+
scroll: "SCROLL_TO",
|
|
1200
|
+
"scroll-to": "SCROLL_TO",
|
|
1201
|
+
"change-to": "CHANGE_TO"
|
|
1202
|
+
};
|
|
1203
|
+
var EASINGS = {
|
|
1204
|
+
linear: "LINEAR",
|
|
1205
|
+
"ease-in": "EASE_IN",
|
|
1206
|
+
"ease-out": "EASE_OUT",
|
|
1207
|
+
"ease-in-out": "EASE_IN_AND_OUT",
|
|
1208
|
+
"ease-in-back": "EASE_IN_BACK",
|
|
1209
|
+
"ease-out-back": "EASE_OUT_BACK",
|
|
1210
|
+
"ease-in-out-back": "EASE_IN_AND_OUT_BACK",
|
|
1211
|
+
gentle: "GENTLE",
|
|
1212
|
+
quick: "QUICK",
|
|
1213
|
+
bouncy: "BOUNCY",
|
|
1214
|
+
slow: "SLOW"
|
|
1215
|
+
};
|
|
1216
|
+
var DIRECTIONAL = {
|
|
1217
|
+
"move-in": "MOVE_IN",
|
|
1218
|
+
"move-out": "MOVE_OUT",
|
|
1219
|
+
push: "PUSH",
|
|
1220
|
+
slide: "SLIDE_IN",
|
|
1221
|
+
"slide-in": "SLIDE_IN",
|
|
1222
|
+
"slide-out": "SLIDE_OUT"
|
|
1223
|
+
};
|
|
1224
|
+
var key = function(v) {
|
|
1225
|
+
return String(v || "").toLowerCase().replace(/[_\s]+/g, "-");
|
|
1226
|
+
};
|
|
1227
|
+
function transition(spec) {
|
|
1228
|
+
let t = key(spec.transition || "instant");
|
|
1229
|
+
if (t === "instant" || t === "none")
|
|
1230
|
+
return null;
|
|
1231
|
+
let direction = key(spec.direction || "left");
|
|
1232
|
+
const m = /^(.*)-(left|right|top|bottom|up|down)$/.exec(t);
|
|
1233
|
+
if (m && DIRECTIONAL[m[1]]) {
|
|
1234
|
+
t = m[1];
|
|
1235
|
+
direction = m[2];
|
|
1236
|
+
}
|
|
1237
|
+
direction = direction === "up" ? "top" : direction === "down" ? "bottom" : direction;
|
|
1238
|
+
let ms = typeof spec.duration === "number" ? spec.duration : 300;
|
|
1239
|
+
if (ms <= 10)
|
|
1240
|
+
ms = ms * 1000;
|
|
1241
|
+
const easing = { type: EASINGS[key(spec.easing || "ease-out")] || "EASE_OUT" };
|
|
1242
|
+
const duration = ms / 1000;
|
|
1243
|
+
if (t === "dissolve" || t === "fade")
|
|
1244
|
+
return { type: "DISSOLVE", easing, duration };
|
|
1245
|
+
if (t === "smart" || t === "smart-animate")
|
|
1246
|
+
return { type: "SMART_ANIMATE", easing, duration };
|
|
1247
|
+
if (DIRECTIONAL[t]) {
|
|
1248
|
+
return {
|
|
1249
|
+
type: DIRECTIONAL[t],
|
|
1250
|
+
direction: direction.toUpperCase(),
|
|
1251
|
+
matchLayers: !!spec.matchLayers,
|
|
1252
|
+
easing,
|
|
1253
|
+
duration
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
throw codeError('Unknown transition "' + spec.transition + '" (instant, dissolve, smart, move-in, move-out, push, slide-in, slide-out)', "BAD_ARGS");
|
|
1257
|
+
}
|
|
1258
|
+
function trigger(spec) {
|
|
1259
|
+
const t = TRIGGERS[key(spec.trigger || "click")];
|
|
1260
|
+
if (!t)
|
|
1261
|
+
throw codeError('Unknown trigger "' + spec.trigger + '" (click, hover, press, drag, after, mouse-enter, mouse-leave)', "BAD_ARGS");
|
|
1262
|
+
if (t === "AFTER_TIMEOUT")
|
|
1263
|
+
return { type: t, timeout: (typeof spec.delay === "number" ? spec.delay : 800) / 1000 };
|
|
1264
|
+
if (t === "MOUSE_ENTER" || t === "MOUSE_LEAVE")
|
|
1265
|
+
return { type: t, delay: (spec.delay || 0) / 1000, deprecatedVersion: false };
|
|
1266
|
+
if (t === "MOUSE_DOWN" || t === "MOUSE_UP")
|
|
1267
|
+
return { type: t, delay: (spec.delay || 0) / 1000 };
|
|
1268
|
+
return { type: t };
|
|
1269
|
+
}
|
|
1270
|
+
async function toReaction(spec, resolve) {
|
|
1271
|
+
const action = key(spec.action || (spec.url ? "url" : spec.to ? "navigate" : "back"));
|
|
1272
|
+
let act;
|
|
1273
|
+
if (action === "back")
|
|
1274
|
+
act = { type: "BACK" };
|
|
1275
|
+
else if (action === "close")
|
|
1276
|
+
act = { type: "CLOSE" };
|
|
1277
|
+
else if (action === "url" || action === "open-url")
|
|
1278
|
+
act = { type: "URL", url: String(spec.url) };
|
|
1279
|
+
else if (NAVIGATION[action]) {
|
|
1280
|
+
if (!spec.to)
|
|
1281
|
+
throw codeError('Action "' + action + '" needs `to` (a frame name or id)', "BAD_ARGS");
|
|
1282
|
+
act = { type: "NODE", destinationId: await resolve(String(spec.to)), navigation: NAVIGATION[action], transition: transition(spec), preserveScrollPosition: !!spec.preserveScroll };
|
|
1283
|
+
} else {
|
|
1284
|
+
throw codeError('Unknown action "' + spec.action + '" (navigate, overlay, swap, scroll-to, change-to, back, close, url)', "BAD_ARGS");
|
|
1285
|
+
}
|
|
1286
|
+
return { trigger: trigger(spec), actions: [act] };
|
|
1287
|
+
}
|
|
1288
|
+
async function resolveFrame(ref) {
|
|
1289
|
+
if (/^[\dI;:]+$/.test(ref))
|
|
1290
|
+
return (await getNode(ref)).id;
|
|
1291
|
+
const top = figma.currentPage.children;
|
|
1292
|
+
for (let i = 0;i < top.length; i++)
|
|
1293
|
+
if (top[i].name === ref)
|
|
1294
|
+
return top[i].id;
|
|
1295
|
+
const any = figma.currentPage.findOne(function(n) {
|
|
1296
|
+
return n.name === ref;
|
|
1297
|
+
});
|
|
1298
|
+
if (!any)
|
|
1299
|
+
throw codeError('No frame named "' + ref + '" on this page', "NOT_FOUND");
|
|
1300
|
+
return any.id;
|
|
1301
|
+
}
|
|
1302
|
+
async function setReactions(node, reactions, replace) {
|
|
1303
|
+
if (typeof node.setReactionsAsync !== "function")
|
|
1304
|
+
throw codeError("A " + node.type + " node cannot have prototype interactions", "BAD_ARGS");
|
|
1305
|
+
const current = replace ? [] : (node.reactions || []).slice();
|
|
1306
|
+
await node.setReactionsAsync(current.concat(reactions));
|
|
1307
|
+
}
|
|
1308
|
+
async function applyBuildReactions(pending, ids, warnings) {
|
|
1309
|
+
const resolve = function(ref) {
|
|
1310
|
+
return ids[ref] ? Promise.resolve(ids[ref]) : resolveFrame(ref);
|
|
1311
|
+
};
|
|
1312
|
+
for (let i = 0;i < pending.length; i++) {
|
|
1313
|
+
const item = pending[i];
|
|
1314
|
+
const list = Array.isArray(item.reactions) ? item.reactions : [item.reactions];
|
|
1315
|
+
const out = [];
|
|
1316
|
+
for (let k = 0;k < list.length; k++) {
|
|
1317
|
+
try {
|
|
1318
|
+
out.push(await toReaction(list[k] || {}, resolve));
|
|
1319
|
+
} catch (e) {
|
|
1320
|
+
warnings.push(item.path + ".reactions[" + k + "]: " + (e.message || e));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
if (out.length)
|
|
1325
|
+
await setReactions(item.node, out, false);
|
|
1326
|
+
} catch (e) {
|
|
1327
|
+
warnings.push(item.path + ".reactions: " + (e.message || e));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async function prototype(p) {
|
|
1332
|
+
const links = Array.isArray(p.links) ? p.links : [];
|
|
1333
|
+
const flows = Array.isArray(p.flows) ? p.flows : [];
|
|
1334
|
+
const clear = Array.isArray(p.clear) ? p.clear : [];
|
|
1335
|
+
const out = {};
|
|
1336
|
+
for (let i = 0;i < clear.length; i++)
|
|
1337
|
+
await setReactions(await getNode(clear[i]), [], true);
|
|
1338
|
+
if (clear.length)
|
|
1339
|
+
out.cleared = clear.length;
|
|
1340
|
+
const bySource = {};
|
|
1341
|
+
const order = [];
|
|
1342
|
+
for (let i = 0;i < links.length; i++) {
|
|
1343
|
+
const l = links[i] || {};
|
|
1344
|
+
if (!l.from)
|
|
1345
|
+
throw codeError("links[" + i + "]: `from` is required (the layer that reacts)", "BAD_ARGS");
|
|
1346
|
+
const from = await resolveFrame(String(l.from));
|
|
1347
|
+
let r;
|
|
1348
|
+
try {
|
|
1349
|
+
r = await toReaction(l, resolveFrame);
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
throw codeError("links[" + i + "]: " + (e.message || e), e.code || "BAD_ARGS");
|
|
1352
|
+
}
|
|
1353
|
+
if (!bySource[from]) {
|
|
1354
|
+
bySource[from] = [];
|
|
1355
|
+
order.push(from);
|
|
1356
|
+
}
|
|
1357
|
+
bySource[from].push(r);
|
|
1358
|
+
}
|
|
1359
|
+
for (let i = 0;i < order.length; i++)
|
|
1360
|
+
await setReactions(await getNode(order[i]), bySource[order[i]], !!p.replace);
|
|
1361
|
+
if (links.length)
|
|
1362
|
+
out.linked = links.length;
|
|
1363
|
+
if (flows.length) {
|
|
1364
|
+
const page = figma.currentPage;
|
|
1365
|
+
const points = page.flowStartingPoints.slice();
|
|
1366
|
+
for (let i = 0;i < flows.length; i++) {
|
|
1367
|
+
const f = flows[i] || {};
|
|
1368
|
+
const id = await resolveFrame(String(f.nodeId || f.frame || f.start || ""));
|
|
1369
|
+
const node = await getNode(id);
|
|
1370
|
+
if (!node.parent || node.parent.type !== "PAGE")
|
|
1371
|
+
throw codeError('Flow start "' + node.name + '" must be a top-level frame', "BAD_ARGS");
|
|
1372
|
+
const name = String(f.name || node.name);
|
|
1373
|
+
let found = false;
|
|
1374
|
+
for (let k = 0;k < points.length; k++) {
|
|
1375
|
+
if (points[k].nodeId === id) {
|
|
1376
|
+
points[k] = { nodeId: id, name };
|
|
1377
|
+
found = true;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (!found)
|
|
1381
|
+
points.push({ nodeId: id, name });
|
|
1382
|
+
}
|
|
1383
|
+
page.flowStartingPoints = points;
|
|
1384
|
+
out.flows = points;
|
|
1385
|
+
}
|
|
1386
|
+
if (p.list || !links.length && !flows.length && !clear.length)
|
|
1387
|
+
out.interactions = await listReactions(p.nodeId);
|
|
1388
|
+
return out;
|
|
1389
|
+
}
|
|
1390
|
+
async function listReactions(nodeId) {
|
|
1391
|
+
const root = nodeId ? await getNode(nodeId) : figma.currentPage;
|
|
1392
|
+
const nodes = (root.type === "PAGE" ? [] : [root]).concat("findAll" in root ? root.findAll(function(n) {
|
|
1393
|
+
return n.reactions && n.reactions.length > 0;
|
|
1394
|
+
}) : []);
|
|
1395
|
+
const names = {};
|
|
1396
|
+
const out = [];
|
|
1397
|
+
for (let i = 0;i < nodes.length && out.length < 300; i++) {
|
|
1398
|
+
const n = nodes[i];
|
|
1399
|
+
const reactions = n.reactions || [];
|
|
1400
|
+
for (let k = 0;k < reactions.length; k++) {
|
|
1401
|
+
const r = reactions[k];
|
|
1402
|
+
const actions = r.actions || (r.action ? [r.action] : []);
|
|
1403
|
+
for (let a = 0;a < actions.length; a++) {
|
|
1404
|
+
const act = actions[a];
|
|
1405
|
+
const item = { from: n.id, fromName: n.name, trigger: r.trigger ? r.trigger.type : null, action: act.type };
|
|
1406
|
+
if (act.type === "NODE") {
|
|
1407
|
+
item.navigation = act.navigation;
|
|
1408
|
+
item.to = act.destinationId;
|
|
1409
|
+
if (act.destinationId && names[act.destinationId] === undefined) {
|
|
1410
|
+
const dest = await figma.getNodeByIdAsync(act.destinationId);
|
|
1411
|
+
names[act.destinationId] = dest ? dest.name : "";
|
|
1412
|
+
}
|
|
1413
|
+
item.toName = names[act.destinationId] || undefined;
|
|
1414
|
+
if (act.transition)
|
|
1415
|
+
item.transition = act.transition.type + (act.transition.direction ? " " + act.transition.direction : "") + " " + Math.round(act.transition.duration * 1000) + "ms";
|
|
1416
|
+
}
|
|
1417
|
+
if (act.type === "URL")
|
|
1418
|
+
item.url = act.url;
|
|
1419
|
+
out.push(item);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return { flows: figma.currentPage.flowStartingPoints, links: out };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// plugin/lib/tokens.ts
|
|
1427
|
+
var TYPES = { color: "COLOR", number: "FLOAT", string: "STRING", boolean: "BOOLEAN" };
|
|
1428
|
+
function counter() {
|
|
1429
|
+
return { created: 0, updated: 0 };
|
|
1430
|
+
}
|
|
1431
|
+
async function designTokens(p) {
|
|
1432
|
+
const warnings = (p.warnings || []).slice();
|
|
1433
|
+
const counts = {
|
|
1434
|
+
collections: counter(),
|
|
1435
|
+
modes: counter(),
|
|
1436
|
+
variables: counter(),
|
|
1437
|
+
paintStyles: counter(),
|
|
1438
|
+
textStyles: counter(),
|
|
1439
|
+
effectStyles: counter()
|
|
1440
|
+
};
|
|
1441
|
+
const collections = Array.isArray(p.collections) ? p.collections : [];
|
|
1442
|
+
const styles = p.styles || {};
|
|
1443
|
+
if (!collections.length && !(styles.colors || []).length && !(styles.text || []).length && !(styles.effects || []).length) {
|
|
1444
|
+
throw codeError("No tokens found: give collections and/or styles (see the tool description for the accepted formats)", "BAD_ARGS");
|
|
1445
|
+
}
|
|
1446
|
+
if (collections.length)
|
|
1447
|
+
await writeVariables(collections, counts, warnings);
|
|
1448
|
+
invalidateCaches();
|
|
1449
|
+
await writeStyles(styles, counts, warnings);
|
|
1450
|
+
invalidateCaches();
|
|
1451
|
+
const out = { counts };
|
|
1452
|
+
if (warnings.length)
|
|
1453
|
+
out.warnings = warnings.slice(0, 60);
|
|
1454
|
+
return out;
|
|
1455
|
+
}
|
|
1456
|
+
async function writeVariables(specs, counts, warnings) {
|
|
1457
|
+
const allCollections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
1458
|
+
const allVars = await figma.variables.getLocalVariablesAsync();
|
|
1459
|
+
const byName = {};
|
|
1460
|
+
const collName = {};
|
|
1461
|
+
for (let i = 0;i < allCollections.length; i++)
|
|
1462
|
+
collName[allCollections[i].id] = allCollections[i].name;
|
|
1463
|
+
const index = function(v, collectionName) {
|
|
1464
|
+
byName[collectionName + "/" + v.name] = v;
|
|
1465
|
+
if (!byName[v.name])
|
|
1466
|
+
byName[v.name] = v;
|
|
1467
|
+
};
|
|
1468
|
+
for (let i = 0;i < allVars.length; i++)
|
|
1469
|
+
index(allVars[i], collName[allVars[i].variableCollectionId]);
|
|
1470
|
+
const pending = [];
|
|
1471
|
+
for (let c = 0;c < specs.length; c++) {
|
|
1472
|
+
const spec = specs[c];
|
|
1473
|
+
let collection = null;
|
|
1474
|
+
for (let i = 0;i < allCollections.length; i++)
|
|
1475
|
+
if (allCollections[i].name === spec.name)
|
|
1476
|
+
collection = allCollections[i];
|
|
1477
|
+
const fresh = !collection;
|
|
1478
|
+
if (!collection) {
|
|
1479
|
+
collection = figma.variables.createVariableCollection(String(spec.name));
|
|
1480
|
+
counts.collections.created++;
|
|
1481
|
+
} else {
|
|
1482
|
+
counts.collections.updated++;
|
|
1483
|
+
}
|
|
1484
|
+
const modeIds = ensureModes(collection, spec.modes || [], fresh, counts, warnings);
|
|
1485
|
+
const existing = {};
|
|
1486
|
+
for (let i = 0;i < allVars.length; i++)
|
|
1487
|
+
if (allVars[i].variableCollectionId === collection.id)
|
|
1488
|
+
existing[allVars[i].name] = allVars[i];
|
|
1489
|
+
const vars = spec.variables || [];
|
|
1490
|
+
for (let i = 0;i < vars.length; i++) {
|
|
1491
|
+
const v = vars[i];
|
|
1492
|
+
const item = { variable: existing[v.name] || null, spec: v, collection, modeIds };
|
|
1493
|
+
if (item.variable && v.type && TYPES[v.type] && item.variable.resolvedType !== TYPES[v.type]) {
|
|
1494
|
+
warnings.push('"' + v.name + '" exists as ' + item.variable.resolvedType + ", cannot change it to " + TYPES[v.type] + ": skipped");
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
pending.push(item);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
for (let pass = 0;pass < 4; pass++) {
|
|
1501
|
+
for (let i = 0;i < pending.length; i++) {
|
|
1502
|
+
const item = pending[i];
|
|
1503
|
+
if (item.variable)
|
|
1504
|
+
continue;
|
|
1505
|
+
const type = item.spec.type ? TYPES[item.spec.type] : aliasTargetType(item.spec, item.collection.name, byName);
|
|
1506
|
+
if (!type)
|
|
1507
|
+
continue;
|
|
1508
|
+
item.variable = figma.variables.createVariable(String(item.spec.name), item.collection, type);
|
|
1509
|
+
index(item.variable, item.collection.name);
|
|
1510
|
+
counts.variables.created++;
|
|
1511
|
+
item.spec.created = true;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
for (let i = 0;i < pending.length; i++) {
|
|
1515
|
+
const item = pending[i];
|
|
1516
|
+
const v = item.variable;
|
|
1517
|
+
if (!v) {
|
|
1518
|
+
warnings.push('"' + item.spec.name + '": alias target not found, skipped');
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
if (!item.spec.created)
|
|
1522
|
+
counts.variables.updated++;
|
|
1523
|
+
if (item.spec.description !== undefined)
|
|
1524
|
+
v.description = String(item.spec.description);
|
|
1525
|
+
if (Array.isArray(item.spec.scopes)) {
|
|
1526
|
+
try {
|
|
1527
|
+
v.scopes = item.spec.scopes;
|
|
1528
|
+
} catch (e) {
|
|
1529
|
+
warnings.push('"' + v.name + '": invalid scopes (' + (e.message || e) + ")");
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const values = item.spec.values || {};
|
|
1533
|
+
const modes = Object.keys(values);
|
|
1534
|
+
for (let m = 0;m < modes.length; m++) {
|
|
1535
|
+
const targets = [];
|
|
1536
|
+
if (modes[m] === "*") {
|
|
1537
|
+
for (let k = 0;k < item.collection.modes.length; k++)
|
|
1538
|
+
targets.push(item.collection.modes[k].modeId);
|
|
1539
|
+
} else if (item.modeIds[modes[m]]) {
|
|
1540
|
+
targets.push(item.modeIds[modes[m]]);
|
|
1541
|
+
} else {
|
|
1542
|
+
warnings.push('"' + v.name + '": unknown mode "' + modes[m] + '"');
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
let value;
|
|
1546
|
+
try {
|
|
1547
|
+
value = toVariableValue(values[modes[m]], v, item.collection.name, byName);
|
|
1548
|
+
} catch (e) {
|
|
1549
|
+
warnings.push('"' + v.name + '": ' + (e.message || e));
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
for (let k = 0;k < targets.length; k++) {
|
|
1553
|
+
try {
|
|
1554
|
+
v.setValueForMode(targets[k], value);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
warnings.push('"' + v.name + '": ' + (e.message || e));
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
function ensureModes(collection, names, fresh, counts, warnings) {
|
|
1563
|
+
const ids = {};
|
|
1564
|
+
if (fresh && names.length) {
|
|
1565
|
+
collection.renameMode(collection.modes[0].modeId, String(names[0]));
|
|
1566
|
+
counts.modes.created++;
|
|
1567
|
+
}
|
|
1568
|
+
for (let i = 0;i < collection.modes.length; i++)
|
|
1569
|
+
ids[collection.modes[i].name] = collection.modes[i].modeId;
|
|
1570
|
+
for (let i = 0;i < names.length; i++) {
|
|
1571
|
+
const name = String(names[i]);
|
|
1572
|
+
if (ids[name])
|
|
1573
|
+
continue;
|
|
1574
|
+
try {
|
|
1575
|
+
ids[name] = collection.addMode(name);
|
|
1576
|
+
counts.modes.created++;
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
warnings.push('Cannot add mode "' + name + '" to "' + collection.name + '": ' + (e.message || e) + " (the Figma plan may limit modes)");
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return ids;
|
|
1582
|
+
}
|
|
1583
|
+
function findAlias(ref, collectionName, byName) {
|
|
1584
|
+
return byName[collectionName + "/" + ref] || byName[ref] || null;
|
|
1585
|
+
}
|
|
1586
|
+
function aliasTargetType(spec, collectionName, byName) {
|
|
1587
|
+
const values = spec.values || {};
|
|
1588
|
+
const keys = Object.keys(values);
|
|
1589
|
+
for (let i = 0;i < keys.length; i++) {
|
|
1590
|
+
const v = values[keys[i]];
|
|
1591
|
+
if (v && typeof v === "object" && v.alias) {
|
|
1592
|
+
const target = findAlias(String(v.alias), collectionName, byName);
|
|
1593
|
+
if (target)
|
|
1594
|
+
return target.resolvedType;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
function toVariableValue(raw, v, collectionName, byName) {
|
|
1600
|
+
if (raw && typeof raw === "object" && raw.alias) {
|
|
1601
|
+
const target = findAlias(String(raw.alias), collectionName, byName);
|
|
1602
|
+
if (!target)
|
|
1603
|
+
throw new Error('alias target "' + raw.alias + '" not found');
|
|
1604
|
+
if (target.id === v.id)
|
|
1605
|
+
throw new Error("a variable cannot alias itself");
|
|
1606
|
+
return figma.variables.createVariableAlias(target);
|
|
1607
|
+
}
|
|
1608
|
+
switch (v.resolvedType) {
|
|
1609
|
+
case "COLOR":
|
|
1610
|
+
return parseHex(String(raw));
|
|
1611
|
+
case "FLOAT":
|
|
1612
|
+
if (typeof raw !== "number" && isNaN(Number(raw)))
|
|
1613
|
+
throw new Error("expected a number, got " + JSON.stringify(raw));
|
|
1614
|
+
return Number(raw);
|
|
1615
|
+
case "BOOLEAN":
|
|
1616
|
+
return raw === true || raw === "true";
|
|
1617
|
+
default:
|
|
1618
|
+
return String(raw);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
async function writeStyles(styles, counts, warnings) {
|
|
1622
|
+
const local = await localStyles();
|
|
1623
|
+
const find = function(list, name) {
|
|
1624
|
+
for (let i = 0;i < list.length; i++)
|
|
1625
|
+
if (list[i].name === name)
|
|
1626
|
+
return list[i];
|
|
1627
|
+
return null;
|
|
1628
|
+
};
|
|
1629
|
+
const colors = styles.colors || [];
|
|
1630
|
+
for (let i = 0;i < colors.length; i++) {
|
|
1631
|
+
const s = colors[i];
|
|
1632
|
+
try {
|
|
1633
|
+
const value = s.value !== undefined ? s.value : s.color;
|
|
1634
|
+
const list = Array.isArray(value) ? value : [value];
|
|
1635
|
+
const paints = [];
|
|
1636
|
+
for (let k = 0;k < list.length; k++)
|
|
1637
|
+
paints.push(await toPaint(list[k]));
|
|
1638
|
+
let style = find(local.paint, s.name);
|
|
1639
|
+
if (style)
|
|
1640
|
+
counts.paintStyles.updated++;
|
|
1641
|
+
else {
|
|
1642
|
+
style = figma.createPaintStyle();
|
|
1643
|
+
style.name = String(s.name);
|
|
1644
|
+
counts.paintStyles.created++;
|
|
1645
|
+
}
|
|
1646
|
+
style.paints = paints;
|
|
1647
|
+
if (s.description !== undefined)
|
|
1648
|
+
style.description = String(s.description);
|
|
1649
|
+
} catch (e) {
|
|
1650
|
+
warnings.push('Color style "' + s.name + '": ' + (e.message || e));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const texts = styles.text || [];
|
|
1654
|
+
for (let i = 0;i < texts.length; i++) {
|
|
1655
|
+
const s = texts[i];
|
|
1656
|
+
try {
|
|
1657
|
+
const font = await loadWithFallback(parseFont(s.font || "Inter", s.weight), warnings);
|
|
1658
|
+
let style = find(local.text, s.name);
|
|
1659
|
+
if (style)
|
|
1660
|
+
counts.textStyles.updated++;
|
|
1661
|
+
else {
|
|
1662
|
+
style = figma.createTextStyle();
|
|
1663
|
+
style.name = String(s.name);
|
|
1664
|
+
counts.textStyles.created++;
|
|
1665
|
+
}
|
|
1666
|
+
style.fontName = font;
|
|
1667
|
+
if (typeof s.size === "number")
|
|
1668
|
+
style.fontSize = s.size;
|
|
1669
|
+
if (s.lineHeight !== undefined && s.lineHeight !== null)
|
|
1670
|
+
style.lineHeight = lineHeight(s.lineHeight);
|
|
1671
|
+
if (s.letterSpacing !== undefined && s.letterSpacing !== null) {
|
|
1672
|
+
const ls = String(s.letterSpacing);
|
|
1673
|
+
style.letterSpacing = /%$/.test(ls) ? { value: parseFloat(ls), unit: "PERCENT" } : { value: Number(s.letterSpacing), unit: "PIXELS" };
|
|
1674
|
+
}
|
|
1675
|
+
if (typeof s.paragraphSpacing === "number")
|
|
1676
|
+
style.paragraphSpacing = s.paragraphSpacing;
|
|
1677
|
+
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE", none: "ORIGINAL" };
|
|
1678
|
+
if (s.case && cases[s.case])
|
|
1679
|
+
style.textCase = cases[s.case];
|
|
1680
|
+
const deco = { underline: "UNDERLINE", strike: "STRIKETHROUGH", strikethrough: "STRIKETHROUGH", none: "NONE" };
|
|
1681
|
+
if (s.decoration && deco[s.decoration])
|
|
1682
|
+
style.textDecoration = deco[s.decoration];
|
|
1683
|
+
if (s.description !== undefined)
|
|
1684
|
+
style.description = String(s.description);
|
|
1685
|
+
} catch (e) {
|
|
1686
|
+
warnings.push('Text style "' + s.name + '": ' + (e.message || e));
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const effects = styles.effects || [];
|
|
1690
|
+
for (let i = 0;i < effects.length; i++) {
|
|
1691
|
+
const s = effects[i];
|
|
1692
|
+
try {
|
|
1693
|
+
const value = s.value !== undefined ? s.value : s;
|
|
1694
|
+
const list = Array.isArray(value) ? value : [value];
|
|
1695
|
+
const out = [];
|
|
1696
|
+
for (let k = 0;k < list.length; k++) {
|
|
1697
|
+
const e = list[k];
|
|
1698
|
+
if (e && typeof e.blur === "number" && e.type === "layer")
|
|
1699
|
+
out.push({ type: "LAYER_BLUR", radius: e.blur, visible: true });
|
|
1700
|
+
else if (e && typeof e.blur === "number" && e.type === "background")
|
|
1701
|
+
out.push({ type: "BACKGROUND_BLUR", radius: e.blur, visible: true });
|
|
1702
|
+
else
|
|
1703
|
+
out.push(shadowEffect(e || {}));
|
|
1704
|
+
}
|
|
1705
|
+
let style = find(local.effect, s.name);
|
|
1706
|
+
if (style)
|
|
1707
|
+
counts.effectStyles.updated++;
|
|
1708
|
+
else {
|
|
1709
|
+
style = figma.createEffectStyle();
|
|
1710
|
+
style.name = String(s.name);
|
|
1711
|
+
counts.effectStyles.created++;
|
|
1712
|
+
}
|
|
1713
|
+
style.effects = out;
|
|
1714
|
+
if (s.description !== undefined)
|
|
1715
|
+
style.description = String(s.description);
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
warnings.push('Effect style "' + s.name + '": ' + (e.message || e));
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
async function loadWithFallback(f, warnings) {
|
|
1722
|
+
const candidates = [f, { family: f.family, style: "Regular" }, { family: "Inter", style: "Regular" }];
|
|
1723
|
+
for (let i = 0;i < candidates.length; i++) {
|
|
1724
|
+
try {
|
|
1725
|
+
await loadFont(candidates[i]);
|
|
1726
|
+
if (i > 0)
|
|
1727
|
+
warnings.push('Font "' + f.family + " " + f.style + '" unavailable, used "' + candidates[i].family + " " + candidates[i].style + '"');
|
|
1728
|
+
return candidates[i];
|
|
1729
|
+
} catch (e) {}
|
|
1730
|
+
}
|
|
1731
|
+
throw codeError('No usable font for "' + f.family + " " + f.style + '"', "FONT");
|
|
1732
|
+
}
|
|
1733
|
+
async function applyModes(node, modes, warnings, path) {
|
|
1734
|
+
if (!modes || typeof modes !== "object" || typeof node.setExplicitVariableModeForCollection !== "function")
|
|
1735
|
+
return;
|
|
1736
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
1737
|
+
const names = Object.keys(modes);
|
|
1738
|
+
for (let i = 0;i < names.length; i++) {
|
|
1739
|
+
let collection = null;
|
|
1740
|
+
for (let k = 0;k < collections.length; k++)
|
|
1741
|
+
if (collections[k].name === names[i] || collections[k].id === names[i])
|
|
1742
|
+
collection = collections[k];
|
|
1743
|
+
if (!collection) {
|
|
1744
|
+
warnings.push(path + ': no variable collection named "' + names[i] + '"');
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
let modeId = "";
|
|
1748
|
+
for (let k = 0;k < collection.modes.length; k++)
|
|
1749
|
+
if (collection.modes[k].name === modes[names[i]])
|
|
1750
|
+
modeId = collection.modes[k].modeId;
|
|
1751
|
+
if (!modeId) {
|
|
1752
|
+
warnings.push(path + ': collection "' + collection.name + '" has no mode "' + modes[names[i]] + '"');
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
node.setExplicitVariableModeForCollection(collection, modeId);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// plugin/lib/build.ts
|
|
1760
|
+
var MAX_NODES3 = 3000;
|
|
1761
|
+
var SHADOW_DEFAULT = { x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" };
|
|
1762
|
+
async function build(p, _timeoutMs, requestId) {
|
|
1763
|
+
const spec = p.spec;
|
|
1764
|
+
if (!spec || typeof spec !== "object")
|
|
1765
|
+
throw codeError("`spec` must be a node object or an array of node objects", "BAD_ARGS");
|
|
1766
|
+
const roots = Array.isArray(spec) ? spec : [spec];
|
|
1767
|
+
const d = p.defaults || {};
|
|
1768
|
+
const ctx = {
|
|
1769
|
+
warnings: [],
|
|
1770
|
+
ids: {},
|
|
1771
|
+
count: 0,
|
|
1772
|
+
images: p.imageBytes || {},
|
|
1773
|
+
fonts: {},
|
|
1774
|
+
defaults: { font: d.font || "Inter", color: d.color || "#111111", size: d.size || 14 },
|
|
1775
|
+
binds: [],
|
|
1776
|
+
reactions: [],
|
|
1777
|
+
requestId,
|
|
1778
|
+
total: countNodes(roots),
|
|
1779
|
+
lastProgress: 0
|
|
1780
|
+
};
|
|
1781
|
+
await preloadFonts(roots, ctx);
|
|
1782
|
+
const parent = await parentFor(p.parentId);
|
|
1783
|
+
const made = [];
|
|
1784
|
+
let cursorX = null;
|
|
1785
|
+
for (let i = 0;i < roots.length; i++) {
|
|
1786
|
+
const node = await createNode(roots[i], parent, ctx, "spec" + (roots.length > 1 ? "[" + i + "]" : ""));
|
|
1787
|
+
if (!node)
|
|
1788
|
+
continue;
|
|
1789
|
+
if (!isAutoLayout(parent) && roots[i].x === undefined && roots[i].y === undefined) {
|
|
1790
|
+
if (cursorX === null) {
|
|
1791
|
+
place(node, p.x, p.y);
|
|
1792
|
+
} else {
|
|
1793
|
+
node.x = cursorX;
|
|
1794
|
+
node.y = made[0].y;
|
|
1795
|
+
}
|
|
1796
|
+
cursorX = node.x + node.width + 80;
|
|
1797
|
+
}
|
|
1798
|
+
made.push(node);
|
|
1799
|
+
}
|
|
1800
|
+
if (ctx.reactions.length)
|
|
1801
|
+
await applyBuildReactions(ctx.reactions, ctx.ids, ctx.warnings);
|
|
1802
|
+
for (let i = 0;i < ctx.binds.length; i++)
|
|
1803
|
+
ctx.warnings.push(ctx.binds[i].path + ": bind needs a component or componentSet ancestor, ignored");
|
|
1804
|
+
if (p.select !== false && made.length && parent.type === "PAGE" && parent === figma.currentPage) {
|
|
1805
|
+
figma.currentPage.selection = made;
|
|
1806
|
+
figma.viewport.scrollAndZoomIntoView(made);
|
|
1807
|
+
}
|
|
1808
|
+
const out = {
|
|
1809
|
+
rootId: made.length ? made[0].id : null,
|
|
1810
|
+
rootIds: made.map(function(n) {
|
|
1811
|
+
return n.id;
|
|
1812
|
+
}),
|
|
1813
|
+
created: ctx.count,
|
|
1814
|
+
ids: ctx.ids
|
|
1815
|
+
};
|
|
1816
|
+
if (ctx.warnings.length)
|
|
1817
|
+
out.warnings = ctx.warnings.slice(0, 50);
|
|
1818
|
+
return out;
|
|
1819
|
+
}
|
|
1820
|
+
function countNodes(list) {
|
|
1821
|
+
let n = 0;
|
|
1822
|
+
for (let i = 0;i < list.length; i++) {
|
|
1823
|
+
const s = list[i];
|
|
1824
|
+
if (!s || typeof s !== "object")
|
|
1825
|
+
continue;
|
|
1826
|
+
n++;
|
|
1827
|
+
if (Array.isArray(s.children))
|
|
1828
|
+
n += countNodes(s.children);
|
|
1829
|
+
if (Array.isArray(s.variants)) {
|
|
1830
|
+
for (let k = 0;k < s.variants.length; k++)
|
|
1831
|
+
n += countNodes([Object.assign({}, s.base || {}, s.variants[k])]);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return n;
|
|
1835
|
+
}
|
|
1836
|
+
function progress(ctx) {
|
|
1837
|
+
if (!ctx.requestId)
|
|
1838
|
+
return;
|
|
1839
|
+
const now = Date.now();
|
|
1840
|
+
if (now - ctx.lastProgress < 150 && ctx.count < ctx.total)
|
|
1841
|
+
return;
|
|
1842
|
+
ctx.lastProgress = now;
|
|
1843
|
+
figma.ui.postMessage({ t: "progress", id: ctx.requestId, text: Math.min(ctx.count, ctx.total) + " / " + ctx.total + " layers" });
|
|
1844
|
+
}
|
|
1845
|
+
function fontKey(s, ctx) {
|
|
1846
|
+
return parseFont(s.font || ctx.defaults.font, s.weight, ctx.defaults.font);
|
|
1847
|
+
}
|
|
1848
|
+
function spanFont(s, span, ctx) {
|
|
1849
|
+
if (!span.font && span.weight === undefined)
|
|
1850
|
+
return null;
|
|
1851
|
+
return parseFont(span.font || s.font || ctx.defaults.font, span.weight !== undefined ? span.weight : span.font ? undefined : s.weight, ctx.defaults.font);
|
|
1852
|
+
}
|
|
1853
|
+
async function preloadFonts(roots, ctx) {
|
|
1854
|
+
const wanted = {};
|
|
1855
|
+
const walk = function(s) {
|
|
1856
|
+
if (!s || typeof s !== "object")
|
|
1857
|
+
return;
|
|
1858
|
+
if (nodeType(s) === "text") {
|
|
1859
|
+
const f = fontKey(s, ctx);
|
|
1860
|
+
wanted[f.family + ":" + f.style] = f;
|
|
1861
|
+
const spans = Array.isArray(s.spans) ? s.spans : [];
|
|
1862
|
+
for (let i = 0;i < spans.length; i++) {
|
|
1863
|
+
const sf = spanFont(s, spans[i] || {}, ctx);
|
|
1864
|
+
if (sf)
|
|
1865
|
+
wanted[sf.family + ":" + sf.style] = sf;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (Array.isArray(s.children))
|
|
1869
|
+
s.children.forEach(walk);
|
|
1870
|
+
if (Array.isArray(s.variants)) {
|
|
1871
|
+
for (let i = 0;i < s.variants.length; i++)
|
|
1872
|
+
walk(Object.assign({}, s.base || {}, s.variants[i], { type: "component" }));
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
roots.forEach(walk);
|
|
1876
|
+
const keys = Object.keys(wanted);
|
|
1877
|
+
await Promise.all(keys.map(async function(key) {
|
|
1878
|
+
const f = wanted[key];
|
|
1879
|
+
const candidates = [f, { family: f.family, style: "Regular" }, { family: "Inter", style: f.style }, { family: "Inter", style: "Regular" }];
|
|
1880
|
+
for (let i = 0;i < candidates.length; i++) {
|
|
1881
|
+
try {
|
|
1882
|
+
await loadFont(candidates[i]);
|
|
1883
|
+
ctx.fonts[key] = candidates[i];
|
|
1884
|
+
if (i > 0)
|
|
1885
|
+
ctx.warnings.push('Font "' + f.family + " " + f.style + '" unavailable, used "' + candidates[i].family + " " + candidates[i].style + '"');
|
|
1886
|
+
return;
|
|
1887
|
+
} catch (e) {}
|
|
1888
|
+
}
|
|
1889
|
+
}));
|
|
1890
|
+
}
|
|
1891
|
+
function nodeType(s) {
|
|
1892
|
+
if (s.type) {
|
|
1893
|
+
const t = String(s.type).toLowerCase().replace(/[_\s-]/g, "");
|
|
1894
|
+
return t === "rectangle" ? "rect" : t === "circle" ? "ellipse" : t === "variants" ? "componentset" : t;
|
|
1895
|
+
}
|
|
1896
|
+
if (Array.isArray(s.variants))
|
|
1897
|
+
return "componentset";
|
|
1898
|
+
if (s.text !== undefined || Array.isArray(s.spans))
|
|
1899
|
+
return "text";
|
|
1900
|
+
if (s.svg)
|
|
1901
|
+
return "svg";
|
|
1902
|
+
if (s.icon)
|
|
1903
|
+
return "icon";
|
|
1904
|
+
if (s.imageKey || s.src)
|
|
1905
|
+
return "image";
|
|
1906
|
+
if (s.component)
|
|
1907
|
+
return "instance";
|
|
1908
|
+
return "frame";
|
|
1909
|
+
}
|
|
1910
|
+
async function createNode(s, parent, ctx, path) {
|
|
1911
|
+
if (!s || typeof s !== "object") {
|
|
1912
|
+
ctx.warnings.push(path + ": not an object, skipped");
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1915
|
+
if (ctx.count >= MAX_NODES3)
|
|
1916
|
+
throw codeError("Spec is too large (max " + MAX_NODES3 + " nodes). Split it into several build calls.", "TOO_LARGE");
|
|
1917
|
+
checkCancelled(ctx.requestId);
|
|
1918
|
+
const type = nodeType(s);
|
|
1919
|
+
let node;
|
|
1920
|
+
switch (type) {
|
|
1921
|
+
case "frame":
|
|
1922
|
+
case "component":
|
|
1923
|
+
node = type === "component" ? figma.createComponent() : figma.createFrame();
|
|
1924
|
+
break;
|
|
1925
|
+
case "text":
|
|
1926
|
+
node = figma.createText();
|
|
1927
|
+
break;
|
|
1928
|
+
case "rect":
|
|
1929
|
+
node = figma.createRectangle();
|
|
1930
|
+
break;
|
|
1931
|
+
case "ellipse":
|
|
1932
|
+
node = figma.createEllipse();
|
|
1933
|
+
break;
|
|
1934
|
+
case "line":
|
|
1935
|
+
node = figma.createLine();
|
|
1936
|
+
break;
|
|
1937
|
+
case "svg":
|
|
1938
|
+
case "icon":
|
|
1939
|
+
if (!s.svg)
|
|
1940
|
+
throw codeError(path + ": icons must be created with the build tool (the server fetches the SVG)", "BAD_ARGS");
|
|
1941
|
+
node = figma.createNodeFromSvg(String(s.svg));
|
|
1942
|
+
break;
|
|
1943
|
+
case "image":
|
|
1944
|
+
node = figma.createRectangle();
|
|
1945
|
+
break;
|
|
1946
|
+
case "instance":
|
|
1947
|
+
node = await createInstance(s, path);
|
|
1948
|
+
break;
|
|
1949
|
+
case "componentset":
|
|
1950
|
+
node = await createComponentSet(s, parent, ctx, path);
|
|
1951
|
+
break;
|
|
1952
|
+
default:
|
|
1953
|
+
throw codeError(path + ': unknown type "' + s.type + '"', "BAD_ARGS");
|
|
1954
|
+
}
|
|
1955
|
+
ctx.count++;
|
|
1956
|
+
progress(ctx);
|
|
1957
|
+
parent.appendChild(node);
|
|
1958
|
+
const parentAuto = isAutoLayout(parent);
|
|
1959
|
+
if (s.bind)
|
|
1960
|
+
ctx.binds.push({ node, bind: s.bind, path });
|
|
1961
|
+
const bindStart = ctx.binds.length;
|
|
1962
|
+
if (s.name)
|
|
1963
|
+
node.name = String(s.name);
|
|
1964
|
+
else if (type === "icon")
|
|
1965
|
+
node.name = "icon/" + s.icon;
|
|
1966
|
+
if (type === "frame" || type === "component")
|
|
1967
|
+
await setupFrame(node, s, ctx, path);
|
|
1968
|
+
else if (type === "componentset")
|
|
1969
|
+
await setupLayout(node, Object.assign({ layout: "row", wrap: true, gap: 16, padding: 16 }, s), ctx, path);
|
|
1970
|
+
else if (type === "text")
|
|
1971
|
+
await setupText(node, s, ctx);
|
|
1972
|
+
else if (type === "image")
|
|
1973
|
+
await setupImage(node, s, ctx, path);
|
|
1974
|
+
else if (type === "line") {
|
|
1975
|
+
node.resize(num(s.w, s.width, 100), 0);
|
|
1976
|
+
if (s.stroke === undefined)
|
|
1977
|
+
node.strokes = [solid(ctx.defaults.color)];
|
|
1978
|
+
} else if ((type === "rect" || type === "ellipse") && s.w === undefined && s.width === undefined && s.size === undefined) {
|
|
1979
|
+
node.resize(100, 100);
|
|
1980
|
+
}
|
|
1981
|
+
if (type === "instance" && s.text && typeof s.text === "object")
|
|
1982
|
+
await overrideTexts(node, s.text, ctx);
|
|
1983
|
+
if (type === "componentset" && s.stroke === undefined) {
|
|
1984
|
+
const set = node;
|
|
1985
|
+
set.strokes = [solid("#9747FF")];
|
|
1986
|
+
set.dashPattern = [10, 5];
|
|
1987
|
+
set.cornerRadius = 5;
|
|
1988
|
+
}
|
|
1989
|
+
await applyVisuals(node, s, ctx, type);
|
|
1990
|
+
if (type === "text" && Array.isArray(s.spans))
|
|
1991
|
+
await applySpans(node, s, ctx, path);
|
|
1992
|
+
applySize(node, s, parentAuto, type, ctx, path);
|
|
1993
|
+
if (type === "component" && !s.variantOf)
|
|
1994
|
+
await addProperties(node, s.properties, ctx.binds.splice(bindStart), ctx, path);
|
|
1995
|
+
if (parent.layoutMode === "GRID")
|
|
1996
|
+
placeInGrid(node, s, ctx, path);
|
|
1997
|
+
if (s.absolute && parentAuto)
|
|
1998
|
+
node.layoutPositioning = "ABSOLUTE";
|
|
1999
|
+
if (!parentAuto || s.absolute) {
|
|
2000
|
+
if (typeof s.x === "number")
|
|
2001
|
+
node.x = s.x;
|
|
2002
|
+
if (typeof s.y === "number")
|
|
2003
|
+
node.y = s.y;
|
|
2004
|
+
}
|
|
2005
|
+
if (s.grow && parentAuto)
|
|
2006
|
+
node.layoutGrow = 1;
|
|
2007
|
+
if (typeof s.rotation === "number")
|
|
2008
|
+
node.rotation = s.rotation;
|
|
2009
|
+
if (s.visible === false)
|
|
2010
|
+
node.visible = false;
|
|
2011
|
+
if (s.locked)
|
|
2012
|
+
node.locked = true;
|
|
2013
|
+
if (s.modes)
|
|
2014
|
+
await applyModes(node, s.modes, ctx.warnings, path);
|
|
2015
|
+
if (s.reactions)
|
|
2016
|
+
ctx.reactions.push({ node, reactions: s.reactions, path });
|
|
2017
|
+
if (s.name) {
|
|
2018
|
+
let key = String(s.name);
|
|
2019
|
+
for (let n = 2;ctx.ids[key]; n++)
|
|
2020
|
+
key = s.name + " #" + n;
|
|
2021
|
+
if (Object.keys(ctx.ids).length < 300)
|
|
2022
|
+
ctx.ids[key] = node.id;
|
|
2023
|
+
}
|
|
2024
|
+
return node;
|
|
2025
|
+
}
|
|
2026
|
+
async function setupFrame(f, s, ctx, path) {
|
|
2027
|
+
f.fills = [];
|
|
2028
|
+
await setupLayout(f, s, ctx, path);
|
|
2029
|
+
const children = Array.isArray(s.children) ? s.children : [];
|
|
2030
|
+
for (let i = 0;i < children.length; i++) {
|
|
2031
|
+
try {
|
|
2032
|
+
await createNode(children[i], f, ctx, path + ".children[" + i + "]");
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
if (e.code === "TOO_LARGE" || e.code === "CANCELLED")
|
|
2035
|
+
throw e;
|
|
2036
|
+
ctx.warnings.push(path + ".children[" + i + "]: " + (e.message || e));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
async function setupLayout(f, s, ctx, path) {
|
|
2041
|
+
const layout = String(s.layout || s.direction || "").toLowerCase();
|
|
2042
|
+
let mode = layout === "row" || layout === "horizontal" ? "HORIZONTAL" : layout === "column" || layout === "col" || layout === "vertical" ? "VERTICAL" : layout === "grid" ? "GRID" : "NONE";
|
|
2043
|
+
f.clipsContent = !!s.clip;
|
|
2044
|
+
if (mode === "GRID") {
|
|
2045
|
+
if (await setupGrid(f, s, ctx, path))
|
|
2046
|
+
return;
|
|
2047
|
+
mode = "HORIZONTAL";
|
|
2048
|
+
s = Object.assign({}, s, { wrap: true, rowGap: s.rowGap !== undefined ? s.rowGap : s.gap, gap: s.columnGap !== undefined ? s.columnGap : s.gap });
|
|
2049
|
+
}
|
|
2050
|
+
if (mode !== "NONE") {
|
|
2051
|
+
f.layoutMode = mode;
|
|
2052
|
+
f.primaryAxisSizingMode = "AUTO";
|
|
2053
|
+
f.counterAxisSizingMode = "AUTO";
|
|
2054
|
+
if (s.gap !== undefined) {
|
|
2055
|
+
if (s.gap === "auto")
|
|
2056
|
+
f.primaryAxisAlignItems = "SPACE_BETWEEN";
|
|
2057
|
+
else
|
|
2058
|
+
await setNumber(f, "itemSpacing", s.gap);
|
|
2059
|
+
}
|
|
2060
|
+
if (s.padding !== undefined)
|
|
2061
|
+
await setPadding(f, s.padding);
|
|
2062
|
+
const justify = { start: "MIN", center: "CENTER", end: "MAX", between: "SPACE_BETWEEN" };
|
|
2063
|
+
const align = { start: "MIN", center: "CENTER", end: "MAX", baseline: "BASELINE" };
|
|
2064
|
+
if (s.justify && justify[s.justify])
|
|
2065
|
+
f.primaryAxisAlignItems = justify[s.justify];
|
|
2066
|
+
if (s.align && align[s.align])
|
|
2067
|
+
f.counterAxisAlignItems = align[s.align];
|
|
2068
|
+
if (s.wrap && mode === "HORIZONTAL") {
|
|
2069
|
+
f.layoutWrap = "WRAP";
|
|
2070
|
+
if (s.rowGap !== undefined)
|
|
2071
|
+
await setNumber(f, "counterAxisSpacing", s.rowGap);
|
|
2072
|
+
}
|
|
2073
|
+
} else if (s.w === undefined && s.width === undefined && s.size === undefined) {
|
|
2074
|
+
f.resize(100, 100);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
function track(v, fallback) {
|
|
2078
|
+
if (typeof v === "number")
|
|
2079
|
+
return { type: "FIXED", value: v };
|
|
2080
|
+
const str = String(v === undefined ? "" : v).toLowerCase();
|
|
2081
|
+
if (str === "hug" || str === "auto")
|
|
2082
|
+
return { type: "HUG" };
|
|
2083
|
+
const fr = /^([\d.]+)fr$/.exec(str);
|
|
2084
|
+
if (fr)
|
|
2085
|
+
return { type: "FLEX", value: parseFloat(fr[1]) };
|
|
2086
|
+
const px = /^([\d.]+)(px)?$/.exec(str);
|
|
2087
|
+
if (px)
|
|
2088
|
+
return { type: "FIXED", value: parseFloat(px[1]) };
|
|
2089
|
+
return fallback === "FLEX" ? { type: "FLEX", value: 1 } : { type: "HUG" };
|
|
2090
|
+
}
|
|
2091
|
+
async function setupGrid(f, s, ctx, path) {
|
|
2092
|
+
try {
|
|
2093
|
+
f.layoutMode = "GRID";
|
|
2094
|
+
} catch (e) {
|
|
2095
|
+
ctx.warnings.push(path + ": grid auto-layout is not available in this Figma version, used a wrapping row");
|
|
2096
|
+
return false;
|
|
2097
|
+
}
|
|
2098
|
+
const kids = Array.isArray(s.children) ? s.children : [];
|
|
2099
|
+
const cols = Array.isArray(s.columns) ? s.columns : null;
|
|
2100
|
+
const rows = Array.isArray(s.rows) ? s.rows : null;
|
|
2101
|
+
const colCount = Math.max(1, cols ? cols.length : Number(s.columns) || 2);
|
|
2102
|
+
let cells = 0;
|
|
2103
|
+
for (let i = 0;i < kids.length; i++)
|
|
2104
|
+
cells += spanOf(kids[i], "col") * spanOf(kids[i], "row");
|
|
2105
|
+
const rowCount = Math.max(1, rows ? rows.length : Number(s.rows) || Math.ceil(cells / colCount));
|
|
2106
|
+
const fixedW = typeof s.w === "number" || typeof s.width === "number" || s.w === "fill";
|
|
2107
|
+
const fixedH = typeof s.h === "number" || typeof s.height === "number" || s.h === "fill";
|
|
2108
|
+
const set = function(what, fn) {
|
|
2109
|
+
try {
|
|
2110
|
+
fn();
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
ctx.warnings.push(path + ": grid " + what + ": " + (e.message || e));
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
set("columns", function() {
|
|
2116
|
+
f.gridColumnCount = colCount;
|
|
2117
|
+
});
|
|
2118
|
+
set("rows", function() {
|
|
2119
|
+
f.gridRowCount = rowCount;
|
|
2120
|
+
});
|
|
2121
|
+
set("column sizes", function() {
|
|
2122
|
+
for (let i = 0;i < colCount; i++)
|
|
2123
|
+
applyTrack(f.gridColumnSizes[i], track(cols ? cols[i] : undefined, fixedW ? "FLEX" : "HUG"));
|
|
2124
|
+
});
|
|
2125
|
+
set("row sizes", function() {
|
|
2126
|
+
for (let i = 0;i < rowCount; i++)
|
|
2127
|
+
applyTrack(f.gridRowSizes[i], track(rows ? rows[i] : undefined, fixedH ? "FLEX" : "HUG"));
|
|
2128
|
+
});
|
|
2129
|
+
if (!fixedW)
|
|
2130
|
+
set("sizing", function() {
|
|
2131
|
+
f.layoutSizingHorizontal = "HUG";
|
|
2132
|
+
});
|
|
2133
|
+
if (!fixedH)
|
|
2134
|
+
set("sizing", function() {
|
|
2135
|
+
f.layoutSizingVertical = "HUG";
|
|
2136
|
+
});
|
|
2137
|
+
if ("gridItemsPositioning" in f)
|
|
2138
|
+
set("auto flow", function() {
|
|
2139
|
+
f.gridItemsPositioning = "ROW_AUTO_FLOW";
|
|
2140
|
+
});
|
|
2141
|
+
const colGap = s.columnGap !== undefined ? s.columnGap : s.gap;
|
|
2142
|
+
const rowGap = s.rowGap !== undefined ? s.rowGap : s.gap;
|
|
2143
|
+
if (colGap !== undefined && colGap !== "auto")
|
|
2144
|
+
await setNumber(f, "gridColumnGap", colGap);
|
|
2145
|
+
if (rowGap !== undefined && rowGap !== "auto")
|
|
2146
|
+
await setNumber(f, "gridRowGap", rowGap);
|
|
2147
|
+
if (s.padding !== undefined)
|
|
2148
|
+
await setPadding(f, s.padding);
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
function applyTrack(t, spec) {
|
|
2152
|
+
if (!t)
|
|
2153
|
+
return;
|
|
2154
|
+
t.type = spec.type;
|
|
2155
|
+
if (spec.value !== undefined && spec.type !== "HUG")
|
|
2156
|
+
t.value = spec.value;
|
|
2157
|
+
}
|
|
2158
|
+
function spanOf(s, axis) {
|
|
2159
|
+
if (!s || typeof s !== "object")
|
|
2160
|
+
return 1;
|
|
2161
|
+
if (Array.isArray(s.span))
|
|
2162
|
+
return Math.max(1, Number(axis === "row" ? s.span[0] : s.span[1]) || 1);
|
|
2163
|
+
const v = axis === "col" ? s.colSpan !== undefined ? s.colSpan : s.span : s.rowSpan;
|
|
2164
|
+
return Math.max(1, Number(v) || 1);
|
|
2165
|
+
}
|
|
2166
|
+
function placeInGrid(node, s, ctx, path) {
|
|
2167
|
+
try {
|
|
2168
|
+
if (spanOf(s, "col") > 1)
|
|
2169
|
+
node.gridColumnSpan = spanOf(s, "col");
|
|
2170
|
+
if (spanOf(s, "row") > 1)
|
|
2171
|
+
node.gridRowSpan = spanOf(s, "row");
|
|
2172
|
+
const align = { start: "MIN", center: "CENTER", end: "MAX" };
|
|
2173
|
+
if (s.cellAlign && align[s.cellAlign])
|
|
2174
|
+
node.gridChildHorizontalAlign = align[s.cellAlign];
|
|
2175
|
+
if (s.cellValign && align[s.cellValign])
|
|
2176
|
+
node.gridChildVerticalAlign = align[s.cellValign];
|
|
2177
|
+
} catch (e) {
|
|
2178
|
+
ctx.warnings.push(path + ": grid span: " + (e.message || e));
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async function setupText(t, s, ctx) {
|
|
2182
|
+
const wanted = fontKey(s, ctx);
|
|
2183
|
+
t.fontName = ctx.fonts[wanted.family + ":" + wanted.style] || { family: "Inter", style: "Regular" };
|
|
2184
|
+
const spans = Array.isArray(s.spans) ? s.spans : [];
|
|
2185
|
+
t.characters = spans.length ? spans.map(function(sp) {
|
|
2186
|
+
return sp && sp.text !== undefined ? String(sp.text) : "";
|
|
2187
|
+
}).join("") : String(s.text === undefined ? "" : s.text);
|
|
2188
|
+
t.fontSize = typeof s.size === "number" ? s.size : ctx.defaults.size;
|
|
2189
|
+
if (s.lineHeight !== undefined)
|
|
2190
|
+
t.lineHeight = lineHeight(s.lineHeight);
|
|
2191
|
+
if (s.letterSpacing !== undefined) {
|
|
2192
|
+
const ls = String(s.letterSpacing);
|
|
2193
|
+
t.letterSpacing = /%$/.test(ls) ? { value: parseFloat(ls), unit: "PERCENT" } : { value: Number(s.letterSpacing), unit: "PIXELS" };
|
|
2194
|
+
}
|
|
2195
|
+
const halign = { left: "LEFT", center: "CENTER", right: "RIGHT", justify: "JUSTIFIED" };
|
|
2196
|
+
if (s.align && halign[s.align])
|
|
2197
|
+
t.textAlignHorizontal = halign[s.align];
|
|
2198
|
+
const valign = { top: "TOP", center: "CENTER", bottom: "BOTTOM" };
|
|
2199
|
+
if (s.valign && valign[s.valign])
|
|
2200
|
+
t.textAlignVertical = valign[s.valign];
|
|
2201
|
+
if (s.decoration === "underline")
|
|
2202
|
+
t.textDecoration = "UNDERLINE";
|
|
2203
|
+
if (s.decoration === "strike" || s.decoration === "strikethrough")
|
|
2204
|
+
t.textDecoration = "STRIKETHROUGH";
|
|
2205
|
+
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE" };
|
|
2206
|
+
if (s.case && cases[s.case])
|
|
2207
|
+
t.textCase = cases[s.case];
|
|
2208
|
+
if (typeof s.maxLines === "number") {
|
|
2209
|
+
t.textTruncation = "ENDING";
|
|
2210
|
+
t.maxLines = s.maxLines;
|
|
2211
|
+
}
|
|
2212
|
+
if (s.fill === undefined && s.color === undefined && !s.textStyle)
|
|
2213
|
+
t.fills = [solid(ctx.defaults.color)];
|
|
2214
|
+
if (s.textStyle) {
|
|
2215
|
+
const style = await findStyle("text", stripPrefix(s.textStyle));
|
|
2216
|
+
await loadFont(style.fontName);
|
|
2217
|
+
await t.setTextStyleIdAsync(style.id);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
async function applySpans(t, s, ctx, path) {
|
|
2221
|
+
let at = 0;
|
|
2222
|
+
for (let i = 0;i < s.spans.length; i++) {
|
|
2223
|
+
const sp = s.spans[i] || {};
|
|
2224
|
+
const end = at + String(sp.text === undefined ? "" : sp.text).length;
|
|
2225
|
+
if (end === at)
|
|
2226
|
+
continue;
|
|
2227
|
+
try {
|
|
2228
|
+
const f = spanFont(s, sp, ctx);
|
|
2229
|
+
if (f)
|
|
2230
|
+
t.setRangeFontName(at, end, ctx.fonts[f.family + ":" + f.style] || f);
|
|
2231
|
+
if (typeof sp.size === "number")
|
|
2232
|
+
t.setRangeFontSize(at, end, sp.size);
|
|
2233
|
+
const color = sp.color !== undefined ? sp.color : sp.fill;
|
|
2234
|
+
if (typeof color === "string" && color.indexOf("style:") === 0) {
|
|
2235
|
+
await t.setRangeFillStyleIdAsync(at, end, (await findStyle("paint", stripPrefix(color))).id);
|
|
2236
|
+
} else if (color !== undefined) {
|
|
2237
|
+
t.setRangeFills(at, end, [await toPaint(color, ctx.images)]);
|
|
2238
|
+
}
|
|
2239
|
+
const deco = { underline: "UNDERLINE", strike: "STRIKETHROUGH", strikethrough: "STRIKETHROUGH", none: "NONE" };
|
|
2240
|
+
if (sp.link) {
|
|
2241
|
+
t.setRangeHyperlink(at, end, { type: "URL", value: String(sp.link) });
|
|
2242
|
+
if (sp.decoration === undefined)
|
|
2243
|
+
t.setRangeTextDecoration(at, end, "UNDERLINE");
|
|
2244
|
+
}
|
|
2245
|
+
if (sp.decoration && deco[sp.decoration])
|
|
2246
|
+
t.setRangeTextDecoration(at, end, deco[sp.decoration]);
|
|
2247
|
+
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE", none: "ORIGINAL" };
|
|
2248
|
+
if (sp.case && cases[sp.case])
|
|
2249
|
+
t.setRangeTextCase(at, end, cases[sp.case]);
|
|
2250
|
+
} catch (e) {
|
|
2251
|
+
ctx.warnings.push(path + ".spans[" + i + "]: " + (e.message || e));
|
|
2252
|
+
}
|
|
2253
|
+
at = end;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
async function setupImage(r, s, ctx, path) {
|
|
2257
|
+
const bytes = ctx.images[s.imageKey];
|
|
2258
|
+
if (!bytes)
|
|
2259
|
+
throw codeError(path + ": image data missing (use `src` with the build tool)", "BAD_ARGS");
|
|
2260
|
+
const image = figma.createImage(bytes);
|
|
2261
|
+
const size = await image.getSizeAsync();
|
|
2262
|
+
let w = num(s.w, s.width, 0);
|
|
2263
|
+
let h = num(s.h, s.height, 0);
|
|
2264
|
+
if (!w && !h) {
|
|
2265
|
+
w = size.width;
|
|
2266
|
+
h = size.height;
|
|
2267
|
+
} else if (!h)
|
|
2268
|
+
h = w * size.height / size.width;
|
|
2269
|
+
else if (!w)
|
|
2270
|
+
w = h * size.width / size.height;
|
|
2271
|
+
r.resize(Math.max(1, w), Math.max(1, h));
|
|
2272
|
+
const modes = { fill: "FILL", fit: "FIT", crop: "CROP", tile: "TILE" };
|
|
2273
|
+
r.fills = [{ type: "IMAGE", imageHash: image.hash, scaleMode: modes[String(s.fit || "fill").toLowerCase()] || "FILL" }];
|
|
2274
|
+
if (!s.name)
|
|
2275
|
+
r.name = "Image";
|
|
2276
|
+
}
|
|
2277
|
+
async function findComponent(ref, path) {
|
|
2278
|
+
let comp = null;
|
|
2279
|
+
if (/^[\dI;:]+$/.test(ref)) {
|
|
2280
|
+
const n = await figma.getNodeByIdAsync(ref);
|
|
2281
|
+
if (n && (n.type === "COMPONENT" || n.type === "COMPONENT_SET"))
|
|
2282
|
+
comp = n;
|
|
2283
|
+
} else if (/^[0-9a-f]{40}$/i.test(ref)) {
|
|
2284
|
+
comp = await figma.importComponentByKeyAsync(ref);
|
|
2285
|
+
} else {
|
|
2286
|
+
const list = await localComponents();
|
|
2287
|
+
for (let i = 0;i < list.length && !comp; i++)
|
|
2288
|
+
if (list[i].name === ref)
|
|
2289
|
+
comp = list[i];
|
|
2290
|
+
for (let i = 0;i < list.length && !comp; i++)
|
|
2291
|
+
if (list[i].name.toLowerCase() === ref.toLowerCase())
|
|
2292
|
+
comp = list[i];
|
|
2293
|
+
}
|
|
2294
|
+
if (!comp)
|
|
2295
|
+
throw codeError(path + ': component "' + ref + '" not found. Use get_design_system to list components.', "NOT_FOUND");
|
|
2296
|
+
return comp;
|
|
2297
|
+
}
|
|
2298
|
+
async function createInstance(s, path) {
|
|
2299
|
+
const comp = await findComponent(String(s.component), path);
|
|
2300
|
+
const main = comp.type === "COMPONENT_SET" ? comp.defaultVariant : comp;
|
|
2301
|
+
const inst = main.createInstance();
|
|
2302
|
+
if (s.props && typeof s.props === "object") {
|
|
2303
|
+
const defs = inst.componentProperties;
|
|
2304
|
+
const out = {};
|
|
2305
|
+
const keys = Object.keys(s.props);
|
|
2306
|
+
for (let i = 0;i < keys.length; i++) {
|
|
2307
|
+
const k = keys[i];
|
|
2308
|
+
let real = defs[k] ? k : "";
|
|
2309
|
+
if (!real) {
|
|
2310
|
+
const all = Object.keys(defs);
|
|
2311
|
+
for (let j = 0;j < all.length && !real; j++)
|
|
2312
|
+
if (all[j].split("#")[0] === k)
|
|
2313
|
+
real = all[j];
|
|
2314
|
+
}
|
|
2315
|
+
if (real)
|
|
2316
|
+
out[real] = s.props[k];
|
|
2317
|
+
}
|
|
2318
|
+
if (Object.keys(out).length)
|
|
2319
|
+
inst.setProperties(out);
|
|
2320
|
+
}
|
|
2321
|
+
return inst;
|
|
2322
|
+
}
|
|
2323
|
+
async function createComponentSet(s, parent, ctx, path) {
|
|
2324
|
+
const variants = Array.isArray(s.variants) ? s.variants : [];
|
|
2325
|
+
if (!variants.length)
|
|
2326
|
+
throw codeError(path + ': a componentSet needs variants:[{props:{Variant:"Primary"}, ...}]', "BAD_ARGS");
|
|
2327
|
+
const start = ctx.binds.length;
|
|
2328
|
+
const comps = [];
|
|
2329
|
+
for (let i = 0;i < variants.length; i++) {
|
|
2330
|
+
const v = Object.assign({}, s.base || {}, variants[i] || {});
|
|
2331
|
+
const props = v.props || {};
|
|
2332
|
+
const keys = Object.keys(props);
|
|
2333
|
+
if (!keys.length)
|
|
2334
|
+
throw codeError(path + ".variants[" + i + ']: props:{Name:"value"} is required', "BAD_ARGS");
|
|
2335
|
+
const spec = Object.assign({}, v, {
|
|
2336
|
+
type: "component",
|
|
2337
|
+
variantOf: true,
|
|
2338
|
+
name: keys.map(function(k) {
|
|
2339
|
+
return k + "=" + props[k];
|
|
2340
|
+
}).join(", ")
|
|
2341
|
+
});
|
|
2342
|
+
delete spec.props;
|
|
2343
|
+
comps.push(await createNode(spec, parent, ctx, path + ".variants[" + i + "]"));
|
|
2344
|
+
}
|
|
2345
|
+
const set = figma.combineAsVariants(comps, parent);
|
|
2346
|
+
set.name = String(s.name || "Component");
|
|
2347
|
+
await addProperties(set, s.properties, ctx.binds.splice(start), ctx, path);
|
|
2348
|
+
return set;
|
|
2349
|
+
}
|
|
2350
|
+
async function addProperties(owner, properties, binds, ctx, path) {
|
|
2351
|
+
const keys = {};
|
|
2352
|
+
const types = {};
|
|
2353
|
+
const props = properties && typeof properties === "object" ? properties : {};
|
|
2354
|
+
const names = Object.keys(props);
|
|
2355
|
+
for (let i = 0;i < names.length; i++) {
|
|
2356
|
+
const name = names[i];
|
|
2357
|
+
let def = props[name];
|
|
2358
|
+
if (typeof def === "string")
|
|
2359
|
+
def = { type: "text", default: def };
|
|
2360
|
+
else if (typeof def === "boolean")
|
|
2361
|
+
def = { type: "boolean", default: def };
|
|
2362
|
+
const t = String(def.type || "text").toLowerCase();
|
|
2363
|
+
try {
|
|
2364
|
+
if (t === "boolean" || t === "bool") {
|
|
2365
|
+
keys[name] = owner.addComponentProperty(name, "BOOLEAN", def.default !== false);
|
|
2366
|
+
types[name] = "BOOLEAN";
|
|
2367
|
+
} else if (t === "instance" || t === "instance_swap" || t === "swap") {
|
|
2368
|
+
const comp = await findComponent(String(def.default), path + ".properties." + name);
|
|
2369
|
+
const main = comp.type === "COMPONENT_SET" ? comp.defaultVariant : comp;
|
|
2370
|
+
const preferred = [];
|
|
2371
|
+
const list = Array.isArray(def.preferred) ? def.preferred : [];
|
|
2372
|
+
for (let k = 0;k < list.length; k++) {
|
|
2373
|
+
const p = await findComponent(String(list[k]), path + ".properties." + name);
|
|
2374
|
+
preferred.push({ type: p.type === "COMPONENT_SET" ? "COMPONENT_SET" : "COMPONENT", key: p.key });
|
|
2375
|
+
}
|
|
2376
|
+
keys[name] = owner.addComponentProperty(name, "INSTANCE_SWAP", main.id, preferred.length ? { preferredValues: preferred } : undefined);
|
|
2377
|
+
types[name] = "INSTANCE_SWAP";
|
|
2378
|
+
} else {
|
|
2379
|
+
keys[name] = owner.addComponentProperty(name, "TEXT", String(def.default === undefined ? "" : def.default));
|
|
2380
|
+
types[name] = "TEXT";
|
|
2381
|
+
}
|
|
2382
|
+
} catch (e) {
|
|
2383
|
+
ctx.warnings.push(path + ".properties." + name + ": " + (e.message || e));
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
for (let i = 0;i < binds.length; i++) {
|
|
2387
|
+
const b = binds[i];
|
|
2388
|
+
const map = typeof b.bind === "string" ? { auto: b.bind } : b.bind;
|
|
2389
|
+
const refs = {};
|
|
2390
|
+
const fields = Object.keys(map || {});
|
|
2391
|
+
for (let k = 0;k < fields.length; k++) {
|
|
2392
|
+
const prop = String(map[fields[k]]);
|
|
2393
|
+
let field = fields[k];
|
|
2394
|
+
if (!keys[prop] && b.node.type === "TEXT" && (field === "auto" || field === "characters")) {
|
|
2395
|
+
keys[prop] = owner.addComponentProperty(prop, "TEXT", b.node.characters);
|
|
2396
|
+
types[prop] = "TEXT";
|
|
2397
|
+
}
|
|
2398
|
+
if (!keys[prop]) {
|
|
2399
|
+
ctx.warnings.push(b.path + ': no component property "' + prop + '" (declare it in properties)');
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
if (field === "auto")
|
|
2403
|
+
field = types[prop] === "TEXT" ? "characters" : types[prop] === "BOOLEAN" ? "visible" : "mainComponent";
|
|
2404
|
+
refs[field] = keys[prop];
|
|
2405
|
+
}
|
|
2406
|
+
if (!Object.keys(refs).length)
|
|
2407
|
+
continue;
|
|
2408
|
+
try {
|
|
2409
|
+
b.node.componentPropertyReferences = Object.assign({}, b.node.componentPropertyReferences || {}, refs);
|
|
2410
|
+
} catch (e) {
|
|
2411
|
+
ctx.warnings.push(b.path + ": bind: " + (e.message || e));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
async function overrideTexts(inst, texts, ctx) {
|
|
2416
|
+
const names = Object.keys(texts);
|
|
2417
|
+
for (let i = 0;i < names.length; i++) {
|
|
2418
|
+
const t = inst.findOne(function(n) {
|
|
2419
|
+
return n.type === "TEXT" && n.name === names[i];
|
|
2420
|
+
});
|
|
2421
|
+
if (!t) {
|
|
2422
|
+
ctx.warnings.push('No text layer "' + names[i] + '" in instance "' + inst.name + '"');
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
const fonts = t.characters.length ? t.getRangeAllFontNames(0, t.characters.length) : [t.fontName];
|
|
2426
|
+
await Promise.all(fonts.map(loadFont));
|
|
2427
|
+
t.characters = String(texts[names[i]]);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
async function applyVisuals(node, s, ctx, type) {
|
|
2431
|
+
const fill = s.fill !== undefined ? s.fill : type === "text" ? s.color : undefined;
|
|
2432
|
+
if (fill !== undefined && "fills" in node && type !== "image")
|
|
2433
|
+
await setPaints(node, "fills", fill, ctx.images);
|
|
2434
|
+
if (s.stroke !== undefined && "strokes" in node) {
|
|
2435
|
+
await setPaints(node, "strokes", s.stroke, ctx.images);
|
|
2436
|
+
if (Array.isArray(s.strokeWidth) && "strokeTopWeight" in node) {
|
|
2437
|
+
const w = s.strokeWidth.map(Number);
|
|
2438
|
+
node.strokeTopWeight = w[0] || 0;
|
|
2439
|
+
node.strokeRightWeight = w[1] === undefined ? w[0] || 0 : w[1] || 0;
|
|
2440
|
+
node.strokeBottomWeight = w[2] === undefined ? w[0] || 0 : w[2] || 0;
|
|
2441
|
+
node.strokeLeftWeight = w[3] === undefined ? w[1] === undefined ? w[0] || 0 : w[1] || 0 : w[3] || 0;
|
|
2442
|
+
} else {
|
|
2443
|
+
node.strokeWeight = typeof s.strokeWidth === "number" ? s.strokeWidth : 1;
|
|
2444
|
+
}
|
|
2445
|
+
if (Array.isArray(s.strokeDash) && "dashPattern" in node)
|
|
2446
|
+
node.dashPattern = s.strokeDash.map(Number);
|
|
2447
|
+
if ("strokeAlign" in node && type !== "line" && type !== "text") {
|
|
2448
|
+
node.strokeAlign = String(s.strokeAlign || "inside").toUpperCase();
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (s.radius !== undefined && "cornerRadius" in node) {
|
|
2452
|
+
if (Array.isArray(s.radius)) {
|
|
2453
|
+
const r = s.radius;
|
|
2454
|
+
await setNumber(node, "topLeftRadius", r[0]);
|
|
2455
|
+
await setNumber(node, "topRightRadius", r[1] === undefined ? r[0] : r[1]);
|
|
2456
|
+
await setNumber(node, "bottomRightRadius", r[2] === undefined ? r[0] : r[2]);
|
|
2457
|
+
await setNumber(node, "bottomLeftRadius", r[3] === undefined ? r[1] === undefined ? r[0] : r[1] : r[3]);
|
|
2458
|
+
} else if (typeof s.radius === "string" && s.radius.indexOf("var:") === 0) {
|
|
2459
|
+
const fields = ["topLeftRadius", "topRightRadius", "bottomRightRadius", "bottomLeftRadius"];
|
|
2460
|
+
for (let i = 0;i < fields.length; i++)
|
|
2461
|
+
await setNumber(node, fields[i], s.radius);
|
|
2462
|
+
} else {
|
|
2463
|
+
node.cornerRadius = Number(s.radius);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (typeof s.opacity === "number")
|
|
2467
|
+
node.opacity = s.opacity;
|
|
2468
|
+
if ("effects" in node && (s.shadow !== undefined || s.blur !== undefined)) {
|
|
2469
|
+
if (typeof s.shadow === "string" && s.shadow.indexOf("style:") === 0) {
|
|
2470
|
+
const style = await findStyle("effect", stripPrefix(s.shadow));
|
|
2471
|
+
await node.setEffectStyleIdAsync(style.id);
|
|
2472
|
+
} else {
|
|
2473
|
+
const effects = [];
|
|
2474
|
+
const shadows = s.shadow === true ? [SHADOW_DEFAULT] : Array.isArray(s.shadow) ? s.shadow : s.shadow ? [s.shadow] : [];
|
|
2475
|
+
for (let i = 0;i < shadows.length; i++)
|
|
2476
|
+
effects.push(shadowEffect(shadows[i]));
|
|
2477
|
+
if (typeof s.blur === "number")
|
|
2478
|
+
effects.push({ type: "LAYER_BLUR", radius: s.blur, visible: true });
|
|
2479
|
+
if (typeof s.backgroundBlur === "number")
|
|
2480
|
+
effects.push({ type: "BACKGROUND_BLUR", radius: s.backgroundBlur, visible: true });
|
|
2481
|
+
node.effects = effects;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
function shadowEffect(sh) {
|
|
2486
|
+
const c = parseHex(sh.color || SHADOW_DEFAULT.color);
|
|
2487
|
+
return {
|
|
2488
|
+
type: sh.inner ? "INNER_SHADOW" : "DROP_SHADOW",
|
|
2489
|
+
color: { r: c.r, g: c.g, b: c.b, a: c.a },
|
|
2490
|
+
offset: { x: num(sh.x, undefined, 0), y: num(sh.y, undefined, 4) },
|
|
2491
|
+
radius: num(sh.blur, undefined, 16),
|
|
2492
|
+
spread: num(sh.spread, undefined, 0),
|
|
2493
|
+
visible: true,
|
|
2494
|
+
blendMode: "NORMAL"
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
function stripPrefix(v) {
|
|
2498
|
+
return String(v).replace(/^(style|var):/, "");
|
|
2499
|
+
}
|
|
2500
|
+
function solid(hex) {
|
|
2501
|
+
const c = parseHex(hex);
|
|
2502
|
+
return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
|
|
2503
|
+
}
|
|
2504
|
+
async function setPaints(node, field, value, images) {
|
|
2505
|
+
if (typeof value === "string" && value.indexOf("style:") === 0) {
|
|
2506
|
+
const style = await findStyle("paint", stripPrefix(value));
|
|
2507
|
+
if (field === "fills")
|
|
2508
|
+
await node.setFillStyleIdAsync(style.id);
|
|
2509
|
+
else
|
|
2510
|
+
await node.setStrokeStyleIdAsync(style.id);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const list = value === null || value === "none" ? [] : Array.isArray(value) ? value : [value];
|
|
2514
|
+
const paints = [];
|
|
2515
|
+
for (let i = 0;i < list.length; i++)
|
|
2516
|
+
paints.push(await toPaint(list[i], images));
|
|
2517
|
+
node[field] = paints;
|
|
2518
|
+
}
|
|
2519
|
+
var FITS = { fill: "FILL", cover: "FILL", fit: "FIT", contain: "FIT", crop: "CROP", tile: "TILE" };
|
|
2520
|
+
async function toPaint(v, images) {
|
|
2521
|
+
if (typeof v === "string") {
|
|
2522
|
+
if (v.indexOf("var:") === 0) {
|
|
2523
|
+
const variable = await findVariable(stripPrefix(v));
|
|
2524
|
+
return figma.variables.setBoundVariableForPaint(solid("#000000"), "color", variable);
|
|
2525
|
+
}
|
|
2526
|
+
return solid(v);
|
|
2527
|
+
}
|
|
2528
|
+
if (v && Array.isArray(v.gradient)) {
|
|
2529
|
+
const stops = v.gradient.map(function(c, i, all) {
|
|
2530
|
+
const hex = typeof c === "string" ? c : c.color;
|
|
2531
|
+
const pos = typeof c === "object" && typeof c.at === "number" ? c.at : all.length === 1 ? 0 : i / (all.length - 1);
|
|
2532
|
+
const rgba = parseHex(hex);
|
|
2533
|
+
return { position: pos, color: { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a } };
|
|
2534
|
+
});
|
|
2535
|
+
const radial = v.type === "radial";
|
|
2536
|
+
const a = (typeof v.angle === "number" ? v.angle : 90) * Math.PI / 180;
|
|
2537
|
+
const cos = Math.cos(a);
|
|
2538
|
+
const sin = Math.sin(a);
|
|
2539
|
+
const transform = radial ? [
|
|
2540
|
+
[1, 0, 0],
|
|
2541
|
+
[0, 1, 0]
|
|
2542
|
+
] : [
|
|
2543
|
+
[cos, sin, 0.5 - 0.5 * cos - 0.5 * sin],
|
|
2544
|
+
[-sin, cos, 0.5 + 0.5 * sin - 0.5 * cos]
|
|
2545
|
+
];
|
|
2546
|
+
return { type: radial ? "GRADIENT_RADIAL" : "GRADIENT_LINEAR", gradientTransform: transform, gradientStops: stops };
|
|
2547
|
+
}
|
|
2548
|
+
if (v && v.imageKey !== undefined) {
|
|
2549
|
+
const bytes = images && images[v.imageKey];
|
|
2550
|
+
if (!bytes)
|
|
2551
|
+
throw codeError("Image fill data missing (use {image: src} with the build tool)", "BAD_ARGS");
|
|
2552
|
+
const paint = { type: "IMAGE", imageHash: figma.createImage(bytes).hash, scaleMode: FITS[String(v.fit || "fill").toLowerCase()] || "FILL" };
|
|
2553
|
+
if (typeof v.opacity === "number")
|
|
2554
|
+
return Object.assign({}, paint, { opacity: v.opacity });
|
|
2555
|
+
return paint;
|
|
2556
|
+
}
|
|
2557
|
+
if (v && typeof v.type === "string")
|
|
2558
|
+
return v;
|
|
2559
|
+
throw codeError("Invalid paint: " + JSON.stringify(v), "BAD_ARGS");
|
|
2560
|
+
}
|
|
2561
|
+
async function setNumber(node, field, value) {
|
|
2562
|
+
if (typeof value === "string" && value.indexOf("var:") === 0) {
|
|
2563
|
+
const variable = await findVariable(stripPrefix(value));
|
|
2564
|
+
node.setBoundVariable(field, variable);
|
|
2565
|
+
} else {
|
|
2566
|
+
node[field] = Number(value);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
async function setPadding(f, p) {
|
|
2570
|
+
const v = Array.isArray(p) ? p : [p];
|
|
2571
|
+
const top = v[0];
|
|
2572
|
+
const right = v[1] === undefined ? v[0] : v[1];
|
|
2573
|
+
const bottom = v[2] === undefined ? v[0] : v[2];
|
|
2574
|
+
const left = v[3] === undefined ? right : v[3];
|
|
2575
|
+
await setNumber(f, "paddingTop", top);
|
|
2576
|
+
await setNumber(f, "paddingRight", right);
|
|
2577
|
+
await setNumber(f, "paddingBottom", bottom);
|
|
2578
|
+
await setNumber(f, "paddingLeft", left);
|
|
2579
|
+
}
|
|
2580
|
+
function lineHeight(v) {
|
|
2581
|
+
if (v === "auto")
|
|
2582
|
+
return { unit: "AUTO" };
|
|
2583
|
+
const s = String(v);
|
|
2584
|
+
if (/%$/.test(s))
|
|
2585
|
+
return { value: parseFloat(s), unit: "PERCENT" };
|
|
2586
|
+
const n = Number(v);
|
|
2587
|
+
return n <= 4 ? { value: n * 100, unit: "PERCENT" } : { value: n, unit: "PIXELS" };
|
|
2588
|
+
}
|
|
2589
|
+
function num(a, b, fallback) {
|
|
2590
|
+
if (typeof a === "number")
|
|
2591
|
+
return a;
|
|
2592
|
+
if (typeof b === "number")
|
|
2593
|
+
return b;
|
|
2594
|
+
return fallback;
|
|
2595
|
+
}
|
|
2596
|
+
function sizing(v) {
|
|
2597
|
+
if (typeof v === "number")
|
|
2598
|
+
return "FIXED";
|
|
2599
|
+
if (v === "fill")
|
|
2600
|
+
return "FILL";
|
|
2601
|
+
if (v === "hug")
|
|
2602
|
+
return "HUG";
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
function applySize(node, s, parentAuto, type, ctx, path) {
|
|
2606
|
+
let w = s.w !== undefined ? s.w : s.width;
|
|
2607
|
+
let h = s.h !== undefined ? s.h : s.height;
|
|
2608
|
+
if (typeof s.size === "number" && type !== "text") {
|
|
2609
|
+
if (w === undefined)
|
|
2610
|
+
w = s.size;
|
|
2611
|
+
if (h === undefined)
|
|
2612
|
+
h = s.size;
|
|
2613
|
+
}
|
|
2614
|
+
if (typeof w === "number" || typeof h === "number") {
|
|
2615
|
+
if (type === "svg" || type === "icon") {
|
|
2616
|
+
const ratio = node.height ? node.width / node.height : 1;
|
|
2617
|
+
const nw = typeof w === "number" ? w : h * ratio;
|
|
2618
|
+
const nh = typeof h === "number" ? h : w / ratio;
|
|
2619
|
+
node.rescale(Math.min(nw / node.width, nh / node.height));
|
|
2620
|
+
} else if (type !== "line" && type !== "image") {
|
|
2621
|
+
node.resize(Math.max(0.01, typeof w === "number" ? w : node.width), Math.max(0.01, typeof h === "number" ? h : node.height));
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
if (type === "text") {
|
|
2625
|
+
const t = node;
|
|
2626
|
+
t.textAutoResize = typeof w === "number" || w === "fill" ? typeof h === "number" ? "NONE" : "HEIGHT" : "WIDTH_AND_HEIGHT";
|
|
2627
|
+
}
|
|
2628
|
+
if (!("layoutSizingHorizontal" in node))
|
|
2629
|
+
return;
|
|
2630
|
+
const hs = sizing(w);
|
|
2631
|
+
const vs = sizing(h);
|
|
2632
|
+
const trySet = function(field, value) {
|
|
2633
|
+
if (value === "FILL" && !parentAuto) {
|
|
2634
|
+
ctx.warnings.push(path + ': "fill" needs an auto-layout parent, ignored');
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
if (value === "HUG" && !isAutoLayout(node) && type !== "text")
|
|
2638
|
+
return;
|
|
2639
|
+
try {
|
|
2640
|
+
node[field] = value;
|
|
2641
|
+
} catch (e) {
|
|
2642
|
+
ctx.warnings.push(path + ": " + field + " " + value + " not applicable");
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
if (hs)
|
|
2646
|
+
trySet("layoutSizingHorizontal", hs);
|
|
2647
|
+
if (vs)
|
|
2648
|
+
trySet("layoutSizingVertical", vs);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// plugin/lib/checkpoint.ts
|
|
2652
|
+
var PAGE_NAME = "⟲ Bridge checkpoints";
|
|
2653
|
+
var KEY = "bridge-checkpoint";
|
|
2654
|
+
async function checkpointPage(create) {
|
|
2655
|
+
for (let i = 0;i < figma.root.children.length; i++) {
|
|
2656
|
+
const p = figma.root.children[i];
|
|
2657
|
+
if (p.getPluginData(KEY) === "page" || p.name === PAGE_NAME) {
|
|
2658
|
+
await p.loadAsync();
|
|
2659
|
+
return p;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
if (!create)
|
|
2663
|
+
return null;
|
|
2664
|
+
const page = figma.createPage();
|
|
2665
|
+
page.name = PAGE_NAME;
|
|
2666
|
+
page.setPluginData(KEY, "page");
|
|
2667
|
+
return page;
|
|
2668
|
+
}
|
|
2669
|
+
function holders(page) {
|
|
2670
|
+
return page.children.filter(function(n) {
|
|
2671
|
+
return n.type === "FRAME" && !!n.getPluginData(KEY);
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
function metaOf(f) {
|
|
2675
|
+
return JSON.parse(f.getPluginData(KEY));
|
|
2676
|
+
}
|
|
2677
|
+
async function checkpoint(p) {
|
|
2678
|
+
const action = String(p.action || "save");
|
|
2679
|
+
if (action === "save")
|
|
2680
|
+
return save(p);
|
|
2681
|
+
if (action === "list")
|
|
2682
|
+
return list2();
|
|
2683
|
+
if (action === "restore")
|
|
2684
|
+
return restore(p);
|
|
2685
|
+
if (action === "delete")
|
|
2686
|
+
return remove(p);
|
|
2687
|
+
throw codeError('Unknown checkpoint action "' + action + '" (save, list, restore, delete)', "BAD_ARGS");
|
|
2688
|
+
}
|
|
2689
|
+
async function save(p) {
|
|
2690
|
+
let nodes = [];
|
|
2691
|
+
if (Array.isArray(p.nodeIds) && p.nodeIds.length) {
|
|
2692
|
+
for (let i = 0;i < p.nodeIds.length; i++)
|
|
2693
|
+
nodes.push(await getNode(p.nodeIds[i]));
|
|
2694
|
+
} else {
|
|
2695
|
+
nodes = figma.currentPage.selection.slice();
|
|
2696
|
+
}
|
|
2697
|
+
if (!nodes.length)
|
|
2698
|
+
throw codeError("Nothing to save: pass nodeIds or select layers", "BAD_ARGS");
|
|
2699
|
+
const page = await checkpointPage(true);
|
|
2700
|
+
const meta = { id: String(Date.now().toString(36)), label: String(p.label || ""), at: Date.now() };
|
|
2701
|
+
const holder = figma.createFrame();
|
|
2702
|
+
page.appendChild(holder);
|
|
2703
|
+
holder.name = "Checkpoint " + meta.id + (meta.label ? " · " + meta.label : "");
|
|
2704
|
+
holder.fills = [];
|
|
2705
|
+
holder.clipsContent = false;
|
|
2706
|
+
holder.setPluginData(KEY, JSON.stringify(meta));
|
|
2707
|
+
const last = holders(page);
|
|
2708
|
+
holder.y = last.length > 1 ? last[last.length - 2].y + last[last.length - 2].height + 200 : 0;
|
|
2709
|
+
for (let i = 0;i < nodes.length; i++) {
|
|
2710
|
+
const n = nodes[i];
|
|
2711
|
+
const parent = n.parent;
|
|
2712
|
+
const item = { orig: n.id, parent: parent ? parent.id : "", index: parent ? parent.children.indexOf(n) : 0, x: n.x, y: n.y, name: n.name };
|
|
2713
|
+
const copy = n.clone();
|
|
2714
|
+
holder.appendChild(copy);
|
|
2715
|
+
copy.setPluginData(KEY, JSON.stringify(item));
|
|
2716
|
+
}
|
|
2717
|
+
return { checkpointId: meta.id, label: meta.label, saved: nodes.length };
|
|
2718
|
+
}
|
|
2719
|
+
async function list2() {
|
|
2720
|
+
const page = await checkpointPage(false);
|
|
2721
|
+
if (!page)
|
|
2722
|
+
return { checkpoints: [] };
|
|
2723
|
+
return {
|
|
2724
|
+
checkpoints: holders(page).map(function(h) {
|
|
2725
|
+
const m = metaOf(h);
|
|
2726
|
+
return {
|
|
2727
|
+
id: m.id,
|
|
2728
|
+
label: m.label,
|
|
2729
|
+
savedAt: new Date(m.at).toISOString(),
|
|
2730
|
+
layers: h.children.map(function(c) {
|
|
2731
|
+
return JSON.parse(c.getPluginData(KEY) || "{}").name || c.name;
|
|
2732
|
+
})
|
|
2733
|
+
};
|
|
2734
|
+
})
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
async function find(id) {
|
|
2738
|
+
const page = await checkpointPage(false);
|
|
2739
|
+
if (page) {
|
|
2740
|
+
const all = holders(page);
|
|
2741
|
+
if (id === "latest" && all.length)
|
|
2742
|
+
return { page, holder: all[all.length - 1] };
|
|
2743
|
+
for (let i = 0;i < all.length; i++)
|
|
2744
|
+
if (metaOf(all[i]).id === id)
|
|
2745
|
+
return { page, holder: all[i] };
|
|
2746
|
+
}
|
|
2747
|
+
throw codeError('Checkpoint "' + id + '" not found. Use checkpoint {action:"list"}.', "NOT_FOUND");
|
|
2748
|
+
}
|
|
2749
|
+
async function restore(p) {
|
|
2750
|
+
const found = await find(String(p.id || "latest"));
|
|
2751
|
+
const restored = {};
|
|
2752
|
+
const kids = found.holder.children.slice();
|
|
2753
|
+
for (let i = 0;i < kids.length; i++) {
|
|
2754
|
+
const copy = kids[i];
|
|
2755
|
+
const item = JSON.parse(copy.getPluginData(KEY) || "{}");
|
|
2756
|
+
const original = item.orig ? await figma.getNodeByIdAsync(item.orig) : null;
|
|
2757
|
+
let parent = item.parent ? await figma.getNodeByIdAsync(item.parent) : null;
|
|
2758
|
+
let index = item.index;
|
|
2759
|
+
if (original && original.parent && !original.removed) {
|
|
2760
|
+
parent = original.parent;
|
|
2761
|
+
index = parent.children.indexOf(original);
|
|
2762
|
+
original.remove();
|
|
2763
|
+
}
|
|
2764
|
+
if (!parent || !("insertChild" in parent))
|
|
2765
|
+
parent = figma.currentPage;
|
|
2766
|
+
const fresh = copy.clone();
|
|
2767
|
+
fresh.setPluginData(KEY, "");
|
|
2768
|
+
parent.insertChild(Math.max(0, Math.min(index, parent.children.length)), fresh);
|
|
2769
|
+
if (!("layoutMode" in parent) || parent.layoutMode === "NONE" || fresh.layoutPositioning === "ABSOLUTE") {
|
|
2770
|
+
fresh.x = item.x;
|
|
2771
|
+
fresh.y = item.y;
|
|
2772
|
+
}
|
|
2773
|
+
fresh.name = item.name || fresh.name;
|
|
2774
|
+
restored[item.orig] = fresh.id;
|
|
2775
|
+
}
|
|
2776
|
+
return { restored, note: "Restored layers have new ids (old → new above). The checkpoint is kept." };
|
|
2777
|
+
}
|
|
2778
|
+
async function remove(p) {
|
|
2779
|
+
if (p.id === "all") {
|
|
2780
|
+
const page = await checkpointPage(false);
|
|
2781
|
+
if (page) {
|
|
2782
|
+
if (figma.currentPage === page)
|
|
2783
|
+
await figma.setCurrentPageAsync(figma.root.children[0]);
|
|
2784
|
+
page.remove();
|
|
2785
|
+
}
|
|
2786
|
+
return { deleted: "all" };
|
|
2787
|
+
}
|
|
2788
|
+
const found = await find(String(p.id || ""));
|
|
2789
|
+
found.holder.remove();
|
|
2790
|
+
if (!holders(found.page).length && figma.root.children.length > 1) {
|
|
2791
|
+
if (figma.currentPage === found.page)
|
|
2792
|
+
await figma.setCurrentPageAsync(figma.root.children[0]);
|
|
2793
|
+
found.page.remove();
|
|
2794
|
+
}
|
|
2795
|
+
return { deleted: p.id };
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// plugin/lib/describe.ts
|
|
2799
|
+
async function describe(p) {
|
|
2800
|
+
let targets;
|
|
2801
|
+
if (p.nodeId)
|
|
2802
|
+
targets = [await getNode(p.nodeId)];
|
|
2803
|
+
else if (figma.currentPage.selection.length)
|
|
2804
|
+
targets = figma.currentPage.selection.slice();
|
|
2805
|
+
else
|
|
2806
|
+
targets = [figma.currentPage];
|
|
2807
|
+
for (let i = 0;i < targets.length; i++)
|
|
2808
|
+
if (targets[i].type === "PAGE")
|
|
2809
|
+
await targets[i].loadAsync();
|
|
2810
|
+
const maxDepth = Math.max(0, Math.min(12, typeof p.depth === "number" ? p.depth : 3));
|
|
2811
|
+
const budget = Math.max(10, Math.min(2000, p.maxNodes || 300));
|
|
2812
|
+
const lines = [];
|
|
2813
|
+
let count = 0;
|
|
2814
|
+
let truncated = false;
|
|
2815
|
+
const walk = async function(node, depth) {
|
|
2816
|
+
if (count >= budget) {
|
|
2817
|
+
truncated = true;
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
count++;
|
|
2821
|
+
lines.push(repeat(" ", depth) + await line(node));
|
|
2822
|
+
const kids = "children" in node ? node.children : [];
|
|
2823
|
+
if (!kids.length)
|
|
2824
|
+
return;
|
|
2825
|
+
if (depth >= maxDepth) {
|
|
2826
|
+
lines.push(repeat(" ", depth + 1) + "… " + kids.length + " children (increase depth or describe " + node.id + ")");
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
for (let i = 0;i < kids.length; i++) {
|
|
2830
|
+
if (count >= budget) {
|
|
2831
|
+
lines.push(repeat(" ", depth + 1) + "… " + (kids.length - i) + " more");
|
|
2832
|
+
truncated = true;
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
await walk(kids[i], depth + 1);
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
for (let i = 0;i < targets.length; i++)
|
|
2839
|
+
await walk(targets[i], 0);
|
|
2840
|
+
return { outline: lines.join(`
|
|
2841
|
+
`), nodes: count, truncated };
|
|
2842
|
+
}
|
|
2843
|
+
function repeat(s, n) {
|
|
2844
|
+
let out = "";
|
|
2845
|
+
for (let i = 0;i < n; i++)
|
|
2846
|
+
out += s;
|
|
2847
|
+
return out;
|
|
2848
|
+
}
|
|
2849
|
+
async function line(n) {
|
|
2850
|
+
const parts = [n.type, JSON.stringify(n.name), n.id];
|
|
2851
|
+
if (n.type === "PAGE" || n.type === "DOCUMENT")
|
|
2852
|
+
return parts.join(" ");
|
|
2853
|
+
if (n.visible === false)
|
|
2854
|
+
parts.push("[hidden]");
|
|
2855
|
+
if (typeof n.width === "number")
|
|
2856
|
+
parts.push(round(n.width) + "×" + round(n.height));
|
|
2857
|
+
if (n.parent && !isAutoLayout(n.parent) && typeof n.x === "number")
|
|
2858
|
+
parts.push("@" + round(n.x) + "," + round(n.y));
|
|
2859
|
+
if (n.parent && isAutoLayout(n.parent) && n.layoutSizingHorizontal) {
|
|
2860
|
+
parts.push("size:" + n.layoutSizingHorizontal.toLowerCase() + "/" + n.layoutSizingVertical.toLowerCase());
|
|
2861
|
+
if (n.layoutPositioning === "ABSOLUTE")
|
|
2862
|
+
parts.push("absolute");
|
|
2863
|
+
}
|
|
2864
|
+
if (isAutoLayout(n))
|
|
2865
|
+
parts.push(layoutText(n));
|
|
2866
|
+
if (n.clipsContent)
|
|
2867
|
+
parts.push("clip");
|
|
2868
|
+
if (n.type === "TEXT") {
|
|
2869
|
+
const text = n.characters.length > 80 ? n.characters.slice(0, 77) + "…" : n.characters;
|
|
2870
|
+
parts.push(JSON.stringify(text));
|
|
2871
|
+
if (n.fontName !== figma.mixed)
|
|
2872
|
+
parts.push(n.fontName.family + " " + n.fontName.style);
|
|
2873
|
+
else
|
|
2874
|
+
parts.push("mixed fonts");
|
|
2875
|
+
if (n.fontSize !== figma.mixed)
|
|
2876
|
+
parts.push(String(round(n.fontSize)) + "px");
|
|
2877
|
+
const lh = n.lineHeight;
|
|
2878
|
+
if (lh !== figma.mixed && lh.unit !== "AUTO")
|
|
2879
|
+
parts.push("lh:" + round(lh.value) + (lh.unit === "PERCENT" ? "%" : "px"));
|
|
2880
|
+
const ts = await styleName(n.textStyleId);
|
|
2881
|
+
if (ts)
|
|
2882
|
+
parts.push("textStyle:" + JSON.stringify(ts));
|
|
2883
|
+
}
|
|
2884
|
+
const fills = await paintsText(n, "fills", "fillStyleId");
|
|
2885
|
+
if (fills)
|
|
2886
|
+
parts.push((n.type === "TEXT" ? "color:" : "fill:") + fills);
|
|
2887
|
+
const strokes = await paintsText(n, "strokes", "strokeStyleId");
|
|
2888
|
+
if (strokes && n.strokeWeight !== figma.mixed && n.strokeWeight > 0)
|
|
2889
|
+
parts.push("stroke:" + strokes + " " + round(n.strokeWeight));
|
|
2890
|
+
if ("cornerRadius" in n) {
|
|
2891
|
+
if (n.cornerRadius === figma.mixed)
|
|
2892
|
+
parts.push("radius:" + [n.topLeftRadius, n.topRightRadius, n.bottomRightRadius, n.bottomLeftRadius].join(","));
|
|
2893
|
+
else if (n.cornerRadius > 0)
|
|
2894
|
+
parts.push("radius:" + round(n.cornerRadius));
|
|
2895
|
+
}
|
|
2896
|
+
if (typeof n.opacity === "number" && n.opacity < 1)
|
|
2897
|
+
parts.push("opacity:" + round(n.opacity));
|
|
2898
|
+
if (n.effects && n.effects.length) {
|
|
2899
|
+
const es = await styleName(n.effectStyleId);
|
|
2900
|
+
parts.push("effects:" + (es ? JSON.stringify(es) : n.effects.map(function(e) {
|
|
2901
|
+
return e.type.toLowerCase().replace("_shadow", "-shadow").replace("layer_", "").replace("background_", "bg-");
|
|
2902
|
+
}).join("+")));
|
|
2903
|
+
}
|
|
2904
|
+
if (n.type === "INSTANCE") {
|
|
2905
|
+
const main = await n.getMainComponentAsync();
|
|
2906
|
+
if (main) {
|
|
2907
|
+
const setName = main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.name : main.name;
|
|
2908
|
+
parts.push("of:" + JSON.stringify(setName));
|
|
2909
|
+
}
|
|
2910
|
+
const props = n.componentProperties;
|
|
2911
|
+
const keys = Object.keys(props);
|
|
2912
|
+
if (keys.length) {
|
|
2913
|
+
parts.push("{" + keys.slice(0, 8).map(function(k) {
|
|
2914
|
+
return k.split("#")[0] + "=" + props[k].value;
|
|
2915
|
+
}).join(", ") + "}");
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
if (n.type === "COMPONENT_SET") {
|
|
2919
|
+
const defs = n.componentPropertyDefinitions;
|
|
2920
|
+
parts.push("variants{" + Object.keys(defs).filter(function(k) {
|
|
2921
|
+
return defs[k].type === "VARIANT";
|
|
2922
|
+
}).map(function(k) {
|
|
2923
|
+
return k + ":" + (defs[k].variantOptions || []).join("|");
|
|
2924
|
+
}).join("; ") + "}");
|
|
2925
|
+
}
|
|
2926
|
+
if (n.boundVariables && Object.keys(n.boundVariables).length)
|
|
2927
|
+
parts.push("vars:" + Object.keys(n.boundVariables).join(","));
|
|
2928
|
+
return parts.join(" ");
|
|
2929
|
+
}
|
|
2930
|
+
function layoutText(n) {
|
|
2931
|
+
if (n.layoutMode === "GRID") {
|
|
2932
|
+
const gaps = n.gridColumnGap === n.gridRowGap ? "gap:" + round(n.gridColumnGap) : "gap:" + round(n.gridColumnGap) + "/" + round(n.gridRowGap);
|
|
2933
|
+
return "grid " + n.gridColumnCount + "×" + n.gridRowCount + (n.gridColumnGap || n.gridRowGap ? " " + gaps : "");
|
|
2934
|
+
}
|
|
2935
|
+
const out = [n.layoutMode === "HORIZONTAL" ? "row" : "column"];
|
|
2936
|
+
if (n.primaryAxisAlignItems === "SPACE_BETWEEN")
|
|
2937
|
+
out.push("gap:auto");
|
|
2938
|
+
else if (n.itemSpacing)
|
|
2939
|
+
out.push("gap:" + round(n.itemSpacing));
|
|
2940
|
+
const pad = [n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft].map(round);
|
|
2941
|
+
if (pad[0] || pad[1] || pad[2] || pad[3]) {
|
|
2942
|
+
out.push("pad:" + (pad[0] === pad[2] && pad[1] === pad[3] ? pad[0] === pad[1] ? pad[0] : pad[0] + "," + pad[1] : pad.join(",")));
|
|
2943
|
+
}
|
|
2944
|
+
const names = { MIN: "start", CENTER: "center", MAX: "end", BASELINE: "baseline" };
|
|
2945
|
+
if (n.primaryAxisAlignItems !== "MIN" && n.primaryAxisAlignItems !== "SPACE_BETWEEN")
|
|
2946
|
+
out.push("justify:" + names[n.primaryAxisAlignItems]);
|
|
2947
|
+
if (n.counterAxisAlignItems !== "MIN")
|
|
2948
|
+
out.push("align:" + names[n.counterAxisAlignItems]);
|
|
2949
|
+
if (n.layoutWrap === "WRAP")
|
|
2950
|
+
out.push("wrap");
|
|
2951
|
+
return out.join(" ");
|
|
2952
|
+
}
|
|
2953
|
+
async function paintsText(n, field, styleField) {
|
|
2954
|
+
if (!(field in n))
|
|
2955
|
+
return null;
|
|
2956
|
+
const style = await styleName(n[styleField]);
|
|
2957
|
+
if (style)
|
|
2958
|
+
return "style:" + JSON.stringify(style);
|
|
2959
|
+
const paints = n[field];
|
|
2960
|
+
if (paints === figma.mixed)
|
|
2961
|
+
return "mixed";
|
|
2962
|
+
const visible = paints.filter(function(p) {
|
|
2963
|
+
return p.visible !== false;
|
|
2964
|
+
});
|
|
2965
|
+
if (!visible.length)
|
|
2966
|
+
return null;
|
|
2967
|
+
return visible.map(function(p) {
|
|
2968
|
+
if (p.type === "SOLID") {
|
|
2969
|
+
const bound = p.boundVariables && p.boundVariables.color ? "var" : "";
|
|
2970
|
+
return (bound ? bound + ":" : "") + toHex(p.color, p.opacity);
|
|
2971
|
+
}
|
|
2972
|
+
if (p.type === "IMAGE")
|
|
2973
|
+
return "image(" + String(p.scaleMode).toLowerCase() + ")";
|
|
2974
|
+
if (p.type.indexOf("GRADIENT") === 0) {
|
|
2975
|
+
return p.type.replace("GRADIENT_", "").toLowerCase() + "-gradient(" + p.gradientStops.map(function(s) {
|
|
2976
|
+
return toHex(s.color);
|
|
2977
|
+
}).join(",") + ")";
|
|
2978
|
+
}
|
|
2979
|
+
return p.type.toLowerCase();
|
|
2980
|
+
}).join("+");
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// plugin/lib/export.ts
|
|
2984
|
+
var MAX_NODES4 = 1500;
|
|
2985
|
+
var MAX_ASSET_BYTES = 40 << 20;
|
|
2986
|
+
var VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "ELLIPSE", "LINE"];
|
|
2987
|
+
var VISUAL_CSS = [
|
|
2988
|
+
"background",
|
|
2989
|
+
"border",
|
|
2990
|
+
"border-top",
|
|
2991
|
+
"border-right",
|
|
2992
|
+
"border-bottom",
|
|
2993
|
+
"border-left",
|
|
2994
|
+
"border-radius",
|
|
2995
|
+
"box-shadow",
|
|
2996
|
+
"opacity",
|
|
2997
|
+
"filter",
|
|
2998
|
+
"backdrop-filter",
|
|
2999
|
+
"mix-blend-mode",
|
|
3000
|
+
"outline",
|
|
3001
|
+
"outline-offset"
|
|
3002
|
+
];
|
|
3003
|
+
async function exportTree(p) {
|
|
3004
|
+
const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
|
|
3005
|
+
if (!node)
|
|
3006
|
+
throw codeError("No nodeId given and nothing is selected", "BAD_ARGS");
|
|
3007
|
+
if (node.type === "PAGE" || node.type === "DOCUMENT")
|
|
3008
|
+
throw codeError("Export a frame or layer, not a page", "BAD_ARGS");
|
|
3009
|
+
const ctx = { count: 0, truncated: false, assets: {}, assetBytes: 0, imageFiles: {}, names: {}, warnings: [] };
|
|
3010
|
+
const tree = await walk(node, null, ctx);
|
|
3011
|
+
return { tree, assets: ctx.assets, nodes: ctx.count, truncated: ctx.truncated, warnings: ctx.warnings.slice(0, 40) };
|
|
3012
|
+
}
|
|
3013
|
+
function fileName(base, ext, ctx) {
|
|
3014
|
+
const clean = String(base).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "asset";
|
|
3015
|
+
const n = ctx.names[clean + ext] = (ctx.names[clean + ext] || 0) + 1;
|
|
3016
|
+
return clean + (n > 1 ? "-" + n : "") + "." + ext;
|
|
3017
|
+
}
|
|
3018
|
+
function sizing2(n, axis) {
|
|
3019
|
+
const v = n["layoutSizing" + (axis === "H" ? "Horizontal" : "Vertical")];
|
|
3020
|
+
return v === "FILL" ? "fill" : v === "HUG" ? "hug" : "fixed";
|
|
3021
|
+
}
|
|
3022
|
+
function isVectorOnly(n) {
|
|
3023
|
+
if (VECTOR_TYPES.indexOf(n.type) !== -1)
|
|
3024
|
+
return !hasImageFill(n);
|
|
3025
|
+
if (n.type === "TEXT" || hasImageFill(n))
|
|
3026
|
+
return false;
|
|
3027
|
+
if (!("children" in n) || !n.children.length)
|
|
3028
|
+
return false;
|
|
3029
|
+
for (let i = 0;i < n.children.length; i++)
|
|
3030
|
+
if (n.children[i].visible && !isVectorOnly(n.children[i]))
|
|
3031
|
+
return false;
|
|
3032
|
+
return true;
|
|
3033
|
+
}
|
|
3034
|
+
function hasImageFill(n) {
|
|
3035
|
+
return "fills" in n && n.fills !== figma.mixed && n.fills.some(function(f) {
|
|
3036
|
+
return f.type === "IMAGE" && f.visible !== false;
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
async function imageAsset(n, ctx) {
|
|
3040
|
+
const paint = n.fills.filter(function(f) {
|
|
3041
|
+
return f.type === "IMAGE" && f.visible !== false;
|
|
3042
|
+
})[0];
|
|
3043
|
+
if (!paint || !paint.imageHash)
|
|
3044
|
+
return null;
|
|
3045
|
+
let file = ctx.imageFiles[paint.imageHash];
|
|
3046
|
+
if (!file) {
|
|
3047
|
+
const image = figma.getImageByHash(paint.imageHash);
|
|
3048
|
+
if (!image)
|
|
3049
|
+
return null;
|
|
3050
|
+
const bytes = await image.getBytesAsync();
|
|
3051
|
+
if (ctx.assetBytes + bytes.length > MAX_ASSET_BYTES) {
|
|
3052
|
+
ctx.warnings.push(n.name + ": image skipped (assets over " + (MAX_ASSET_BYTES >> 20) + " MB)");
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
ctx.assetBytes += bytes.length;
|
|
3056
|
+
file = fileName(n.name, "img", ctx);
|
|
3057
|
+
ctx.assets[file] = { b64: figma.base64Encode(bytes) };
|
|
3058
|
+
ctx.imageFiles[paint.imageHash] = file;
|
|
3059
|
+
}
|
|
3060
|
+
return { file, fit: paint.scaleMode === "FIT" ? "contain" : paint.scaleMode === "TILE" ? "tile" : "cover" };
|
|
3061
|
+
}
|
|
3062
|
+
function tracks(list) {
|
|
3063
|
+
return (list || []).map(function(t) {
|
|
3064
|
+
return t.type === "FIXED" ? round(t.value) + "px" : t.type === "HUG" ? "auto" : (t.value || 1) + "fr";
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
async function walk(n, parent, ctx) {
|
|
3068
|
+
ctx.count++;
|
|
3069
|
+
const box = n.absoluteBoundingBox || { x: 0, y: 0, width: n.width || 0, height: n.height || 0 };
|
|
3070
|
+
const pbox = parent && parent.absoluteBoundingBox;
|
|
3071
|
+
const out = {
|
|
3072
|
+
id: n.id,
|
|
3073
|
+
name: n.name,
|
|
3074
|
+
type: n.type,
|
|
3075
|
+
x: pbox ? round(box.x - pbox.x) : 0,
|
|
3076
|
+
y: pbox ? round(box.y - pbox.y) : 0,
|
|
3077
|
+
w: round(box.width),
|
|
3078
|
+
h: round(box.height),
|
|
3079
|
+
sizeH: sizing2(n, "H"),
|
|
3080
|
+
sizeV: sizing2(n, "V")
|
|
3081
|
+
};
|
|
3082
|
+
if (n.layoutPositioning === "ABSOLUTE")
|
|
3083
|
+
out.absolute = true;
|
|
3084
|
+
if (n.layoutGrow === 1)
|
|
3085
|
+
out.grow = true;
|
|
3086
|
+
if (parent && parent.layoutMode === "GRID") {
|
|
3087
|
+
if (n.gridColumnSpan > 1)
|
|
3088
|
+
out.colSpan = n.gridColumnSpan;
|
|
3089
|
+
if (n.gridRowSpan > 1)
|
|
3090
|
+
out.rowSpan = n.gridRowSpan;
|
|
3091
|
+
}
|
|
3092
|
+
if (n.rotation && Math.abs(n.rotation) > 0.01 && n.type !== "TEXT")
|
|
3093
|
+
out.rotation = round(n.rotation);
|
|
3094
|
+
if (n.type === "INSTANCE") {
|
|
3095
|
+
const info = await instanceInfo(n);
|
|
3096
|
+
if (info)
|
|
3097
|
+
out.component = info;
|
|
3098
|
+
}
|
|
3099
|
+
let css = {};
|
|
3100
|
+
try {
|
|
3101
|
+
css = await n.getCSSAsync();
|
|
3102
|
+
} catch (e) {}
|
|
3103
|
+
const visual = {};
|
|
3104
|
+
for (let i = 0;i < VISUAL_CSS.length; i++)
|
|
3105
|
+
if (css[VISUAL_CSS[i]] !== undefined)
|
|
3106
|
+
visual[VISUAL_CSS[i]] = css[VISUAL_CSS[i]];
|
|
3107
|
+
out.css = visual;
|
|
3108
|
+
if (n.type !== "TEXT" && isVectorOnly(n)) {
|
|
3109
|
+
try {
|
|
3110
|
+
const svg = await n.exportAsync({ format: "SVG_STRING" });
|
|
3111
|
+
const file = fileName(n.name, "svg", ctx);
|
|
3112
|
+
ctx.assets[file] = { svg };
|
|
3113
|
+
out.asset = { file, kind: "svg" };
|
|
3114
|
+
out.css = {};
|
|
3115
|
+
return out;
|
|
3116
|
+
} catch (e) {
|
|
3117
|
+
ctx.warnings.push(n.name + ": SVG export failed, kept as a box");
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
if (hasImageFill(n)) {
|
|
3121
|
+
const img = await imageAsset(n, ctx);
|
|
3122
|
+
if (img) {
|
|
3123
|
+
delete out.css.background;
|
|
3124
|
+
if ("children" in n && n.children.length)
|
|
3125
|
+
out.bgImage = img;
|
|
3126
|
+
else
|
|
3127
|
+
out.asset = { file: img.file, kind: "image", fit: img.fit };
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
if (n.type === "TEXT") {
|
|
3131
|
+
out.text = textOf(n);
|
|
3132
|
+
return out;
|
|
3133
|
+
}
|
|
3134
|
+
if ("layoutMode" in n && n.layoutMode && n.layoutMode !== "NONE") {
|
|
3135
|
+
const l = {
|
|
3136
|
+
mode: n.layoutMode === "HORIZONTAL" ? "row" : n.layoutMode === "VERTICAL" ? "column" : "grid",
|
|
3137
|
+
padding: [n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft].map(round)
|
|
3138
|
+
};
|
|
3139
|
+
if (l.mode === "grid") {
|
|
3140
|
+
l.columns = tracks(n.gridColumnSizes);
|
|
3141
|
+
l.rows = tracks(n.gridRowSizes);
|
|
3142
|
+
l.columnGap = round(n.gridColumnGap || 0);
|
|
3143
|
+
l.rowGap = round(n.gridRowGap || 0);
|
|
3144
|
+
} else {
|
|
3145
|
+
l.gap = n.primaryAxisAlignItems === "SPACE_BETWEEN" ? 0 : round(n.itemSpacing || 0);
|
|
3146
|
+
l.justify = n.primaryAxisAlignItems;
|
|
3147
|
+
l.align = n.counterAxisAlignItems;
|
|
3148
|
+
if (n.layoutWrap === "WRAP") {
|
|
3149
|
+
l.wrap = true;
|
|
3150
|
+
l.rowGap = round(n.counterAxisSpacing || 0);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
out.layout = l;
|
|
3154
|
+
}
|
|
3155
|
+
if (n.clipsContent)
|
|
3156
|
+
out.clip = true;
|
|
3157
|
+
if ("children" in n && n.children.length && !out.asset) {
|
|
3158
|
+
out.children = [];
|
|
3159
|
+
for (let i = 0;i < n.children.length; i++) {
|
|
3160
|
+
const c = n.children[i];
|
|
3161
|
+
if (!c.visible)
|
|
3162
|
+
continue;
|
|
3163
|
+
if (ctx.count >= MAX_NODES4) {
|
|
3164
|
+
ctx.truncated = true;
|
|
3165
|
+
break;
|
|
3166
|
+
}
|
|
3167
|
+
out.children.push(await walk(c, n, ctx));
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return out;
|
|
3171
|
+
}
|
|
3172
|
+
function textOf(t) {
|
|
3173
|
+
const segments = t.getStyledTextSegments(["fontName", "fontSize", "fontWeight", "fills", "textDecoration", "hyperlink", "letterSpacing", "lineHeight", "textCase"]);
|
|
3174
|
+
return {
|
|
3175
|
+
autoResize: t.textAutoResize,
|
|
3176
|
+
align: t.textAlignHorizontal,
|
|
3177
|
+
valign: t.textAlignVertical,
|
|
3178
|
+
truncate: t.textTruncation === "ENDING" ? t.maxLines || 1 : 0,
|
|
3179
|
+
segments: segments.map(function(s) {
|
|
3180
|
+
const fill = s.fills.filter(function(f) {
|
|
3181
|
+
return f.visible !== false;
|
|
3182
|
+
})[0];
|
|
3183
|
+
const seg = {
|
|
3184
|
+
text: s.characters,
|
|
3185
|
+
family: s.fontName.family,
|
|
3186
|
+
style: s.fontName.style,
|
|
3187
|
+
weight: s.fontWeight,
|
|
3188
|
+
size: round(s.fontSize)
|
|
3189
|
+
};
|
|
3190
|
+
if (fill && fill.type === "SOLID")
|
|
3191
|
+
seg.color = toHex(fill.color, fill.opacity);
|
|
3192
|
+
if (s.textDecoration !== "NONE")
|
|
3193
|
+
seg.decoration = s.textDecoration === "UNDERLINE" ? "underline" : "line-through";
|
|
3194
|
+
if (s.hyperlink && s.hyperlink.type === "URL")
|
|
3195
|
+
seg.link = s.hyperlink.value;
|
|
3196
|
+
if (s.letterSpacing && s.letterSpacing.value)
|
|
3197
|
+
seg.letterSpacing = s.letterSpacing.unit === "PERCENT" ? round(s.letterSpacing.value / 100) + "em" : round(s.letterSpacing.value) + "px";
|
|
3198
|
+
if (s.lineHeight && s.lineHeight.unit !== "AUTO")
|
|
3199
|
+
seg.lineHeight = s.lineHeight.unit === "PERCENT" ? String(round(s.lineHeight.value / 100)) : round(s.lineHeight.value) + "px";
|
|
3200
|
+
if (s.textCase && s.textCase !== "ORIGINAL")
|
|
3201
|
+
seg.textCase = s.textCase === "UPPER" ? "uppercase" : s.textCase === "LOWER" ? "lowercase" : "capitalize";
|
|
3202
|
+
return seg;
|
|
3203
|
+
})
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
async function instanceInfo(n) {
|
|
3207
|
+
let main = null;
|
|
3208
|
+
try {
|
|
3209
|
+
main = await n.getMainComponentAsync();
|
|
3210
|
+
} catch (e) {}
|
|
3211
|
+
if (!main)
|
|
3212
|
+
return null;
|
|
3213
|
+
const set = main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.name : null;
|
|
3214
|
+
const props = {};
|
|
3215
|
+
const defs = n.componentProperties;
|
|
3216
|
+
const keys = Object.keys(defs);
|
|
3217
|
+
for (let i = 0;i < keys.length; i++) {
|
|
3218
|
+
const d = defs[keys[i]];
|
|
3219
|
+
if (d.type === "INSTANCE_SWAP")
|
|
3220
|
+
continue;
|
|
3221
|
+
props[keys[i].split("#")[0]] = d.value;
|
|
3222
|
+
}
|
|
3223
|
+
const info = { name: set || main.name, props };
|
|
3224
|
+
if (set)
|
|
3225
|
+
info.variant = main.name;
|
|
3226
|
+
const text = n.findOne(function(t) {
|
|
3227
|
+
return t.type === "TEXT" && t.visible;
|
|
3228
|
+
});
|
|
3229
|
+
if (text && text.characters.trim())
|
|
3230
|
+
info.text = text.characters.trim().slice(0, 200);
|
|
3231
|
+
return info;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// plugin/lib/design-system.ts
|
|
3235
|
+
var ALL = ["colors", "text", "effects", "variables", "components"];
|
|
3236
|
+
async function getDesignSystem(p) {
|
|
3237
|
+
const include = Array.isArray(p.include) && p.include.length ? p.include : ALL;
|
|
3238
|
+
const limit = Math.max(1, Math.min(2000, p.limit || 300));
|
|
3239
|
+
const out = {};
|
|
3240
|
+
const counts = {};
|
|
3241
|
+
if (include.indexOf("colors") !== -1 || include.indexOf("text") !== -1 || include.indexOf("effects") !== -1) {
|
|
3242
|
+
const styles = await localStyles();
|
|
3243
|
+
if (include.indexOf("colors") !== -1) {
|
|
3244
|
+
counts.colors = styles.paint.length;
|
|
3245
|
+
out.colors = styles.paint.slice(0, limit).map(function(s) {
|
|
3246
|
+
return { name: s.name, value: paintValue(s.paints) };
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
if (include.indexOf("text") !== -1) {
|
|
3250
|
+
counts.text = styles.text.length;
|
|
3251
|
+
out.text = styles.text.slice(0, limit).map(function(s) {
|
|
3252
|
+
const lh = s.lineHeight;
|
|
3253
|
+
return {
|
|
3254
|
+
name: s.name,
|
|
3255
|
+
font: s.fontName.family + ":" + s.fontName.style,
|
|
3256
|
+
size: round(s.fontSize),
|
|
3257
|
+
lineHeight: lh.unit === "AUTO" ? "auto" : round(lh.value) + (lh.unit === "PERCENT" ? "%" : "px"),
|
|
3258
|
+
letterSpacing: s.letterSpacing.value ? round(s.letterSpacing.value) + (s.letterSpacing.unit === "PERCENT" ? "%" : "px") : undefined
|
|
3259
|
+
};
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
if (include.indexOf("effects") !== -1) {
|
|
3263
|
+
counts.effects = styles.effect.length;
|
|
3264
|
+
out.effects = styles.effect.slice(0, limit).map(function(s) {
|
|
3265
|
+
return {
|
|
3266
|
+
name: s.name,
|
|
3267
|
+
effects: s.effects.map(function(e) {
|
|
3268
|
+
return e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW" ? e.type.toLowerCase() + " " + e.offset.x + "," + e.offset.y + " blur " + e.radius + " " + toHex(e.color) : e.type.toLowerCase() + " " + e.radius;
|
|
3269
|
+
})
|
|
3270
|
+
};
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
if (include.indexOf("variables") !== -1) {
|
|
3275
|
+
const v = await localVariables();
|
|
3276
|
+
const byId = {};
|
|
3277
|
+
for (let i = 0;i < v.vars.length; i++)
|
|
3278
|
+
byId[v.vars[i].id] = v.vars[i];
|
|
3279
|
+
counts.variables = v.vars.length;
|
|
3280
|
+
out.variables = v.collections.map(function(c) {
|
|
3281
|
+
const defaultMode = c.defaultModeId;
|
|
3282
|
+
const vars = v.vars.filter(function(x) {
|
|
3283
|
+
return x.variableCollectionId === c.id;
|
|
3284
|
+
}).slice(0, limit).map(function(x) {
|
|
3285
|
+
return { name: x.name, type: x.resolvedType.toLowerCase(), value: variableValue(x.valuesByMode[defaultMode], byId) };
|
|
3286
|
+
});
|
|
3287
|
+
return {
|
|
3288
|
+
collection: c.name,
|
|
3289
|
+
modes: c.modes.map(function(m) {
|
|
3290
|
+
return m.name;
|
|
3291
|
+
}),
|
|
3292
|
+
variables: vars
|
|
3293
|
+
};
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
if (include.indexOf("components") !== -1) {
|
|
3297
|
+
const list = await localComponents();
|
|
3298
|
+
counts.components = list.length;
|
|
3299
|
+
out.components = list.slice(0, limit).map(function(c) {
|
|
3300
|
+
const item = { name: c.name, id: c.id, page: (pageOf(c) || { name: "?" }).name };
|
|
3301
|
+
if (c.type === "COMPONENT_SET") {
|
|
3302
|
+
const defs = c.componentPropertyDefinitions;
|
|
3303
|
+
const props = {};
|
|
3304
|
+
const keys = Object.keys(defs);
|
|
3305
|
+
for (let i = 0;i < keys.length; i++) {
|
|
3306
|
+
const d = defs[keys[i]];
|
|
3307
|
+
props[keys[i].split("#")[0]] = d.type === "VARIANT" ? d.variantOptions : d.type.toLowerCase();
|
|
3308
|
+
}
|
|
3309
|
+
item.props = props;
|
|
3310
|
+
item.variants = c.children.length;
|
|
3311
|
+
} else {
|
|
3312
|
+
const defs = c.componentPropertyDefinitions;
|
|
3313
|
+
const keys = Object.keys(defs);
|
|
3314
|
+
if (keys.length) {
|
|
3315
|
+
item.props = {};
|
|
3316
|
+
for (let i = 0;i < keys.length; i++)
|
|
3317
|
+
item.props[keys[i].split("#")[0]] = defs[keys[i]].type.toLowerCase();
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (c.description)
|
|
3321
|
+
item.description = c.description.slice(0, 120);
|
|
3322
|
+
item.key = c.key;
|
|
3323
|
+
return item;
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
out.counts = counts;
|
|
3327
|
+
out.usage = 'In build specs: fill:"style:<color name>", textStyle:"style:<text name>", shadow:"style:<effect name>", ' + 'fill:"var:<variable>", gap/padding/radius:"var:<number variable>", {type:"instance", component:"<component name>", props:{...}}.';
|
|
3328
|
+
return out;
|
|
3329
|
+
}
|
|
3330
|
+
function paintValue(paints) {
|
|
3331
|
+
if (!paints.length)
|
|
3332
|
+
return "none";
|
|
3333
|
+
const p = paints[0];
|
|
3334
|
+
if (p.type === "SOLID")
|
|
3335
|
+
return toHex(p.color, p.opacity);
|
|
3336
|
+
if (p.type.indexOf("GRADIENT") === 0) {
|
|
3337
|
+
return p.type.replace("GRADIENT_", "").toLowerCase() + "-gradient(" + p.gradientStops.map(function(s) {
|
|
3338
|
+
return toHex(s.color);
|
|
3339
|
+
}).join(",") + ")";
|
|
3340
|
+
}
|
|
3341
|
+
return p.type.toLowerCase();
|
|
3342
|
+
}
|
|
3343
|
+
function variableValue(v, byId) {
|
|
3344
|
+
if (v && typeof v === "object") {
|
|
3345
|
+
if (v.type === "VARIABLE_ALIAS")
|
|
3346
|
+
return "→ " + (byId[v.id] ? byId[v.id].name : v.id);
|
|
3347
|
+
if ("r" in v)
|
|
3348
|
+
return toHex(v);
|
|
3349
|
+
}
|
|
3350
|
+
return v;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// plugin/lib/tokens-export.ts
|
|
3354
|
+
var TYPES2 = { COLOR: "color", FLOAT: "number", STRING: "string", BOOLEAN: "boolean" };
|
|
3355
|
+
async function exportTokens(p) {
|
|
3356
|
+
const only = Array.isArray(p.collections) && p.collections.length ? p.collections : null;
|
|
3357
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
3358
|
+
const vars = await figma.variables.getLocalVariablesAsync();
|
|
3359
|
+
const byId = {};
|
|
3360
|
+
for (let i = 0;i < vars.length; i++)
|
|
3361
|
+
byId[vars[i].id] = vars[i];
|
|
3362
|
+
const collName = {};
|
|
3363
|
+
for (let i = 0;i < collections.length; i++)
|
|
3364
|
+
collName[collections[i].id] = collections[i].name;
|
|
3365
|
+
const warnings = [];
|
|
3366
|
+
const aliasOf = async function(id) {
|
|
3367
|
+
let v = byId[id] || null;
|
|
3368
|
+
if (!v) {
|
|
3369
|
+
try {
|
|
3370
|
+
v = await figma.variables.getVariableByIdAsync(id);
|
|
3371
|
+
} catch (e) {}
|
|
3372
|
+
if (v)
|
|
3373
|
+
byId[id] = v;
|
|
3374
|
+
}
|
|
3375
|
+
if (!v)
|
|
3376
|
+
return { alias: { collection: "", name: id, missing: true } };
|
|
3377
|
+
let collection = collName[v.variableCollectionId];
|
|
3378
|
+
if (collection === undefined) {
|
|
3379
|
+
try {
|
|
3380
|
+
const c = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
|
|
3381
|
+
collection = c ? c.name : "";
|
|
3382
|
+
} catch (e) {
|
|
3383
|
+
collection = "";
|
|
3384
|
+
}
|
|
3385
|
+
collName[v.variableCollectionId] = collection;
|
|
3386
|
+
}
|
|
3387
|
+
const out = { alias: { collection, name: v.name } };
|
|
3388
|
+
if (v.remote)
|
|
3389
|
+
out.alias.remote = true;
|
|
3390
|
+
return out;
|
|
3391
|
+
};
|
|
3392
|
+
const value = async function(raw, type) {
|
|
3393
|
+
if (raw && typeof raw === "object" && raw.type === "VARIABLE_ALIAS")
|
|
3394
|
+
return aliasOf(raw.id);
|
|
3395
|
+
if (type === "COLOR" && raw && typeof raw === "object")
|
|
3396
|
+
return toHex(raw);
|
|
3397
|
+
if (type === "FLOAT")
|
|
3398
|
+
return round(Number(raw));
|
|
3399
|
+
return raw;
|
|
3400
|
+
};
|
|
3401
|
+
const outCollections = [];
|
|
3402
|
+
for (let c = 0;c < collections.length; c++) {
|
|
3403
|
+
const col = collections[c];
|
|
3404
|
+
if (only && only.indexOf(col.name) === -1)
|
|
3405
|
+
continue;
|
|
3406
|
+
const modes = col.modes.slice().sort(function(a, b) {
|
|
3407
|
+
return (a.modeId === col.defaultModeId ? 0 : 1) - (b.modeId === col.defaultModeId ? 0 : 1);
|
|
3408
|
+
});
|
|
3409
|
+
const list = [];
|
|
3410
|
+
for (let i = 0;i < vars.length; i++) {
|
|
3411
|
+
const v = vars[i];
|
|
3412
|
+
if (v.variableCollectionId !== col.id)
|
|
3413
|
+
continue;
|
|
3414
|
+
const values = {};
|
|
3415
|
+
for (let m = 0;m < modes.length; m++)
|
|
3416
|
+
values[modes[m].name] = await value(v.valuesByMode[modes[m].modeId], v.resolvedType);
|
|
3417
|
+
const item = { name: v.name, type: TYPES2[v.resolvedType] || "string", values };
|
|
3418
|
+
if (v.description)
|
|
3419
|
+
item.description = v.description;
|
|
3420
|
+
if (v.scopes && v.scopes.length && !(v.scopes.length === 1 && v.scopes[0] === "ALL_SCOPES"))
|
|
3421
|
+
item.scopes = v.scopes.slice();
|
|
3422
|
+
if (v.hiddenFromPublishing)
|
|
3423
|
+
item.hidden = true;
|
|
3424
|
+
list.push(item);
|
|
3425
|
+
}
|
|
3426
|
+
outCollections.push({
|
|
3427
|
+
name: col.name,
|
|
3428
|
+
modes: modes.map(function(m) {
|
|
3429
|
+
return m.name;
|
|
3430
|
+
}),
|
|
3431
|
+
variables: list
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
const styles = await localStyles();
|
|
3435
|
+
const paint = [];
|
|
3436
|
+
for (let i = 0;i < styles.paint.length; i++) {
|
|
3437
|
+
const s = styles.paint[i];
|
|
3438
|
+
const paints = [];
|
|
3439
|
+
for (let k = 0;k < s.paints.length; k++) {
|
|
3440
|
+
const pt = s.paints[k];
|
|
3441
|
+
if (pt.visible === false)
|
|
3442
|
+
continue;
|
|
3443
|
+
if (pt.type === "SOLID") {
|
|
3444
|
+
const bound = pt.boundVariables && pt.boundVariables.color;
|
|
3445
|
+
paints.push(bound ? await aliasOf(bound.id) : toHex(pt.color, pt.opacity));
|
|
3446
|
+
} else if (pt.type.indexOf("GRADIENT") === 0) {
|
|
3447
|
+
const t = pt.gradientTransform;
|
|
3448
|
+
const g = {
|
|
3449
|
+
gradient: pt.gradientStops.map(function(st) {
|
|
3450
|
+
return { color: toHex(st.color), at: round(st.position) };
|
|
3451
|
+
})
|
|
3452
|
+
};
|
|
3453
|
+
if (pt.type === "GRADIENT_LINEAR")
|
|
3454
|
+
g.angle = round(Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI);
|
|
3455
|
+
else
|
|
3456
|
+
g.type = pt.type.replace("GRADIENT_", "").toLowerCase();
|
|
3457
|
+
paints.push(g);
|
|
3458
|
+
} else {
|
|
3459
|
+
warnings.push('Paint style "' + s.name + '": ' + pt.type.toLowerCase() + " paints are not exported");
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
if (!paints.length)
|
|
3463
|
+
continue;
|
|
3464
|
+
const item = { name: s.name, value: paints.length === 1 ? paints[0] : paints };
|
|
3465
|
+
if (s.description)
|
|
3466
|
+
item.description = s.description;
|
|
3467
|
+
paint.push(item);
|
|
3468
|
+
}
|
|
3469
|
+
const text = [];
|
|
3470
|
+
for (let i = 0;i < styles.text.length; i++) {
|
|
3471
|
+
const s = styles.text[i];
|
|
3472
|
+
const item = { name: s.name, font: s.fontName.family, style: s.fontName.style, size: round(s.fontSize) };
|
|
3473
|
+
const lh = s.lineHeight;
|
|
3474
|
+
if (lh.unit === "PIXELS")
|
|
3475
|
+
item.lineHeight = round(lh.value);
|
|
3476
|
+
else if (lh.unit === "PERCENT")
|
|
3477
|
+
item.lineHeight = round(lh.value) + "%";
|
|
3478
|
+
if (s.letterSpacing.value)
|
|
3479
|
+
item.letterSpacing = s.letterSpacing.unit === "PERCENT" ? round(s.letterSpacing.value) + "%" : round(s.letterSpacing.value);
|
|
3480
|
+
if (s.textCase && s.textCase !== "ORIGINAL")
|
|
3481
|
+
item.case = s.textCase === "UPPER" ? "upper" : s.textCase === "LOWER" ? "lower" : "title";
|
|
3482
|
+
if (s.textDecoration && s.textDecoration !== "NONE")
|
|
3483
|
+
item.decoration = s.textDecoration === "UNDERLINE" ? "underline" : "strike";
|
|
3484
|
+
if (s.paragraphSpacing)
|
|
3485
|
+
item.paragraphSpacing = round(s.paragraphSpacing);
|
|
3486
|
+
const bound = s.boundVariables || {};
|
|
3487
|
+
const keys = Object.keys(bound);
|
|
3488
|
+
if (keys.length) {
|
|
3489
|
+
item.variables = {};
|
|
3490
|
+
for (let k = 0;k < keys.length; k++)
|
|
3491
|
+
if (bound[keys[k]] && bound[keys[k]].id)
|
|
3492
|
+
item.variables[keys[k]] = (await aliasOf(bound[keys[k]].id)).alias;
|
|
3493
|
+
}
|
|
3494
|
+
if (s.description)
|
|
3495
|
+
item.description = s.description;
|
|
3496
|
+
text.push(item);
|
|
3497
|
+
}
|
|
3498
|
+
const effect = [];
|
|
3499
|
+
for (let i = 0;i < styles.effect.length; i++) {
|
|
3500
|
+
const s = styles.effect[i];
|
|
3501
|
+
const list = [];
|
|
3502
|
+
for (let k = 0;k < s.effects.length; k++) {
|
|
3503
|
+
const e = s.effects[k];
|
|
3504
|
+
if (e.visible === false)
|
|
3505
|
+
continue;
|
|
3506
|
+
if (e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW") {
|
|
3507
|
+
const sh = { x: round(e.offset.x), y: round(e.offset.y), blur: round(e.radius), spread: round(e.spread || 0), color: toHex(e.color) };
|
|
3508
|
+
if (e.type === "INNER_SHADOW")
|
|
3509
|
+
sh.inner = true;
|
|
3510
|
+
if (e.boundVariables && e.boundVariables.color)
|
|
3511
|
+
sh.colorVariable = (await aliasOf(e.boundVariables.color.id)).alias;
|
|
3512
|
+
list.push(sh);
|
|
3513
|
+
} else {
|
|
3514
|
+
list.push({ type: e.type === "LAYER_BLUR" ? "layer" : "background", blur: round(e.radius) });
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
if (!list.length)
|
|
3518
|
+
continue;
|
|
3519
|
+
const item = { name: s.name, value: list };
|
|
3520
|
+
if (s.description)
|
|
3521
|
+
item.description = s.description;
|
|
3522
|
+
effect.push(item);
|
|
3523
|
+
}
|
|
3524
|
+
const out = { fileName: figma.root.name, collections: outCollections, styles: { colors: paint, text, effects: effect } };
|
|
3525
|
+
if (warnings.length)
|
|
3526
|
+
out.warnings = warnings.slice(0, 40);
|
|
3527
|
+
return out;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// plugin/lib/find.ts
|
|
3531
|
+
var MAX_LIMIT = 500;
|
|
3532
|
+
function matcher(q) {
|
|
3533
|
+
if (q === undefined || q === null || q === "")
|
|
3534
|
+
return null;
|
|
3535
|
+
const str = String(q);
|
|
3536
|
+
const m = /^\/(.+)\/([gimsuy]*)$/.exec(str);
|
|
3537
|
+
if (m) {
|
|
3538
|
+
let re;
|
|
3539
|
+
try {
|
|
3540
|
+
re = new RegExp(m[1], m[2].replace("g", ""));
|
|
3541
|
+
} catch (e) {
|
|
3542
|
+
throw codeError("Invalid regex " + str + ": " + (e.message || e), "BAD_ARGS");
|
|
3543
|
+
}
|
|
3544
|
+
return function(s) {
|
|
3545
|
+
return re.test(s);
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
const needle = str.toLowerCase();
|
|
3549
|
+
return function(s) {
|
|
3550
|
+
return s.toLowerCase().indexOf(needle) !== -1;
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
async function find2(p) {
|
|
3554
|
+
const byName = matcher(p.name);
|
|
3555
|
+
const byText = matcher(p.text);
|
|
3556
|
+
const byStyle = matcher(p.style);
|
|
3557
|
+
const byComponent = matcher(p.component);
|
|
3558
|
+
const types = (Array.isArray(p.type) ? p.type : p.type ? [p.type] : []).map(function(t) {
|
|
3559
|
+
return String(t).toUpperCase().replace(/[\s-]/g, "_");
|
|
3560
|
+
});
|
|
3561
|
+
if (!byName && !byText && !byStyle && !byComponent && !types.length) {
|
|
3562
|
+
throw codeError("Give at least one filter: name, text, type, style or component", "BAD_ARGS");
|
|
3563
|
+
}
|
|
3564
|
+
const limit = Math.max(1, Math.min(MAX_LIMIT, p.limit || 50));
|
|
3565
|
+
let roots;
|
|
3566
|
+
if (p.parentId)
|
|
3567
|
+
roots = [await getNode(p.parentId)];
|
|
3568
|
+
else if (p.pageId)
|
|
3569
|
+
roots = [await getNode(p.pageId)];
|
|
3570
|
+
else
|
|
3571
|
+
roots = figma.root.children.slice();
|
|
3572
|
+
for (let i = 0;i < roots.length; i++)
|
|
3573
|
+
if (roots[i].type === "PAGE")
|
|
3574
|
+
await roots[i].loadAsync();
|
|
3575
|
+
const test = function(n) {
|
|
3576
|
+
if (types.length && types.indexOf(n.type) === -1)
|
|
3577
|
+
return false;
|
|
3578
|
+
if (byName && !byName(n.name))
|
|
3579
|
+
return false;
|
|
3580
|
+
if (byText && !(n.type === "TEXT" && byText(n.characters)))
|
|
3581
|
+
return false;
|
|
3582
|
+
if (byComponent && n.type !== "INSTANCE")
|
|
3583
|
+
return false;
|
|
3584
|
+
if (byStyle && !(n.fillStyleId || n.strokeStyleId || n.textStyleId || n.effectStyleId))
|
|
3585
|
+
return false;
|
|
3586
|
+
return true;
|
|
3587
|
+
};
|
|
3588
|
+
let candidates = [];
|
|
3589
|
+
for (let i = 0;i < roots.length; i++) {
|
|
3590
|
+
const r = roots[i];
|
|
3591
|
+
if (r.type !== "PAGE" && test(r))
|
|
3592
|
+
candidates.push(r);
|
|
3593
|
+
if ("findAll" in r)
|
|
3594
|
+
candidates = candidates.concat(r.findAll(test));
|
|
3595
|
+
}
|
|
3596
|
+
const matches = [];
|
|
3597
|
+
let total = 0;
|
|
3598
|
+
for (let i = 0;i < candidates.length; i++) {
|
|
3599
|
+
const n = candidates[i];
|
|
3600
|
+
if (byStyle && !await usesStyle(n, byStyle))
|
|
3601
|
+
continue;
|
|
3602
|
+
if (byComponent && !await isInstanceOf(n, byComponent, String(p.component)))
|
|
3603
|
+
continue;
|
|
3604
|
+
total++;
|
|
3605
|
+
if (matches.length < limit)
|
|
3606
|
+
matches.push(describeMatch(n));
|
|
3607
|
+
}
|
|
3608
|
+
return { total, matches, truncated: total > matches.length };
|
|
3609
|
+
}
|
|
3610
|
+
async function usesStyle(n, test) {
|
|
3611
|
+
const fields = ["fillStyleId", "strokeStyleId", "textStyleId", "effectStyleId"];
|
|
3612
|
+
for (let i = 0;i < fields.length; i++) {
|
|
3613
|
+
const name = await styleName(n[fields[i]]);
|
|
3614
|
+
if (name && test(name))
|
|
3615
|
+
return true;
|
|
3616
|
+
}
|
|
3617
|
+
return false;
|
|
3618
|
+
}
|
|
3619
|
+
async function isInstanceOf(n, test, ref) {
|
|
3620
|
+
const main = await n.getMainComponentAsync();
|
|
3621
|
+
if (!main)
|
|
3622
|
+
return false;
|
|
3623
|
+
if (main.id === ref || main.key === ref || test(main.name))
|
|
3624
|
+
return true;
|
|
3625
|
+
const set = main.parent && main.parent.type === "COMPONENT_SET" ? main.parent : null;
|
|
3626
|
+
return !!set && (set.id === ref || set.key === ref || test(set.name));
|
|
3627
|
+
}
|
|
3628
|
+
function describeMatch(n) {
|
|
3629
|
+
const names = [];
|
|
3630
|
+
let p = n.parent;
|
|
3631
|
+
while (p && p.type !== "PAGE" && p.type !== "DOCUMENT") {
|
|
3632
|
+
names.unshift(p.name);
|
|
3633
|
+
p = p.parent;
|
|
3634
|
+
}
|
|
3635
|
+
const page = pageOf(n);
|
|
3636
|
+
const out = { id: n.id, name: n.name, type: n.type, page: page ? page.name : null, path: names.join(" / ") };
|
|
3637
|
+
if (n.type === "TEXT")
|
|
3638
|
+
out.text = n.characters.length > 80 ? n.characters.slice(0, 77) + "…" : n.characters;
|
|
3639
|
+
return out;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// plugin/lib/selection.ts
|
|
3643
|
+
var MAX_WAIT_MS = 120000;
|
|
3644
|
+
var waiters = {};
|
|
3645
|
+
var listening = false;
|
|
3646
|
+
function selectionSummary() {
|
|
3647
|
+
const sel = figma.currentPage.selection;
|
|
3648
|
+
return {
|
|
3649
|
+
page: { id: figma.currentPage.id, name: figma.currentPage.name },
|
|
3650
|
+
count: sel.length,
|
|
3651
|
+
selection: sel.slice(0, 100).map(function(n) {
|
|
3652
|
+
const b = n.absoluteBoundingBox;
|
|
3653
|
+
return {
|
|
3654
|
+
id: n.id,
|
|
3655
|
+
name: n.name,
|
|
3656
|
+
type: n.type,
|
|
3657
|
+
bounds: b ? { x: round(b.x), y: round(b.y), width: round(b.width), height: round(b.height) } : null
|
|
3658
|
+
};
|
|
3659
|
+
})
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
function onSelectionChange() {
|
|
3663
|
+
if (!figma.currentPage.selection.length)
|
|
3664
|
+
return;
|
|
3665
|
+
const ids = Object.keys(waiters);
|
|
3666
|
+
for (let i = 0;i < ids.length; i++)
|
|
3667
|
+
finish(ids[i], null);
|
|
3668
|
+
}
|
|
3669
|
+
function finish(id, error, timedOut) {
|
|
3670
|
+
const w = waiters[id];
|
|
3671
|
+
if (!w)
|
|
3672
|
+
return;
|
|
3673
|
+
delete waiters[id];
|
|
3674
|
+
clearTimeout(w.timer);
|
|
3675
|
+
figma.ui.postMessage({ t: "waitDone", id });
|
|
3676
|
+
if (!Object.keys(waiters).length && listening) {
|
|
3677
|
+
figma.off("selectionchange", onSelectionChange);
|
|
3678
|
+
listening = false;
|
|
3679
|
+
}
|
|
3680
|
+
if (error)
|
|
3681
|
+
w.reject(error);
|
|
3682
|
+
else {
|
|
3683
|
+
const out = selectionSummary();
|
|
3684
|
+
out.timedOut = !!timedOut;
|
|
3685
|
+
w.resolve(out);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
function waitForSelection(p, timeoutMs, requestId) {
|
|
3689
|
+
const ms = Math.max(1000, Math.min(MAX_WAIT_MS, Number(p.timeoutMs) || timeoutMs || 60000));
|
|
3690
|
+
const message = String(p.message || "Select one or more layers");
|
|
3691
|
+
return new Promise(function(resolve, reject) {
|
|
3692
|
+
waiters[requestId] = {
|
|
3693
|
+
resolve,
|
|
3694
|
+
reject,
|
|
3695
|
+
timer: setTimeout(function() {
|
|
3696
|
+
finish(requestId, null, true);
|
|
3697
|
+
}, ms)
|
|
3698
|
+
};
|
|
3699
|
+
if (!listening) {
|
|
3700
|
+
figma.on("selectionchange", onSelectionChange);
|
|
3701
|
+
listening = true;
|
|
3702
|
+
}
|
|
3703
|
+
figma.ui.postMessage({ t: "wait", id: requestId, message, until: Date.now() + ms });
|
|
3704
|
+
figma.notify("Your agent is waiting: " + message, { timeout: 4000 });
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
function cancelWait(id) {
|
|
3708
|
+
finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
|
|
3709
|
+
}
|
|
3710
|
+
// package.json
|
|
3711
|
+
var version = "1.8.0";
|
|
3712
|
+
|
|
3713
|
+
// plugin/code.ts
|
|
3714
|
+
var DEFAULT_SIZE = { width: 340, height: 540 };
|
|
3715
|
+
var MIN_SIZE = { width: 280, height: 260 };
|
|
3716
|
+
var MAX_SIZE = { width: 900, height: 1200 };
|
|
3717
|
+
var COMPACT_HEIGHT = 44;
|
|
3718
|
+
figma.skipInvisibleInstanceChildren = true;
|
|
3719
|
+
figma.showUI(__html__, { width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height, themeColors: true, title: "Figma Bridge" });
|
|
3720
|
+
var sessionId = randomId();
|
|
3721
|
+
var size = { width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height };
|
|
3722
|
+
var compact = false;
|
|
3723
|
+
function randomId() {
|
|
3724
|
+
let s = "";
|
|
3725
|
+
for (let i = 0;i < 16; i++)
|
|
3726
|
+
s += Math.floor(Math.random() * 16).toString(16);
|
|
3727
|
+
return s;
|
|
3728
|
+
}
|
|
3729
|
+
function post(msg) {
|
|
3730
|
+
figma.ui.postMessage(msg);
|
|
3731
|
+
}
|
|
3732
|
+
function sessionInfo() {
|
|
3733
|
+
return { id: sessionId, fileName: figma.root.name, page: figma.currentPage.name };
|
|
3734
|
+
}
|
|
3735
|
+
function clampSize(w, h) {
|
|
3736
|
+
return {
|
|
3737
|
+
width: Math.round(Math.min(MAX_SIZE.width, Math.max(MIN_SIZE.width, Number(w) || DEFAULT_SIZE.width))),
|
|
3738
|
+
height: Math.round(Math.min(MAX_SIZE.height, Math.max(MIN_SIZE.height, Number(h) || DEFAULT_SIZE.height)))
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
function applySize2() {
|
|
3742
|
+
figma.ui.resize(size.width, compact ? COMPACT_HEIGHT : size.height);
|
|
3743
|
+
}
|
|
3744
|
+
async function sendInit() {
|
|
3745
|
+
const channel = await figma.clientStorage.getAsync("channel") || "default";
|
|
3746
|
+
const saved = await figma.clientStorage.getAsync("size");
|
|
3747
|
+
if (saved)
|
|
3748
|
+
size = clampSize(saved.width, saved.height);
|
|
3749
|
+
compact = !!await figma.clientStorage.getAsync("compact");
|
|
3750
|
+
applySize2();
|
|
3751
|
+
post({ t: "init", version, session: sessionInfo(), settings: { channel, compact } });
|
|
3752
|
+
}
|
|
3753
|
+
try {
|
|
3754
|
+
if (!figma.root.getRelaunchData().open)
|
|
3755
|
+
figma.root.setRelaunchData({ open: "Connect this file to your AI agent" });
|
|
3756
|
+
} catch (e) {}
|
|
3757
|
+
var lastFileName = figma.root.name;
|
|
3758
|
+
figma.on("currentpagechange", function() {
|
|
3759
|
+
post({ t: "info", session: sessionInfo() });
|
|
3760
|
+
});
|
|
3761
|
+
setInterval(function() {
|
|
3762
|
+
if (figma.root.name !== lastFileName) {
|
|
3763
|
+
lastFileName = figma.root.name;
|
|
3764
|
+
post({ t: "info", session: sessionInfo() });
|
|
3765
|
+
}
|
|
3766
|
+
}, 3000);
|
|
3767
|
+
figma.ui.onmessage = function(msg) {
|
|
3768
|
+
if (!msg || typeof msg !== "object")
|
|
3769
|
+
return;
|
|
3770
|
+
if (msg.t === "ready")
|
|
3771
|
+
sendInit();
|
|
3772
|
+
else if (msg.t === "req")
|
|
3773
|
+
handleRequest(msg);
|
|
3774
|
+
else if (msg.t === "set")
|
|
3775
|
+
figma.clientStorage.setAsync(msg.key, msg.value);
|
|
3776
|
+
else if (msg.t === "compact") {
|
|
3777
|
+
compact = !!msg.value;
|
|
3778
|
+
applySize2();
|
|
3779
|
+
figma.clientStorage.setAsync("compact", compact);
|
|
3780
|
+
} else if (msg.t === "resize") {
|
|
3781
|
+
size = clampSize(msg.width, msg.height);
|
|
3782
|
+
compact = false;
|
|
3783
|
+
applySize2();
|
|
3784
|
+
if (msg.save)
|
|
3785
|
+
figma.clientStorage.setAsync("size", size);
|
|
3786
|
+
} else if (msg.t === "resetSize") {
|
|
3787
|
+
size = { width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height };
|
|
3788
|
+
applySize2();
|
|
3789
|
+
figma.clientStorage.setAsync("size", size);
|
|
3790
|
+
} else if (msg.t === "notify")
|
|
3791
|
+
figma.notify(String(msg.text), { timeout: 2500 });
|
|
3792
|
+
else if (msg.t === "focus")
|
|
3793
|
+
focusNode(String(msg.nodeId));
|
|
3794
|
+
else if (msg.t === "cancelWait")
|
|
3795
|
+
cancelWait(String(msg.id));
|
|
3796
|
+
else if (msg.t === "cancel") {
|
|
3797
|
+
markCancelled(String(msg.id));
|
|
3798
|
+
cancelWait(String(msg.id));
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
async function focusNode(id) {
|
|
3802
|
+
const node = await figma.getNodeByIdAsync(id);
|
|
3803
|
+
if (!node || node.type === "DOCUMENT") {
|
|
3804
|
+
figma.notify("This layer no longer exists", { timeout: 2000 });
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
const page = pageOf(node);
|
|
3808
|
+
if (page && page !== figma.currentPage)
|
|
3809
|
+
await figma.setCurrentPageAsync(page);
|
|
3810
|
+
if (node.type === "PAGE")
|
|
3811
|
+
return;
|
|
3812
|
+
figma.currentPage.selection = [node];
|
|
3813
|
+
figma.viewport.scrollAndZoomIntoView([node]);
|
|
3814
|
+
}
|
|
3815
|
+
var HANDLERS = {
|
|
3816
|
+
run_script: runScript,
|
|
3817
|
+
build,
|
|
3818
|
+
describe,
|
|
3819
|
+
find: find2,
|
|
3820
|
+
get_design_system: getDesignSystem,
|
|
3821
|
+
design_tokens: designTokens,
|
|
3822
|
+
export_tokens: exportTokens,
|
|
3823
|
+
audit,
|
|
3824
|
+
get_css: getCss,
|
|
3825
|
+
checkpoint,
|
|
3826
|
+
screenshot,
|
|
3827
|
+
place_image: placeImage,
|
|
3828
|
+
import_svg: importSvg,
|
|
3829
|
+
get_context: getContext,
|
|
3830
|
+
list_fonts: listFonts,
|
|
3831
|
+
wait_for_selection: waitForSelection,
|
|
3832
|
+
prototype,
|
|
3833
|
+
annotate,
|
|
3834
|
+
export_tree: exportTree,
|
|
3835
|
+
ping: function() {
|
|
3836
|
+
return Promise.resolve({ pong: true, session: sessionInfo() });
|
|
3837
|
+
}
|
|
3838
|
+
};
|
|
3839
|
+
var READ_ONLY = {
|
|
3840
|
+
describe: true,
|
|
3841
|
+
find: true,
|
|
3842
|
+
get_design_system: true,
|
|
3843
|
+
export_tokens: true,
|
|
3844
|
+
audit: true,
|
|
3845
|
+
get_css: true,
|
|
3846
|
+
screenshot: true,
|
|
3847
|
+
get_context: true,
|
|
3848
|
+
list_fonts: true,
|
|
3849
|
+
wait_for_selection: true,
|
|
3850
|
+
export_tree: true,
|
|
3851
|
+
ping: true
|
|
3852
|
+
};
|
|
3853
|
+
async function handleRequest(msg) {
|
|
3854
|
+
const started = Date.now();
|
|
3855
|
+
const mutates = !READ_ONLY[msg.method] || !!(msg.params && (msg.params.fix === true || msg.params.fixes && msg.params.fixes.length));
|
|
3856
|
+
let reply;
|
|
3857
|
+
if (mutates)
|
|
3858
|
+
commitUndo();
|
|
3859
|
+
try {
|
|
3860
|
+
const handler = HANDLERS[msg.method];
|
|
3861
|
+
if (!handler)
|
|
3862
|
+
throw codeError("Unknown method: " + msg.method + " (reopen the plugin after updating it)", "UNKNOWN_METHOD");
|
|
3863
|
+
const result = await handler(msg.params || {}, Math.min(Number(msg.timeoutMs) || 30000, 120000), String(msg.id));
|
|
3864
|
+
reply = { t: "res", id: msg.id, ok: true, result };
|
|
3865
|
+
} catch (e) {
|
|
3866
|
+
reply = Object.assign({ t: "res", id: msg.id, ok: false }, describeError(e));
|
|
3867
|
+
}
|
|
3868
|
+
if (mutates) {
|
|
3869
|
+
commitUndo();
|
|
3870
|
+
invalidateCaches();
|
|
3871
|
+
}
|
|
3872
|
+
clearCancelled(String(msg.id));
|
|
3873
|
+
reply.ms = Date.now() - started;
|
|
3874
|
+
post(reply);
|
|
3875
|
+
}
|
|
3876
|
+
function commitUndo() {
|
|
3877
|
+
try {
|
|
3878
|
+
figma.commitUndo();
|
|
3879
|
+
} catch (e) {}
|
|
3880
|
+
}
|
|
3881
|
+
function describeError(e) {
|
|
3882
|
+
const out = {
|
|
3883
|
+
error: e && e.message ? String(e.message) : String(e),
|
|
3884
|
+
code: e && e.code || "PLUGIN_ERROR"
|
|
3885
|
+
};
|
|
3886
|
+
if (e && e.stack)
|
|
3887
|
+
out.stack = String(e.stack).split(`
|
|
3888
|
+
`).slice(0, 6).join(`
|
|
3889
|
+
`);
|
|
3890
|
+
if (e && e.line)
|
|
3891
|
+
out.line = e.line;
|
|
3892
|
+
if (e && e.logs && e.logs.length)
|
|
3893
|
+
out.logs = e.logs;
|
|
3894
|
+
return out;
|
|
3895
|
+
}
|
|
3896
|
+
var AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
|
|
3897
|
+
var SCRIPT_ARGS = ["figma", "console", "utils", "lib"];
|
|
3898
|
+
var lineBase = null;
|
|
3899
|
+
var currentConsole = console;
|
|
3900
|
+
var lib = {};
|
|
3901
|
+
var libHash = "";
|
|
3902
|
+
function loadLibrary(hash, sources) {
|
|
3903
|
+
if (!hash || hash === libHash)
|
|
3904
|
+
return;
|
|
3905
|
+
const names = Object.keys(lib);
|
|
3906
|
+
for (let i = 0;i < names.length; i++)
|
|
3907
|
+
delete lib[names[i]];
|
|
3908
|
+
const keys = Object.keys(sources || {});
|
|
3909
|
+
for (let i = 0;i < keys.length; i++) {
|
|
3910
|
+
const name = keys[i];
|
|
3911
|
+
try {
|
|
3912
|
+
const fn = new AsyncFunction("figma", "utils", "lib", "console", "args", sources[name]);
|
|
3913
|
+
lib[name] = function(args) {
|
|
3914
|
+
return fn(figma, utils, lib, currentConsole, args);
|
|
3915
|
+
};
|
|
3916
|
+
} catch (e) {
|
|
3917
|
+
const message = "Snippet " + name + " does not compile: " + (e.message || e);
|
|
3918
|
+
lib[name] = function() {
|
|
3919
|
+
return Promise.reject(new Error(message));
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
libHash = hash;
|
|
3924
|
+
}
|
|
3925
|
+
function compile(code) {
|
|
3926
|
+
if (!/\breturn\b/.test(code)) {
|
|
3927
|
+
try {
|
|
3928
|
+
const expr = code.trim().replace(/;+\s*$/, "");
|
|
3929
|
+
return { fn: new AsyncFunction(SCRIPT_ARGS[0], SCRIPT_ARGS[1], SCRIPT_ARGS[2], SCRIPT_ARGS[3], `return (
|
|
3930
|
+
` + expr + `
|
|
3931
|
+
);`), offset: 1 };
|
|
3932
|
+
} catch (e) {}
|
|
3933
|
+
}
|
|
3934
|
+
return { fn: new AsyncFunction(SCRIPT_ARGS[0], SCRIPT_ARGS[1], SCRIPT_ARGS[2], SCRIPT_ARGS[3], code), offset: 0 };
|
|
3935
|
+
}
|
|
3936
|
+
function stackLine(stack) {
|
|
3937
|
+
const lines = String(stack || "").split(`
|
|
3938
|
+
`);
|
|
3939
|
+
for (let pass = 0;pass < 2; pass++) {
|
|
3940
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3941
|
+
const l = lines[i];
|
|
3942
|
+
if (pass === 0 && !/anonymous|eval|<input>/.test(l))
|
|
3943
|
+
continue;
|
|
3944
|
+
const m = /:(\d+)(?::\d+)?\)?\s*$/.exec(l);
|
|
3945
|
+
if (m)
|
|
3946
|
+
return Number(m[1]);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
return null;
|
|
3950
|
+
}
|
|
3951
|
+
async function scriptLine(e, offset) {
|
|
3952
|
+
if (lineBase === null) {
|
|
3953
|
+
lineBase = 0;
|
|
3954
|
+
try {
|
|
3955
|
+
await new AsyncFunction("throw new Error('probe')")();
|
|
3956
|
+
} catch (probe) {
|
|
3957
|
+
const n = stackLine(probe.stack);
|
|
3958
|
+
if (n !== null)
|
|
3959
|
+
lineBase = n - 1;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
const raw = stackLine(e && e.stack);
|
|
3963
|
+
if (raw === null) {
|
|
3964
|
+
const m = /line (\d+)/i.exec(String(e && e.message));
|
|
3965
|
+
return m ? Number(m[1]) : undefined;
|
|
3966
|
+
}
|
|
3967
|
+
const line = raw - lineBase - offset;
|
|
3968
|
+
return line >= 1 ? line : undefined;
|
|
3969
|
+
}
|
|
3970
|
+
async function runScript(p, timeoutMs) {
|
|
3971
|
+
const logs = [];
|
|
3972
|
+
const scriptConsole = makeConsole(logs);
|
|
3973
|
+
let compiled = { fn: null, offset: 0 };
|
|
3974
|
+
let timer;
|
|
3975
|
+
loadLibrary(p.libHash, p.lib);
|
|
3976
|
+
currentConsole = scriptConsole;
|
|
3977
|
+
try {
|
|
3978
|
+
compiled = compile(String(p.code || ""));
|
|
3979
|
+
const value = await Promise.race([
|
|
3980
|
+
compiled.fn(figma, scriptConsole, utils, lib),
|
|
3981
|
+
new Promise(function(_, reject) {
|
|
3982
|
+
timer = setTimeout(function() {
|
|
3983
|
+
reject(codeError("Script timed out after " + timeoutMs + " ms (async work may still be running in Figma)", "TIMEOUT"));
|
|
3984
|
+
}, timeoutMs);
|
|
3985
|
+
})
|
|
3986
|
+
]);
|
|
3987
|
+
const out = { result: toSafe(value, 0, []) };
|
|
3988
|
+
if (logs.length)
|
|
3989
|
+
out.logs = logs;
|
|
3990
|
+
return out;
|
|
3991
|
+
} catch (e) {
|
|
3992
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
3993
|
+
if (!err.code)
|
|
3994
|
+
err.code = err.name === "SyntaxError" ? "SYNTAX_ERROR" : "SCRIPT_ERROR";
|
|
3995
|
+
err.line = await scriptLine(err, compiled.offset);
|
|
3996
|
+
err.logs = logs;
|
|
3997
|
+
throw err;
|
|
3998
|
+
} finally {
|
|
3999
|
+
clearTimeout(timer);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
function makeConsole(logs) {
|
|
4003
|
+
function add(level) {
|
|
4004
|
+
return function() {
|
|
4005
|
+
const parts = [];
|
|
4006
|
+
for (let i = 0;i < arguments.length; i++) {
|
|
4007
|
+
const a = arguments[i];
|
|
4008
|
+
parts.push(typeof a === "string" ? a : safeStringify(a));
|
|
4009
|
+
}
|
|
4010
|
+
if (logs.length < 200)
|
|
4011
|
+
logs.push((level === "log" ? "" : level + ": ") + parts.join(" ").slice(0, 2000));
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
return { log: add("log"), info: add("info"), warn: add("warn"), error: add("error"), debug: add("debug") };
|
|
4015
|
+
}
|
|
4016
|
+
var utils = {
|
|
4017
|
+
loadFonts: async function(...fonts) {
|
|
4018
|
+
const names = fontNamesOf(fonts);
|
|
4019
|
+
await Promise.all(names.map(loadFont));
|
|
4020
|
+
return names.length;
|
|
4021
|
+
},
|
|
4022
|
+
node: function(id) {
|
|
4023
|
+
return figma.getNodeByIdAsync(id);
|
|
4024
|
+
},
|
|
4025
|
+
page: async function(nameOrId) {
|
|
4026
|
+
const pages = figma.root.children;
|
|
4027
|
+
let page = null;
|
|
4028
|
+
for (let i = 0;i < pages.length; i++)
|
|
4029
|
+
if (pages[i].id === nameOrId || pages[i].name === nameOrId)
|
|
4030
|
+
page = pages[i];
|
|
4031
|
+
if (!page)
|
|
4032
|
+
throw new Error("Page not found: " + nameOrId);
|
|
4033
|
+
await figma.setCurrentPageAsync(page);
|
|
4034
|
+
return page;
|
|
4035
|
+
},
|
|
4036
|
+
hex: function(hex) {
|
|
4037
|
+
const c = parseHex(hex);
|
|
4038
|
+
return c.a === 1 ? { r: c.r, g: c.g, b: c.b } : c;
|
|
4039
|
+
},
|
|
4040
|
+
solid: function(hex, opacity) {
|
|
4041
|
+
const c = parseHex(hex);
|
|
4042
|
+
return [{ type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: opacity === undefined ? c.a : opacity }];
|
|
4043
|
+
},
|
|
4044
|
+
build: function(spec, options) {
|
|
4045
|
+
return build(Object.assign({}, options || {}, { spec, select: options && options.select === true }));
|
|
4046
|
+
},
|
|
4047
|
+
describe: async function(node, depth) {
|
|
4048
|
+
const id = node && typeof node === "object" ? node.id : node;
|
|
4049
|
+
const out = await describe({ nodeId: id, depth });
|
|
4050
|
+
return out.outline;
|
|
4051
|
+
}
|
|
4052
|
+
};
|
|
4053
|
+
async function getCss(p) {
|
|
4054
|
+
const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
|
|
4055
|
+
if (!node)
|
|
4056
|
+
throw codeError("No nodeId given and nothing is selected", "BAD_ARGS");
|
|
4057
|
+
if (typeof node.getCSSAsync !== "function")
|
|
4058
|
+
throw codeError("A " + node.type + " node has no CSS", "BAD_ARGS");
|
|
4059
|
+
const out = { nodeId: node.id, name: node.name, css: await node.getCSSAsync() };
|
|
4060
|
+
if (p.children && "children" in node) {
|
|
4061
|
+
out.children = [];
|
|
4062
|
+
const kids = node.children.slice(0, 50);
|
|
4063
|
+
for (let i = 0;i < kids.length; i++) {
|
|
4064
|
+
out.children.push({ nodeId: kids[i].id, name: kids[i].name, css: await kids[i].getCSSAsync() });
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
return out;
|
|
4068
|
+
}
|
|
4069
|
+
async function screenshot(p) {
|
|
4070
|
+
const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
|
|
4071
|
+
if (!node)
|
|
4072
|
+
throw codeError("No nodeId given and nothing is selected", "BAD_ARGS");
|
|
4073
|
+
if (typeof node.exportAsync !== "function")
|
|
4074
|
+
throw codeError("A " + node.type + " node cannot be exported", "BAD_ARGS");
|
|
4075
|
+
const box = node.absoluteRenderBounds || node.absoluteBoundingBox || { width: node.width || 1, height: node.height || 1 };
|
|
4076
|
+
let scale = p.scale || 1;
|
|
4077
|
+
if (!p.scale && p.width)
|
|
4078
|
+
scale = Math.max(0.05, Math.min(4, p.width / Math.max(box.width, 1)));
|
|
4079
|
+
if (p.maxDimension)
|
|
4080
|
+
scale = Math.min(scale, p.maxDimension / Math.max(box.width, box.height, 1));
|
|
4081
|
+
const format = p.format === "JPG" ? "JPG" : "PNG";
|
|
4082
|
+
const bytes = await node.exportAsync({ format, constraint: { type: "SCALE", value: scale } });
|
|
4083
|
+
const bb = node.absoluteBoundingBox;
|
|
4084
|
+
const offset = node.absoluteRenderBounds && bb ? { x: round(node.absoluteRenderBounds.x - bb.x), y: round(node.absoluteRenderBounds.y - bb.y) } : { x: 0, y: 0 };
|
|
4085
|
+
return { bytes, format, scale, offset, name: node.name, nodeId: node.id };
|
|
4086
|
+
}
|
|
4087
|
+
async function placeImage(p) {
|
|
4088
|
+
if (!(p.bytes instanceof Uint8Array))
|
|
4089
|
+
throw codeError("No image bytes received", "BAD_ARGS");
|
|
4090
|
+
const image = figma.createImage(p.bytes);
|
|
4091
|
+
const imgSize = await image.getSizeAsync();
|
|
4092
|
+
const paint = { type: "IMAGE", imageHash: image.hash, scaleMode: p.scaleMode || "FILL" };
|
|
4093
|
+
let target;
|
|
4094
|
+
if (p.nodeId) {
|
|
4095
|
+
target = await getNode(p.nodeId);
|
|
4096
|
+
if (!("fills" in target))
|
|
4097
|
+
throw codeError("A " + target.type + " node has no fills", "BAD_ARGS");
|
|
4098
|
+
if (p.name)
|
|
4099
|
+
target.name = p.name;
|
|
4100
|
+
} else {
|
|
4101
|
+
let w = p.width;
|
|
4102
|
+
let h = p.height;
|
|
4103
|
+
if (!w && !h) {
|
|
4104
|
+
w = imgSize.width;
|
|
4105
|
+
h = imgSize.height;
|
|
4106
|
+
} else if (!h)
|
|
4107
|
+
h = w * imgSize.height / imgSize.width;
|
|
4108
|
+
else if (!w)
|
|
4109
|
+
w = h * imgSize.width / imgSize.height;
|
|
4110
|
+
target = figma.createRectangle();
|
|
4111
|
+
target.name = p.name || "Image";
|
|
4112
|
+
target.resize(Math.max(1, w), Math.max(1, h));
|
|
4113
|
+
(await parentFor(p.parentId)).appendChild(target);
|
|
4114
|
+
place(target, p.x, p.y);
|
|
4115
|
+
}
|
|
4116
|
+
target.fills = [paint];
|
|
4117
|
+
return {
|
|
4118
|
+
nodeId: target.id,
|
|
4119
|
+
name: target.name,
|
|
4120
|
+
imageHash: image.hash,
|
|
4121
|
+
width: round(target.width),
|
|
4122
|
+
height: round(target.height),
|
|
4123
|
+
imageWidth: imgSize.width,
|
|
4124
|
+
imageHeight: imgSize.height
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
async function importSvg(p) {
|
|
4128
|
+
if (!p.svg)
|
|
4129
|
+
throw codeError("Empty SVG", "BAD_ARGS");
|
|
4130
|
+
const node = figma.createNodeFromSvg(String(p.svg));
|
|
4131
|
+
if (p.name)
|
|
4132
|
+
node.name = p.name;
|
|
4133
|
+
(await parentFor(p.parentId)).appendChild(node);
|
|
4134
|
+
if (typeof p.size === "number" && node.height > 0)
|
|
4135
|
+
node.rescale(p.size / Math.max(node.width, node.height));
|
|
4136
|
+
if (!(("layoutMode" in node.parent) && node.parent.layoutMode !== "NONE"))
|
|
4137
|
+
place(node, p.x, p.y);
|
|
4138
|
+
return { nodeId: node.id, name: node.name, width: round(node.width), height: round(node.height) };
|
|
4139
|
+
}
|
|
4140
|
+
function bounds(n) {
|
|
4141
|
+
const b = n.absoluteBoundingBox;
|
|
4142
|
+
return b ? { x: round(b.x), y: round(b.y), width: round(b.width), height: round(b.height) } : null;
|
|
4143
|
+
}
|
|
4144
|
+
async function getContext() {
|
|
4145
|
+
const sel = figma.currentPage.selection;
|
|
4146
|
+
const vb = figma.viewport.bounds;
|
|
4147
|
+
return {
|
|
4148
|
+
fileName: figma.root.name,
|
|
4149
|
+
pages: figma.root.children.map(function(pg) {
|
|
4150
|
+
return { id: pg.id, name: pg.name };
|
|
4151
|
+
}),
|
|
4152
|
+
currentPage: { id: figma.currentPage.id, name: figma.currentPage.name },
|
|
4153
|
+
selectionCount: sel.length,
|
|
4154
|
+
selection: sel.slice(0, 100).map(function(n) {
|
|
4155
|
+
return { id: n.id, name: n.name, type: n.type, bounds: bounds(n) };
|
|
4156
|
+
}),
|
|
4157
|
+
viewport: {
|
|
4158
|
+
center: { x: round(figma.viewport.center.x), y: round(figma.viewport.center.y) },
|
|
4159
|
+
zoom: round(figma.viewport.zoom),
|
|
4160
|
+
bounds: { x: round(vb.x), y: round(vb.y), width: round(vb.width), height: round(vb.height) }
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
}
|
|
4164
|
+
var fontCache = null;
|
|
4165
|
+
async function listFonts(p) {
|
|
4166
|
+
if (!fontCache)
|
|
4167
|
+
fontCache = await figma.listAvailableFontsAsync();
|
|
4168
|
+
const filter = String(p.filter || "").toLowerCase();
|
|
4169
|
+
const families = {};
|
|
4170
|
+
const order = [];
|
|
4171
|
+
for (let i = 0;i < fontCache.length; i++) {
|
|
4172
|
+
const f = fontCache[i].fontName;
|
|
4173
|
+
if (filter && f.family.toLowerCase().indexOf(filter) === -1)
|
|
4174
|
+
continue;
|
|
4175
|
+
if (!families[f.family]) {
|
|
4176
|
+
families[f.family] = [];
|
|
4177
|
+
order.push(f.family);
|
|
4178
|
+
}
|
|
4179
|
+
families[f.family].push(f.style);
|
|
4180
|
+
}
|
|
4181
|
+
const limit = p.limit || 100;
|
|
4182
|
+
return {
|
|
4183
|
+
totalFamilies: order.length,
|
|
4184
|
+
families: order.slice(0, limit).map(function(family) {
|
|
4185
|
+
return { family, styles: families[family] };
|
|
4186
|
+
})
|
|
4187
|
+
};
|
|
4188
|
+
}
|
|
4189
|
+
})();
|