@clovis500c/figma-bridge 1.5.0 → 1.12.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/README.md +56 -14
- package/dist/server.js +6557 -983
- package/package.json +9 -3
- package/plugin/code.js +1480 -47
- package/plugin/manifest.json +2 -1
- package/plugin/ui.html +11 -3
package/plugin/code.js
CHANGED
|
@@ -255,6 +255,67 @@
|
|
|
255
255
|
p = p.parent;
|
|
256
256
|
return p;
|
|
257
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
|
+
}
|
|
258
319
|
|
|
259
320
|
// plugin/lib/annotate.ts
|
|
260
321
|
var COLORS = ["yellow", "orange", "red", "pink", "violet", "blue", "teal", "green"];
|
|
@@ -357,11 +418,596 @@
|
|
|
357
418
|
return { count: out.length, nodes: out };
|
|
358
419
|
}
|
|
359
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
|
+
|
|
360
995
|
// plugin/lib/audit.ts
|
|
361
|
-
var
|
|
362
|
-
var
|
|
996
|
+
var MAX_NODES2 = 5000;
|
|
997
|
+
var MAX_ISSUES2 = 120;
|
|
363
998
|
var DEFAULT_NAME = /^(Frame|Rectangle|Ellipse|Group|Vector|Text|Line|Polygon|Star|Component|Instance|Section|Image)( \d+)?$/;
|
|
364
|
-
async function audit(p) {
|
|
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) {
|
|
365
1011
|
let roots;
|
|
366
1012
|
if (p.nodeId)
|
|
367
1013
|
roots = [await getNode(p.nodeId)];
|
|
@@ -380,11 +1026,11 @@
|
|
|
380
1026
|
if (only && only.indexOf(rule) === -1)
|
|
381
1027
|
return;
|
|
382
1028
|
counts[rule] = (counts[rule] || 0) + 1;
|
|
383
|
-
if (issues.length <
|
|
1029
|
+
if (issues.length < MAX_ISSUES2)
|
|
384
1030
|
issues.push({ rule, severity, nodeId: n.id, name: n.name, message });
|
|
385
1031
|
};
|
|
386
1032
|
const visit = function(n, clipBox) {
|
|
387
|
-
if (visited >=
|
|
1033
|
+
if (visited >= MAX_NODES2)
|
|
388
1034
|
return;
|
|
389
1035
|
visited++;
|
|
390
1036
|
if (n.visible === false)
|
|
@@ -455,7 +1101,7 @@
|
|
|
455
1101
|
fonts: familyList,
|
|
456
1102
|
fontSizes: sizeList,
|
|
457
1103
|
issues,
|
|
458
|
-
truncated: visited >=
|
|
1104
|
+
truncated: visited >= MAX_NODES2 || issues.length >= MAX_ISSUES2
|
|
459
1105
|
};
|
|
460
1106
|
}
|
|
461
1107
|
function checkText(n, report, families, sizes, pageBg) {
|
|
@@ -480,19 +1126,21 @@
|
|
|
480
1126
|
if (tb.x + tb.width > pb.x + pb.width + 1 || tb.x < pb.x - 1)
|
|
481
1127
|
report("text-overflow", "warning", n, "Text extends outside its container");
|
|
482
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) {
|
|
483
1135
|
const fg = solidOf(n.fills);
|
|
484
1136
|
if (!fg || n.fontSize === figma.mixed)
|
|
485
|
-
return;
|
|
1137
|
+
return null;
|
|
486
1138
|
const bg = backgroundOf(n, pageBg);
|
|
487
1139
|
if (!bg)
|
|
488
|
-
return;
|
|
489
|
-
const ratio = contrastRatio(fg, bg);
|
|
1140
|
+
return null;
|
|
490
1141
|
const bold = n.fontName !== figma.mixed && /bold|black|heavy|semi/i.test(n.fontName.style);
|
|
491
1142
|
const large = n.fontSize >= 24 || bold && n.fontSize >= 18.66;
|
|
492
|
-
|
|
493
|
-
if (ratio < min) {
|
|
494
|
-
report("contrast", ratio < min - 1.5 ? "error" : "warning", n, "Contrast " + round(ratio) + ":1 is below " + min + ":1 (WCAG AA" + (large ? ", large text" : "") + ")");
|
|
495
|
-
}
|
|
1143
|
+
return { ratio: contrastRatio(fg, bg), min: large ? 3 : 4.5, large };
|
|
496
1144
|
}
|
|
497
1145
|
function solidOf(paints) {
|
|
498
1146
|
if (!paints || paints === figma.mixed)
|
|
@@ -529,6 +1177,186 @@
|
|
|
529
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;
|
|
530
1178
|
}
|
|
531
1179
|
|
|
1180
|
+
// plugin/lib/figjam.ts
|
|
1181
|
+
var EDITORS = { sticky: "figjam", shape: "figjam", connector: "figjam", table: "figjam", codeblock: "figjam", slide: "slides" };
|
|
1182
|
+
var EDITOR_NAMES = { figma: "Figma Design", figjam: "FigJam", slides: "Figma Slides", dev: "Dev Mode" };
|
|
1183
|
+
function requireEditor(type, path) {
|
|
1184
|
+
const wanted = EDITORS[type];
|
|
1185
|
+
if (wanted && figma.editorType !== wanted) {
|
|
1186
|
+
throw codeError(path + ': "' + type + '" needs a ' + EDITOR_NAMES[wanted] + " file; this file is open in " + (EDITOR_NAMES[figma.editorType] || figma.editorType) + ".", "WRONG_EDITOR");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
var STICKY_COLORS = {
|
|
1190
|
+
white: "#FFFFFF",
|
|
1191
|
+
gray: "#E6E6E6",
|
|
1192
|
+
grey: "#E6E6E6",
|
|
1193
|
+
green: "#B3EFBD",
|
|
1194
|
+
teal: "#B3F4EF",
|
|
1195
|
+
blue: "#A8DAFF",
|
|
1196
|
+
violet: "#D3BDFF",
|
|
1197
|
+
purple: "#D3BDFF",
|
|
1198
|
+
pink: "#FFA8DB",
|
|
1199
|
+
red: "#FFB8A8",
|
|
1200
|
+
orange: "#FFD3A8",
|
|
1201
|
+
yellow: "#FFE299"
|
|
1202
|
+
};
|
|
1203
|
+
function solid(color) {
|
|
1204
|
+
const hex = STICKY_COLORS[String(color).toLowerCase()] || color;
|
|
1205
|
+
const c = parseHex(hex);
|
|
1206
|
+
return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
|
|
1207
|
+
}
|
|
1208
|
+
async function setSublayerText(sub, text, opts) {
|
|
1209
|
+
const font = sub.fontName;
|
|
1210
|
+
if (font !== figma.mixed)
|
|
1211
|
+
await loadFont(font);
|
|
1212
|
+
else if (sub.characters.length) {
|
|
1213
|
+
const fonts = sub.getRangeAllFontNames(0, sub.characters.length);
|
|
1214
|
+
for (let i = 0;i < fonts.length; i++)
|
|
1215
|
+
await loadFont(fonts[i]);
|
|
1216
|
+
}
|
|
1217
|
+
if (opts && opts.font) {
|
|
1218
|
+
await loadFont(opts.font);
|
|
1219
|
+
sub.fontName = opts.font;
|
|
1220
|
+
}
|
|
1221
|
+
sub.characters = String(text === undefined || text === null ? "" : text);
|
|
1222
|
+
if (opts && typeof opts.size === "number")
|
|
1223
|
+
sub.fontSize = opts.size;
|
|
1224
|
+
if (opts && opts.color)
|
|
1225
|
+
sub.fills = [solid(opts.color)];
|
|
1226
|
+
}
|
|
1227
|
+
async function createSticky(s) {
|
|
1228
|
+
const n = figma.createSticky();
|
|
1229
|
+
if (s.wide)
|
|
1230
|
+
n.isWideWidth = true;
|
|
1231
|
+
if (s.author === false)
|
|
1232
|
+
n.authorVisible = false;
|
|
1233
|
+
if (s.color || s.fill)
|
|
1234
|
+
n.fills = [solid(s.color || s.fill)];
|
|
1235
|
+
await setSublayerText(n.text, s.text);
|
|
1236
|
+
return n;
|
|
1237
|
+
}
|
|
1238
|
+
var SHAPES = {
|
|
1239
|
+
square: "SQUARE",
|
|
1240
|
+
rect: "SQUARE",
|
|
1241
|
+
rectangle: "SQUARE",
|
|
1242
|
+
rounded: "ROUNDED_RECTANGLE",
|
|
1243
|
+
roundedrectangle: "ROUNDED_RECTANGLE",
|
|
1244
|
+
ellipse: "ELLIPSE",
|
|
1245
|
+
circle: "ELLIPSE",
|
|
1246
|
+
diamond: "DIAMOND",
|
|
1247
|
+
decision: "DIAMOND",
|
|
1248
|
+
triangle: "TRIANGLE_UP",
|
|
1249
|
+
triangleup: "TRIANGLE_UP",
|
|
1250
|
+
triangledown: "TRIANGLE_DOWN",
|
|
1251
|
+
parallelogram: "PARALLELOGRAM_RIGHT",
|
|
1252
|
+
parallelogramleft: "PARALLELOGRAM_LEFT",
|
|
1253
|
+
database: "ENG_DATABASE",
|
|
1254
|
+
cylinder: "ENG_DATABASE",
|
|
1255
|
+
queue: "ENG_QUEUE",
|
|
1256
|
+
file: "ENG_FILE",
|
|
1257
|
+
folder: "ENG_FOLDER",
|
|
1258
|
+
trapezoid: "TRAPEZOID",
|
|
1259
|
+
process: "PREDEFINED_PROCESS",
|
|
1260
|
+
subroutine: "PREDEFINED_PROCESS",
|
|
1261
|
+
shield: "SHIELD",
|
|
1262
|
+
document: "DOCUMENT_SINGLE",
|
|
1263
|
+
documents: "DOCUMENT_MULTIPLE",
|
|
1264
|
+
input: "MANUAL_INPUT",
|
|
1265
|
+
hexagon: "HEXAGON",
|
|
1266
|
+
chevron: "CHEVRON",
|
|
1267
|
+
pentagon: "PENTAGON",
|
|
1268
|
+
octagon: "OCTAGON",
|
|
1269
|
+
star: "STAR",
|
|
1270
|
+
plus: "PLUS",
|
|
1271
|
+
arrowleft: "ARROW_LEFT",
|
|
1272
|
+
arrowright: "ARROW_RIGHT",
|
|
1273
|
+
junction: "SUMMING_JUNCTION",
|
|
1274
|
+
or: "OR",
|
|
1275
|
+
speech: "SPEECH_BUBBLE",
|
|
1276
|
+
speechbubble: "SPEECH_BUBBLE",
|
|
1277
|
+
storage: "INTERNAL_STORAGE"
|
|
1278
|
+
};
|
|
1279
|
+
async function createShape(s, path, warnings) {
|
|
1280
|
+
const n = figma.createShapeWithText();
|
|
1281
|
+
const key = String(s.shape || "rounded").toLowerCase().replace(/[^a-z]/g, "");
|
|
1282
|
+
const shape = SHAPES[key] || String(s.shape || "").toUpperCase();
|
|
1283
|
+
try {
|
|
1284
|
+
n.shapeType = shape;
|
|
1285
|
+
} catch (e) {
|
|
1286
|
+
warnings.push(path + ': unknown shape "' + s.shape + '", used a rounded rectangle');
|
|
1287
|
+
n.shapeType = "ROUNDED_RECTANGLE";
|
|
1288
|
+
}
|
|
1289
|
+
const w = typeof s.w === "number" ? s.w : typeof s.width === "number" ? s.width : 0;
|
|
1290
|
+
const h = typeof s.h === "number" ? s.h : typeof s.height === "number" ? s.height : 0;
|
|
1291
|
+
if (w || h)
|
|
1292
|
+
n.resize(w || n.width, h || n.height);
|
|
1293
|
+
if (s.fill !== undefined)
|
|
1294
|
+
n.fills = s.fill === null ? [] : [solid(s.fill)];
|
|
1295
|
+
if (s.stroke !== undefined)
|
|
1296
|
+
n.strokes = s.stroke === null ? [] : [solid(s.stroke)];
|
|
1297
|
+
if (typeof s.strokeWidth === "number")
|
|
1298
|
+
n.strokeWeight = s.strokeWidth;
|
|
1299
|
+
if (s.text !== undefined)
|
|
1300
|
+
await setSublayerText(n.text, s.text, { size: s.size, color: s.color });
|
|
1301
|
+
return n;
|
|
1302
|
+
}
|
|
1303
|
+
async function createTable(s, path) {
|
|
1304
|
+
const rows = Array.isArray(s.rows) ? s.rows : [];
|
|
1305
|
+
if (!rows.length)
|
|
1306
|
+
throw codeError(path + ': a table needs rows:[["Header 1","Header 2"],["a","b"]]', "BAD_ARGS");
|
|
1307
|
+
let cols = 0;
|
|
1308
|
+
for (let i = 0;i < rows.length; i++)
|
|
1309
|
+
cols = Math.max(cols, Array.isArray(rows[i]) ? rows[i].length : 1);
|
|
1310
|
+
const t = figma.createTable(rows.length, cols);
|
|
1311
|
+
for (let r = 0;r < rows.length; r++) {
|
|
1312
|
+
for (let c = 0;c < cols; c++) {
|
|
1313
|
+
const value = Array.isArray(rows[r]) ? rows[r][c] : c === 0 ? rows[r] : "";
|
|
1314
|
+
const cell = t.cellAt(r, c);
|
|
1315
|
+
await setSublayerText(cell.text, value === undefined ? "" : value);
|
|
1316
|
+
if (r === 0 && s.header !== false)
|
|
1317
|
+
cell.fills = [solid(s.headerFill || "#F2F2F2")];
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return t;
|
|
1321
|
+
}
|
|
1322
|
+
var LANGUAGES = ["TYPESCRIPT", "CPP", "RUBY", "CSS", "JAVASCRIPT", "HTML", "JSON", "GRAPHQL", "PYTHON", "GO", "SQL", "SWIFT", "KOTLIN", "RUST", "BASH", "PLAINTEXT", "DART"];
|
|
1323
|
+
var LANGUAGE_ALIASES = { ts: "TYPESCRIPT", tsx: "TYPESCRIPT", js: "JAVASCRIPT", jsx: "JAVASCRIPT", "c++": "CPP", py: "PYTHON", sh: "BASH", shell: "BASH", text: "PLAINTEXT", txt: "PLAINTEXT", golang: "GO", rb: "RUBY" };
|
|
1324
|
+
function createCodeBlock(s) {
|
|
1325
|
+
const n = figma.createCodeBlock();
|
|
1326
|
+
const lang = String(s.language || "plaintext").toLowerCase();
|
|
1327
|
+
const code = LANGUAGE_ALIASES[lang] || lang.toUpperCase();
|
|
1328
|
+
n.codeLanguage = LANGUAGES.indexOf(code) !== -1 ? code : "PLAINTEXT";
|
|
1329
|
+
n.code = String(s.code === undefined ? "" : s.code);
|
|
1330
|
+
return n;
|
|
1331
|
+
}
|
|
1332
|
+
function createSlide() {
|
|
1333
|
+
return figma.createSlide();
|
|
1334
|
+
}
|
|
1335
|
+
var CAPS = { arrow: "ARROW_LINES", triangle: "ARROW_EQUILATERAL", filled: "TRIANGLE_FILLED", diamond: "DIAMOND_FILLED", circle: "CIRCLE_FILLED", none: "NONE" };
|
|
1336
|
+
var LINES = { elbowed: "ELBOWED", elbow: "ELBOWED", straight: "STRAIGHT", curved: "CURVED", curve: "CURVED" };
|
|
1337
|
+
var MAGNETS = ["AUTO", "TOP", "LEFT", "BOTTOM", "RIGHT", "CENTER", "NONE"];
|
|
1338
|
+
async function createConnector(s, fromId, toId) {
|
|
1339
|
+
const n = figma.createConnector();
|
|
1340
|
+
const magnet = function(v) {
|
|
1341
|
+
const m = String(v || "AUTO").toUpperCase();
|
|
1342
|
+
return MAGNETS.indexOf(m) !== -1 ? m : "AUTO";
|
|
1343
|
+
};
|
|
1344
|
+
n.connectorStart = { endpointNodeId: fromId, magnet: magnet(s.fromMagnet) };
|
|
1345
|
+
n.connectorEnd = { endpointNodeId: toId, magnet: magnet(s.toMagnet) };
|
|
1346
|
+
n.connectorLineType = LINES[String(s.line || "elbowed").toLowerCase()] || "ELBOWED";
|
|
1347
|
+
n.connectorEndStrokeCap = CAPS[String(s.endArrow === undefined ? "arrow" : s.endArrow).toLowerCase()] || "ARROW_LINES";
|
|
1348
|
+
n.connectorStartStrokeCap = CAPS[String(s.startArrow === undefined ? "none" : s.startArrow).toLowerCase()] || "NONE";
|
|
1349
|
+
if (s.color || s.stroke)
|
|
1350
|
+
n.strokes = [solid(s.color || s.stroke)];
|
|
1351
|
+
if (typeof s.strokeWidth === "number")
|
|
1352
|
+
n.strokeWeight = s.strokeWidth;
|
|
1353
|
+
if (s.dashed)
|
|
1354
|
+
n.dashPattern = [8, 6];
|
|
1355
|
+
if (s.label !== undefined && s.label !== "")
|
|
1356
|
+
await setSublayerText(n.text, s.label);
|
|
1357
|
+
return n;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
532
1360
|
// plugin/lib/prototype.ts
|
|
533
1361
|
var TRIGGERS = {
|
|
534
1362
|
click: "ON_CLICK",
|
|
@@ -1109,13 +1937,15 @@
|
|
|
1109
1937
|
}
|
|
1110
1938
|
|
|
1111
1939
|
// plugin/lib/build.ts
|
|
1112
|
-
var
|
|
1940
|
+
var MAX_NODES3 = 3000;
|
|
1113
1941
|
var SHADOW_DEFAULT = { x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" };
|
|
1114
1942
|
async function build(p, _timeoutMs, requestId) {
|
|
1115
1943
|
const spec = p.spec;
|
|
1116
1944
|
if (!spec || typeof spec !== "object")
|
|
1117
1945
|
throw codeError("`spec` must be a node object or an array of node objects", "BAD_ARGS");
|
|
1118
|
-
const roots = Array.isArray(spec) ? spec :
|
|
1946
|
+
const roots = Array.isArray(spec) ? spec : Array.isArray(spec.slides) ? spec.slides.map(function(sl) {
|
|
1947
|
+
return Object.assign({ type: "slide" }, sl);
|
|
1948
|
+
}) : [spec];
|
|
1119
1949
|
const d = p.defaults || {};
|
|
1120
1950
|
const ctx = {
|
|
1121
1951
|
warnings: [],
|
|
@@ -1126,6 +1956,8 @@
|
|
|
1126
1956
|
defaults: { font: d.font || "Inter", color: d.color || "#111111", size: d.size || 14 },
|
|
1127
1957
|
binds: [],
|
|
1128
1958
|
reactions: [],
|
|
1959
|
+
connectors: [],
|
|
1960
|
+
keys: {},
|
|
1129
1961
|
requestId,
|
|
1130
1962
|
total: countNodes(roots),
|
|
1131
1963
|
lastProgress: 0
|
|
@@ -1138,7 +1970,7 @@
|
|
|
1138
1970
|
const node = await createNode(roots[i], parent, ctx, "spec" + (roots.length > 1 ? "[" + i + "]" : ""));
|
|
1139
1971
|
if (!node)
|
|
1140
1972
|
continue;
|
|
1141
|
-
if (!isAutoLayout(parent) && roots[i].x === undefined && roots[i].y === undefined) {
|
|
1973
|
+
if (node.type !== "SLIDE" && !isAutoLayout(parent) && roots[i].x === undefined && roots[i].y === undefined) {
|
|
1142
1974
|
if (cursorX === null) {
|
|
1143
1975
|
place(node, p.x, p.y);
|
|
1144
1976
|
} else {
|
|
@@ -1151,6 +1983,7 @@
|
|
|
1151
1983
|
}
|
|
1152
1984
|
if (ctx.reactions.length)
|
|
1153
1985
|
await applyBuildReactions(ctx.reactions, ctx.ids, ctx.warnings);
|
|
1986
|
+
const connectors = await createConnectors(ctx);
|
|
1154
1987
|
for (let i = 0;i < ctx.binds.length; i++)
|
|
1155
1988
|
ctx.warnings.push(ctx.binds[i].path + ": bind needs a component or componentSet ancestor, ignored");
|
|
1156
1989
|
if (p.select !== false && made.length && parent.type === "PAGE" && parent === figma.currentPage) {
|
|
@@ -1162,7 +1995,7 @@
|
|
|
1162
1995
|
rootIds: made.map(function(n) {
|
|
1163
1996
|
return n.id;
|
|
1164
1997
|
}),
|
|
1165
|
-
created: ctx.count,
|
|
1998
|
+
created: ctx.count + connectors,
|
|
1166
1999
|
ids: ctx.ids
|
|
1167
2000
|
};
|
|
1168
2001
|
if (ctx.warnings.length)
|
|
@@ -1243,7 +2076,7 @@
|
|
|
1243
2076
|
function nodeType(s) {
|
|
1244
2077
|
if (s.type) {
|
|
1245
2078
|
const t = String(s.type).toLowerCase().replace(/[_\s-]/g, "");
|
|
1246
|
-
return t === "rectangle" ? "rect" : t === "circle" ? "ellipse" : t === "variants" ? "componentset" : t;
|
|
2079
|
+
return t === "rectangle" ? "rect" : t === "circle" ? "ellipse" : t === "variants" ? "componentset" : t === "shapewithtext" ? "shape" : t === "code" ? "codeblock" : t;
|
|
1247
2080
|
}
|
|
1248
2081
|
if (Array.isArray(s.variants))
|
|
1249
2082
|
return "componentset";
|
|
@@ -1264,12 +2097,34 @@
|
|
|
1264
2097
|
ctx.warnings.push(path + ": not an object, skipped");
|
|
1265
2098
|
return null;
|
|
1266
2099
|
}
|
|
1267
|
-
if (ctx.count >=
|
|
1268
|
-
throw codeError("Spec is too large (max " +
|
|
2100
|
+
if (ctx.count >= MAX_NODES3)
|
|
2101
|
+
throw codeError("Spec is too large (max " + MAX_NODES3 + " nodes). Split it into several build calls.", "TOO_LARGE");
|
|
1269
2102
|
checkCancelled(ctx.requestId);
|
|
1270
2103
|
const type = nodeType(s);
|
|
2104
|
+
requireEditor(type, path);
|
|
1271
2105
|
let node;
|
|
1272
2106
|
switch (type) {
|
|
2107
|
+
case "connector":
|
|
2108
|
+
ctx.connectors.push({ spec: s, parent, path });
|
|
2109
|
+
return null;
|
|
2110
|
+
case "sticky":
|
|
2111
|
+
node = await createSticky(s);
|
|
2112
|
+
break;
|
|
2113
|
+
case "shape":
|
|
2114
|
+
node = await createShape(s, path, ctx.warnings);
|
|
2115
|
+
break;
|
|
2116
|
+
case "table":
|
|
2117
|
+
node = await createTable(s, path);
|
|
2118
|
+
break;
|
|
2119
|
+
case "codeblock":
|
|
2120
|
+
node = createCodeBlock(s);
|
|
2121
|
+
break;
|
|
2122
|
+
case "section":
|
|
2123
|
+
node = figma.createSection();
|
|
2124
|
+
break;
|
|
2125
|
+
case "slide":
|
|
2126
|
+
node = createSlide();
|
|
2127
|
+
break;
|
|
1273
2128
|
case "frame":
|
|
1274
2129
|
case "component":
|
|
1275
2130
|
node = type === "component" ? figma.createComponent() : figma.createFrame();
|
|
@@ -1306,8 +2161,11 @@
|
|
|
1306
2161
|
}
|
|
1307
2162
|
ctx.count++;
|
|
1308
2163
|
progress(ctx);
|
|
1309
|
-
parent.
|
|
2164
|
+
if (type !== "slide" || parent.type !== "PAGE")
|
|
2165
|
+
parent.appendChild(node);
|
|
1310
2166
|
const parentAuto = isAutoLayout(parent);
|
|
2167
|
+
if (s.key !== undefined)
|
|
2168
|
+
ctx.keys[String(s.key)] = node.id;
|
|
1311
2169
|
if (s.bind)
|
|
1312
2170
|
ctx.binds.push({ node, bind: s.bind, path });
|
|
1313
2171
|
const bindStart = ctx.binds.length;
|
|
@@ -1317,6 +2175,10 @@
|
|
|
1317
2175
|
node.name = "icon/" + s.icon;
|
|
1318
2176
|
if (type === "frame" || type === "component")
|
|
1319
2177
|
await setupFrame(node, s, ctx, path);
|
|
2178
|
+
else if (type === "slide")
|
|
2179
|
+
await setupFrame(node, s, ctx, path, true);
|
|
2180
|
+
else if (type === "section")
|
|
2181
|
+
await setupSection(node, s, ctx, path);
|
|
1320
2182
|
else if (type === "componentset")
|
|
1321
2183
|
await setupLayout(node, Object.assign({ layout: "row", wrap: true, gap: 16, padding: 16 }, s), ctx, path);
|
|
1322
2184
|
else if (type === "text")
|
|
@@ -1326,7 +2188,7 @@
|
|
|
1326
2188
|
else if (type === "line") {
|
|
1327
2189
|
node.resize(num(s.w, s.width, 100), 0);
|
|
1328
2190
|
if (s.stroke === undefined)
|
|
1329
|
-
node.strokes = [
|
|
2191
|
+
node.strokes = [solid2(ctx.defaults.color)];
|
|
1330
2192
|
} else if ((type === "rect" || type === "ellipse") && s.w === undefined && s.width === undefined && s.size === undefined) {
|
|
1331
2193
|
node.resize(100, 100);
|
|
1332
2194
|
}
|
|
@@ -1334,14 +2196,18 @@
|
|
|
1334
2196
|
await overrideTexts(node, s.text, ctx);
|
|
1335
2197
|
if (type === "componentset" && s.stroke === undefined) {
|
|
1336
2198
|
const set = node;
|
|
1337
|
-
set.strokes = [
|
|
2199
|
+
set.strokes = [solid2("#9747FF")];
|
|
1338
2200
|
set.dashPattern = [10, 5];
|
|
1339
2201
|
set.cornerRadius = 5;
|
|
1340
2202
|
}
|
|
1341
|
-
|
|
2203
|
+
if (FIXED_LOOK.indexOf(type) === -1)
|
|
2204
|
+
await applyVisuals(node, s, ctx, type);
|
|
2205
|
+
else if (typeof s.opacity === "number")
|
|
2206
|
+
node.opacity = s.opacity;
|
|
1342
2207
|
if (type === "text" && Array.isArray(s.spans))
|
|
1343
2208
|
await applySpans(node, s, ctx, path);
|
|
1344
|
-
|
|
2209
|
+
if (FIXED_LOOK.indexOf(type) === -1 && type !== "section" && type !== "slide")
|
|
2210
|
+
applySize(node, s, parentAuto, type, ctx, path);
|
|
1345
2211
|
if (type === "component" && !s.variantOf)
|
|
1346
2212
|
await addProperties(node, s.properties, ctx.binds.splice(bindStart), ctx, path);
|
|
1347
2213
|
if (parent.layoutMode === "GRID")
|
|
@@ -1356,7 +2222,7 @@
|
|
|
1356
2222
|
}
|
|
1357
2223
|
if (s.grow && parentAuto)
|
|
1358
2224
|
node.layoutGrow = 1;
|
|
1359
|
-
if (typeof s.rotation === "number")
|
|
2225
|
+
if (typeof s.rotation === "number" && "rotation" in node)
|
|
1360
2226
|
node.rotation = s.rotation;
|
|
1361
2227
|
if (s.visible === false)
|
|
1362
2228
|
node.visible = false;
|
|
@@ -1375,20 +2241,83 @@
|
|
|
1375
2241
|
}
|
|
1376
2242
|
return node;
|
|
1377
2243
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
2244
|
+
var FIXED_LOOK = ["sticky", "shape", "table", "codeblock"];
|
|
2245
|
+
async function setupFrame(f, s, ctx, path, keepFills) {
|
|
2246
|
+
if (!keepFills)
|
|
2247
|
+
f.fills = [];
|
|
1380
2248
|
await setupLayout(f, s, ctx, path);
|
|
1381
2249
|
const children = Array.isArray(s.children) ? s.children : [];
|
|
1382
2250
|
for (let i = 0;i < children.length; i++) {
|
|
1383
2251
|
try {
|
|
1384
2252
|
await createNode(children[i], f, ctx, path + ".children[" + i + "]");
|
|
1385
2253
|
} catch (e) {
|
|
1386
|
-
if (e.code === "TOO_LARGE" || e.code === "CANCELLED")
|
|
2254
|
+
if (e.code === "TOO_LARGE" || e.code === "CANCELLED" || e.code === "WRONG_EDITOR")
|
|
1387
2255
|
throw e;
|
|
1388
2256
|
ctx.warnings.push(path + ".children[" + i + "]: " + (e.message || e));
|
|
1389
2257
|
}
|
|
1390
2258
|
}
|
|
1391
2259
|
}
|
|
2260
|
+
async function setupSection(sec, s, ctx, path) {
|
|
2261
|
+
const children = Array.isArray(s.children) ? s.children : [];
|
|
2262
|
+
const made = [];
|
|
2263
|
+
for (let i = 0;i < children.length; i++) {
|
|
2264
|
+
try {
|
|
2265
|
+
const n = await createNode(children[i], sec, ctx, path + ".children[" + i + "]");
|
|
2266
|
+
if (n)
|
|
2267
|
+
made.push(n);
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
if (e.code === "TOO_LARGE" || e.code === "CANCELLED" || e.code === "WRONG_EDITOR")
|
|
2270
|
+
throw e;
|
|
2271
|
+
ctx.warnings.push(path + ".children[" + i + "]: " + (e.message || e));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const w = num(s.w, s.width, 0);
|
|
2275
|
+
const h = num(s.h, s.height, 0);
|
|
2276
|
+
if (w && h) {
|
|
2277
|
+
sec.resizeWithoutConstraints(w, h);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
const pad = typeof s.padding === "number" ? s.padding : 40;
|
|
2281
|
+
let right = 0;
|
|
2282
|
+
let bottom = 0;
|
|
2283
|
+
for (let i = 0;i < made.length; i++) {
|
|
2284
|
+
right = Math.max(right, made[i].x + made[i].width);
|
|
2285
|
+
bottom = Math.max(bottom, made[i].y + made[i].height);
|
|
2286
|
+
}
|
|
2287
|
+
sec.resizeWithoutConstraints(Math.max(w || right + pad, 100), Math.max(h || bottom + pad, 100));
|
|
2288
|
+
}
|
|
2289
|
+
async function createConnectors(ctx) {
|
|
2290
|
+
let made = 0;
|
|
2291
|
+
for (let i = 0;i < ctx.connectors.length; i++) {
|
|
2292
|
+
const c = ctx.connectors[i];
|
|
2293
|
+
const ends = [];
|
|
2294
|
+
const sides = ["from", "to"];
|
|
2295
|
+
for (let k = 0;k < 2; k++) {
|
|
2296
|
+
const ref = c.spec[sides[k]];
|
|
2297
|
+
let id = ref === undefined ? "" : ctx.keys[String(ref)] || ctx.ids[String(ref)] || "";
|
|
2298
|
+
if (!id && ref !== undefined && /^[\dI;:]+$/.test(String(ref))) {
|
|
2299
|
+
const n = await figma.getNodeByIdAsync(String(ref));
|
|
2300
|
+
if (n)
|
|
2301
|
+
id = n.id;
|
|
2302
|
+
}
|
|
2303
|
+
ends.push(id);
|
|
2304
|
+
}
|
|
2305
|
+
if (!ends[0] || !ends[1]) {
|
|
2306
|
+
ctx.warnings.push(c.path + ': connector ends not found (from "' + c.spec.from + '", to "' + c.spec.to + '")');
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
try {
|
|
2310
|
+
const n = await createConnector(c.spec, ends[0], ends[1]);
|
|
2311
|
+
c.parent.appendChild(n);
|
|
2312
|
+
if (c.spec.name)
|
|
2313
|
+
n.name = String(c.spec.name);
|
|
2314
|
+
made++;
|
|
2315
|
+
} catch (e) {
|
|
2316
|
+
ctx.warnings.push(c.path + ": " + (e.message || e));
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
return made;
|
|
2320
|
+
}
|
|
1392
2321
|
async function setupLayout(f, s, ctx, path) {
|
|
1393
2322
|
const layout = String(s.layout || s.direction || "").toLowerCase();
|
|
1394
2323
|
let mode = layout === "row" || layout === "horizontal" ? "HORIZONTAL" : layout === "column" || layout === "col" || layout === "vertical" ? "VERTICAL" : layout === "grid" ? "GRID" : "NONE";
|
|
@@ -1422,7 +2351,7 @@
|
|
|
1422
2351
|
if (s.rowGap !== undefined)
|
|
1423
2352
|
await setNumber(f, "counterAxisSpacing", s.rowGap);
|
|
1424
2353
|
}
|
|
1425
|
-
} else if (s.w === undefined && s.width === undefined && s.size === undefined) {
|
|
2354
|
+
} else if (s.w === undefined && s.width === undefined && s.size === undefined && f.type !== "SLIDE") {
|
|
1426
2355
|
f.resize(100, 100);
|
|
1427
2356
|
}
|
|
1428
2357
|
}
|
|
@@ -1562,7 +2491,7 @@
|
|
|
1562
2491
|
t.maxLines = s.maxLines;
|
|
1563
2492
|
}
|
|
1564
2493
|
if (s.fill === undefined && s.color === undefined && !s.textStyle)
|
|
1565
|
-
t.fills = [
|
|
2494
|
+
t.fills = [solid2(ctx.defaults.color)];
|
|
1566
2495
|
if (s.textStyle) {
|
|
1567
2496
|
const style = await findStyle("text", stripPrefix(s.textStyle));
|
|
1568
2497
|
await loadFont(style.fontName);
|
|
@@ -1586,7 +2515,7 @@
|
|
|
1586
2515
|
if (typeof color === "string" && color.indexOf("style:") === 0) {
|
|
1587
2516
|
await t.setRangeFillStyleIdAsync(at, end, (await findStyle("paint", stripPrefix(color))).id);
|
|
1588
2517
|
} else if (color !== undefined) {
|
|
1589
|
-
t.setRangeFills(at, end, [await toPaint(color)]);
|
|
2518
|
+
t.setRangeFills(at, end, [await toPaint(color, ctx.images)]);
|
|
1590
2519
|
}
|
|
1591
2520
|
const deco = { underline: "UNDERLINE", strike: "STRIKETHROUGH", strikethrough: "STRIKETHROUGH", none: "NONE" };
|
|
1592
2521
|
if (sp.link) {
|
|
@@ -1596,7 +2525,7 @@
|
|
|
1596
2525
|
}
|
|
1597
2526
|
if (sp.decoration && deco[sp.decoration])
|
|
1598
2527
|
t.setRangeTextDecoration(at, end, deco[sp.decoration]);
|
|
1599
|
-
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE" };
|
|
2528
|
+
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE", none: "ORIGINAL" };
|
|
1600
2529
|
if (sp.case && cases[sp.case])
|
|
1601
2530
|
t.setRangeTextCase(at, end, cases[sp.case]);
|
|
1602
2531
|
} catch (e) {
|
|
@@ -1782,10 +2711,20 @@
|
|
|
1782
2711
|
async function applyVisuals(node, s, ctx, type) {
|
|
1783
2712
|
const fill = s.fill !== undefined ? s.fill : type === "text" ? s.color : undefined;
|
|
1784
2713
|
if (fill !== undefined && "fills" in node && type !== "image")
|
|
1785
|
-
await setPaints(node, "fills", fill);
|
|
2714
|
+
await setPaints(node, "fills", fill, ctx.images);
|
|
1786
2715
|
if (s.stroke !== undefined && "strokes" in node) {
|
|
1787
|
-
await setPaints(node, "strokes", s.stroke);
|
|
1788
|
-
|
|
2716
|
+
await setPaints(node, "strokes", s.stroke, ctx.images);
|
|
2717
|
+
if (Array.isArray(s.strokeWidth) && "strokeTopWeight" in node) {
|
|
2718
|
+
const w = s.strokeWidth.map(Number);
|
|
2719
|
+
node.strokeTopWeight = w[0] || 0;
|
|
2720
|
+
node.strokeRightWeight = w[1] === undefined ? w[0] || 0 : w[1] || 0;
|
|
2721
|
+
node.strokeBottomWeight = w[2] === undefined ? w[0] || 0 : w[2] || 0;
|
|
2722
|
+
node.strokeLeftWeight = w[3] === undefined ? w[1] === undefined ? w[0] || 0 : w[1] || 0 : w[3] || 0;
|
|
2723
|
+
} else {
|
|
2724
|
+
node.strokeWeight = typeof s.strokeWidth === "number" ? s.strokeWidth : 1;
|
|
2725
|
+
}
|
|
2726
|
+
if (Array.isArray(s.strokeDash) && "dashPattern" in node)
|
|
2727
|
+
node.dashPattern = s.strokeDash.map(Number);
|
|
1789
2728
|
if ("strokeAlign" in node && type !== "line" && type !== "text") {
|
|
1790
2729
|
node.strokeAlign = String(s.strokeAlign || "inside").toUpperCase();
|
|
1791
2730
|
}
|
|
@@ -1839,11 +2778,11 @@
|
|
|
1839
2778
|
function stripPrefix(v) {
|
|
1840
2779
|
return String(v).replace(/^(style|var):/, "");
|
|
1841
2780
|
}
|
|
1842
|
-
function
|
|
2781
|
+
function solid2(hex) {
|
|
1843
2782
|
const c = parseHex(hex);
|
|
1844
2783
|
return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
|
|
1845
2784
|
}
|
|
1846
|
-
async function setPaints(node, field, value) {
|
|
2785
|
+
async function setPaints(node, field, value, images) {
|
|
1847
2786
|
if (typeof value === "string" && value.indexOf("style:") === 0) {
|
|
1848
2787
|
const style = await findStyle("paint", stripPrefix(value));
|
|
1849
2788
|
if (field === "fills")
|
|
@@ -1855,16 +2794,17 @@
|
|
|
1855
2794
|
const list = value === null || value === "none" ? [] : Array.isArray(value) ? value : [value];
|
|
1856
2795
|
const paints = [];
|
|
1857
2796
|
for (let i = 0;i < list.length; i++)
|
|
1858
|
-
paints.push(await toPaint(list[i]));
|
|
2797
|
+
paints.push(await toPaint(list[i], images));
|
|
1859
2798
|
node[field] = paints;
|
|
1860
2799
|
}
|
|
1861
|
-
|
|
2800
|
+
var FITS = { fill: "FILL", cover: "FILL", fit: "FIT", contain: "FIT", crop: "CROP", tile: "TILE" };
|
|
2801
|
+
async function toPaint(v, images) {
|
|
1862
2802
|
if (typeof v === "string") {
|
|
1863
2803
|
if (v.indexOf("var:") === 0) {
|
|
1864
2804
|
const variable = await findVariable(stripPrefix(v));
|
|
1865
|
-
return figma.variables.setBoundVariableForPaint(
|
|
2805
|
+
return figma.variables.setBoundVariableForPaint(solid2("#000000"), "color", variable);
|
|
1866
2806
|
}
|
|
1867
|
-
return
|
|
2807
|
+
return solid2(v);
|
|
1868
2808
|
}
|
|
1869
2809
|
if (v && Array.isArray(v.gradient)) {
|
|
1870
2810
|
const stops = v.gradient.map(function(c, i, all) {
|
|
@@ -1886,6 +2826,15 @@
|
|
|
1886
2826
|
];
|
|
1887
2827
|
return { type: radial ? "GRADIENT_RADIAL" : "GRADIENT_LINEAR", gradientTransform: transform, gradientStops: stops };
|
|
1888
2828
|
}
|
|
2829
|
+
if (v && v.imageKey !== undefined) {
|
|
2830
|
+
const bytes = images && images[v.imageKey];
|
|
2831
|
+
if (!bytes)
|
|
2832
|
+
throw codeError("Image fill data missing (use {image: src} with the build tool)", "BAD_ARGS");
|
|
2833
|
+
const paint = { type: "IMAGE", imageHash: figma.createImage(bytes).hash, scaleMode: FITS[String(v.fit || "fill").toLowerCase()] || "FILL" };
|
|
2834
|
+
if (typeof v.opacity === "number")
|
|
2835
|
+
return Object.assign({}, paint, { opacity: v.opacity });
|
|
2836
|
+
return paint;
|
|
2837
|
+
}
|
|
1889
2838
|
if (v && typeof v.type === "string")
|
|
1890
2839
|
return v;
|
|
1891
2840
|
throw codeError("Invalid paint: " + JSON.stringify(v), "BAD_ARGS");
|
|
@@ -2313,7 +3262,7 @@
|
|
|
2313
3262
|
}
|
|
2314
3263
|
|
|
2315
3264
|
// plugin/lib/export.ts
|
|
2316
|
-
var
|
|
3265
|
+
var MAX_NODES4 = 1500;
|
|
2317
3266
|
var MAX_ASSET_BYTES = 40 << 20;
|
|
2318
3267
|
var VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "ELLIPSE", "LINE"];
|
|
2319
3268
|
var VISUAL_CSS = [
|
|
@@ -2423,6 +3372,11 @@
|
|
|
2423
3372
|
}
|
|
2424
3373
|
if (n.rotation && Math.abs(n.rotation) > 0.01 && n.type !== "TEXT")
|
|
2425
3374
|
out.rotation = round(n.rotation);
|
|
3375
|
+
if (n.type === "INSTANCE") {
|
|
3376
|
+
const info = await instanceInfo(n);
|
|
3377
|
+
if (info)
|
|
3378
|
+
out.component = info;
|
|
3379
|
+
}
|
|
2426
3380
|
let css = {};
|
|
2427
3381
|
try {
|
|
2428
3382
|
css = await n.getCSSAsync();
|
|
@@ -2487,7 +3441,7 @@
|
|
|
2487
3441
|
const c = n.children[i];
|
|
2488
3442
|
if (!c.visible)
|
|
2489
3443
|
continue;
|
|
2490
|
-
if (ctx.count >=
|
|
3444
|
+
if (ctx.count >= MAX_NODES4) {
|
|
2491
3445
|
ctx.truncated = true;
|
|
2492
3446
|
break;
|
|
2493
3447
|
}
|
|
@@ -2530,6 +3484,33 @@
|
|
|
2530
3484
|
})
|
|
2531
3485
|
};
|
|
2532
3486
|
}
|
|
3487
|
+
async function instanceInfo(n) {
|
|
3488
|
+
let main = null;
|
|
3489
|
+
try {
|
|
3490
|
+
main = await n.getMainComponentAsync();
|
|
3491
|
+
} catch (e) {}
|
|
3492
|
+
if (!main)
|
|
3493
|
+
return null;
|
|
3494
|
+
const set = main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.name : null;
|
|
3495
|
+
const props = {};
|
|
3496
|
+
const defs = n.componentProperties;
|
|
3497
|
+
const keys = Object.keys(defs);
|
|
3498
|
+
for (let i = 0;i < keys.length; i++) {
|
|
3499
|
+
const d = defs[keys[i]];
|
|
3500
|
+
if (d.type === "INSTANCE_SWAP")
|
|
3501
|
+
continue;
|
|
3502
|
+
props[keys[i].split("#")[0]] = d.value;
|
|
3503
|
+
}
|
|
3504
|
+
const info = { name: set || main.name, props };
|
|
3505
|
+
if (set)
|
|
3506
|
+
info.variant = main.name;
|
|
3507
|
+
const text = n.findOne(function(t) {
|
|
3508
|
+
return t.type === "TEXT" && t.visible;
|
|
3509
|
+
});
|
|
3510
|
+
if (text && text.characters.trim())
|
|
3511
|
+
info.text = text.characters.trim().slice(0, 200);
|
|
3512
|
+
return info;
|
|
3513
|
+
}
|
|
2533
3514
|
|
|
2534
3515
|
// plugin/lib/design-system.ts
|
|
2535
3516
|
var ALL = ["colors", "text", "effects", "variables", "components"];
|
|
@@ -2650,6 +3631,183 @@
|
|
|
2650
3631
|
return v;
|
|
2651
3632
|
}
|
|
2652
3633
|
|
|
3634
|
+
// plugin/lib/tokens-export.ts
|
|
3635
|
+
var TYPES2 = { COLOR: "color", FLOAT: "number", STRING: "string", BOOLEAN: "boolean" };
|
|
3636
|
+
async function exportTokens(p) {
|
|
3637
|
+
const only = Array.isArray(p.collections) && p.collections.length ? p.collections : null;
|
|
3638
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
3639
|
+
const vars = await figma.variables.getLocalVariablesAsync();
|
|
3640
|
+
const byId = {};
|
|
3641
|
+
for (let i = 0;i < vars.length; i++)
|
|
3642
|
+
byId[vars[i].id] = vars[i];
|
|
3643
|
+
const collName = {};
|
|
3644
|
+
for (let i = 0;i < collections.length; i++)
|
|
3645
|
+
collName[collections[i].id] = collections[i].name;
|
|
3646
|
+
const warnings = [];
|
|
3647
|
+
const aliasOf = async function(id) {
|
|
3648
|
+
let v = byId[id] || null;
|
|
3649
|
+
if (!v) {
|
|
3650
|
+
try {
|
|
3651
|
+
v = await figma.variables.getVariableByIdAsync(id);
|
|
3652
|
+
} catch (e) {}
|
|
3653
|
+
if (v)
|
|
3654
|
+
byId[id] = v;
|
|
3655
|
+
}
|
|
3656
|
+
if (!v)
|
|
3657
|
+
return { alias: { collection: "", name: id, missing: true } };
|
|
3658
|
+
let collection = collName[v.variableCollectionId];
|
|
3659
|
+
if (collection === undefined) {
|
|
3660
|
+
try {
|
|
3661
|
+
const c = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
|
|
3662
|
+
collection = c ? c.name : "";
|
|
3663
|
+
} catch (e) {
|
|
3664
|
+
collection = "";
|
|
3665
|
+
}
|
|
3666
|
+
collName[v.variableCollectionId] = collection;
|
|
3667
|
+
}
|
|
3668
|
+
const out = { alias: { collection, name: v.name } };
|
|
3669
|
+
if (v.remote)
|
|
3670
|
+
out.alias.remote = true;
|
|
3671
|
+
return out;
|
|
3672
|
+
};
|
|
3673
|
+
const value = async function(raw, type) {
|
|
3674
|
+
if (raw && typeof raw === "object" && raw.type === "VARIABLE_ALIAS")
|
|
3675
|
+
return aliasOf(raw.id);
|
|
3676
|
+
if (type === "COLOR" && raw && typeof raw === "object")
|
|
3677
|
+
return toHex(raw);
|
|
3678
|
+
if (type === "FLOAT")
|
|
3679
|
+
return round(Number(raw));
|
|
3680
|
+
return raw;
|
|
3681
|
+
};
|
|
3682
|
+
const outCollections = [];
|
|
3683
|
+
for (let c = 0;c < collections.length; c++) {
|
|
3684
|
+
const col = collections[c];
|
|
3685
|
+
if (only && only.indexOf(col.name) === -1)
|
|
3686
|
+
continue;
|
|
3687
|
+
const modes = col.modes.slice().sort(function(a, b) {
|
|
3688
|
+
return (a.modeId === col.defaultModeId ? 0 : 1) - (b.modeId === col.defaultModeId ? 0 : 1);
|
|
3689
|
+
});
|
|
3690
|
+
const list = [];
|
|
3691
|
+
for (let i = 0;i < vars.length; i++) {
|
|
3692
|
+
const v = vars[i];
|
|
3693
|
+
if (v.variableCollectionId !== col.id)
|
|
3694
|
+
continue;
|
|
3695
|
+
const values = {};
|
|
3696
|
+
for (let m = 0;m < modes.length; m++)
|
|
3697
|
+
values[modes[m].name] = await value(v.valuesByMode[modes[m].modeId], v.resolvedType);
|
|
3698
|
+
const item = { name: v.name, type: TYPES2[v.resolvedType] || "string", values };
|
|
3699
|
+
if (v.description)
|
|
3700
|
+
item.description = v.description;
|
|
3701
|
+
if (v.scopes && v.scopes.length && !(v.scopes.length === 1 && v.scopes[0] === "ALL_SCOPES"))
|
|
3702
|
+
item.scopes = v.scopes.slice();
|
|
3703
|
+
if (v.hiddenFromPublishing)
|
|
3704
|
+
item.hidden = true;
|
|
3705
|
+
list.push(item);
|
|
3706
|
+
}
|
|
3707
|
+
outCollections.push({
|
|
3708
|
+
name: col.name,
|
|
3709
|
+
modes: modes.map(function(m) {
|
|
3710
|
+
return m.name;
|
|
3711
|
+
}),
|
|
3712
|
+
variables: list
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
const styles = await localStyles();
|
|
3716
|
+
const paint = [];
|
|
3717
|
+
for (let i = 0;i < styles.paint.length; i++) {
|
|
3718
|
+
const s = styles.paint[i];
|
|
3719
|
+
const paints = [];
|
|
3720
|
+
for (let k = 0;k < s.paints.length; k++) {
|
|
3721
|
+
const pt = s.paints[k];
|
|
3722
|
+
if (pt.visible === false)
|
|
3723
|
+
continue;
|
|
3724
|
+
if (pt.type === "SOLID") {
|
|
3725
|
+
const bound = pt.boundVariables && pt.boundVariables.color;
|
|
3726
|
+
paints.push(bound ? await aliasOf(bound.id) : toHex(pt.color, pt.opacity));
|
|
3727
|
+
} else if (pt.type.indexOf("GRADIENT") === 0) {
|
|
3728
|
+
const t = pt.gradientTransform;
|
|
3729
|
+
const g = {
|
|
3730
|
+
gradient: pt.gradientStops.map(function(st) {
|
|
3731
|
+
return { color: toHex(st.color), at: round(st.position) };
|
|
3732
|
+
})
|
|
3733
|
+
};
|
|
3734
|
+
if (pt.type === "GRADIENT_LINEAR")
|
|
3735
|
+
g.angle = round(Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI);
|
|
3736
|
+
else
|
|
3737
|
+
g.type = pt.type.replace("GRADIENT_", "").toLowerCase();
|
|
3738
|
+
paints.push(g);
|
|
3739
|
+
} else {
|
|
3740
|
+
warnings.push('Paint style "' + s.name + '": ' + pt.type.toLowerCase() + " paints are not exported");
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
if (!paints.length)
|
|
3744
|
+
continue;
|
|
3745
|
+
const item = { name: s.name, value: paints.length === 1 ? paints[0] : paints };
|
|
3746
|
+
if (s.description)
|
|
3747
|
+
item.description = s.description;
|
|
3748
|
+
paint.push(item);
|
|
3749
|
+
}
|
|
3750
|
+
const text = [];
|
|
3751
|
+
for (let i = 0;i < styles.text.length; i++) {
|
|
3752
|
+
const s = styles.text[i];
|
|
3753
|
+
const item = { name: s.name, font: s.fontName.family, style: s.fontName.style, size: round(s.fontSize) };
|
|
3754
|
+
const lh = s.lineHeight;
|
|
3755
|
+
if (lh.unit === "PIXELS")
|
|
3756
|
+
item.lineHeight = round(lh.value);
|
|
3757
|
+
else if (lh.unit === "PERCENT")
|
|
3758
|
+
item.lineHeight = round(lh.value) + "%";
|
|
3759
|
+
if (s.letterSpacing.value)
|
|
3760
|
+
item.letterSpacing = s.letterSpacing.unit === "PERCENT" ? round(s.letterSpacing.value) + "%" : round(s.letterSpacing.value);
|
|
3761
|
+
if (s.textCase && s.textCase !== "ORIGINAL")
|
|
3762
|
+
item.case = s.textCase === "UPPER" ? "upper" : s.textCase === "LOWER" ? "lower" : "title";
|
|
3763
|
+
if (s.textDecoration && s.textDecoration !== "NONE")
|
|
3764
|
+
item.decoration = s.textDecoration === "UNDERLINE" ? "underline" : "strike";
|
|
3765
|
+
if (s.paragraphSpacing)
|
|
3766
|
+
item.paragraphSpacing = round(s.paragraphSpacing);
|
|
3767
|
+
const bound = s.boundVariables || {};
|
|
3768
|
+
const keys = Object.keys(bound);
|
|
3769
|
+
if (keys.length) {
|
|
3770
|
+
item.variables = {};
|
|
3771
|
+
for (let k = 0;k < keys.length; k++)
|
|
3772
|
+
if (bound[keys[k]] && bound[keys[k]].id)
|
|
3773
|
+
item.variables[keys[k]] = (await aliasOf(bound[keys[k]].id)).alias;
|
|
3774
|
+
}
|
|
3775
|
+
if (s.description)
|
|
3776
|
+
item.description = s.description;
|
|
3777
|
+
text.push(item);
|
|
3778
|
+
}
|
|
3779
|
+
const effect = [];
|
|
3780
|
+
for (let i = 0;i < styles.effect.length; i++) {
|
|
3781
|
+
const s = styles.effect[i];
|
|
3782
|
+
const list = [];
|
|
3783
|
+
for (let k = 0;k < s.effects.length; k++) {
|
|
3784
|
+
const e = s.effects[k];
|
|
3785
|
+
if (e.visible === false)
|
|
3786
|
+
continue;
|
|
3787
|
+
if (e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW") {
|
|
3788
|
+
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) };
|
|
3789
|
+
if (e.type === "INNER_SHADOW")
|
|
3790
|
+
sh.inner = true;
|
|
3791
|
+
if (e.boundVariables && e.boundVariables.color)
|
|
3792
|
+
sh.colorVariable = (await aliasOf(e.boundVariables.color.id)).alias;
|
|
3793
|
+
list.push(sh);
|
|
3794
|
+
} else {
|
|
3795
|
+
list.push({ type: e.type === "LAYER_BLUR" ? "layer" : "background", blur: round(e.radius) });
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
if (!list.length)
|
|
3799
|
+
continue;
|
|
3800
|
+
const item = { name: s.name, value: list };
|
|
3801
|
+
if (s.description)
|
|
3802
|
+
item.description = s.description;
|
|
3803
|
+
effect.push(item);
|
|
3804
|
+
}
|
|
3805
|
+
const out = { fileName: figma.root.name, collections: outCollections, styles: { colors: paint, text, effects: effect } };
|
|
3806
|
+
if (warnings.length)
|
|
3807
|
+
out.warnings = warnings.slice(0, 40);
|
|
3808
|
+
return out;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
2653
3811
|
// plugin/lib/find.ts
|
|
2654
3812
|
var MAX_LIMIT = 500;
|
|
2655
3813
|
function matcher(q) {
|
|
@@ -2762,6 +3920,243 @@
|
|
|
2762
3920
|
return out;
|
|
2763
3921
|
}
|
|
2764
3922
|
|
|
3923
|
+
// plugin/lib/roblox.ts
|
|
3924
|
+
var MAX_NODES5 = 2000;
|
|
3925
|
+
var VECTORS = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
|
|
3926
|
+
async function robloxTree(p) {
|
|
3927
|
+
const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
|
|
3928
|
+
if (!node)
|
|
3929
|
+
throw codeError("No nodeId given and nothing is selected", "BAD_ARGS");
|
|
3930
|
+
if (node.type === "PAGE" || node.type === "DOCUMENT")
|
|
3931
|
+
throw codeError("Export a frame, not a page", "BAD_ARGS");
|
|
3932
|
+
const ctx = { count: 0, truncated: false, images: {}, imageBytes: 0 };
|
|
3933
|
+
const tree = await walk2(node, null, ctx);
|
|
3934
|
+
return { tree, images: ctx.images, nodes: ctx.count, truncated: ctx.truncated, fileName: figma.root.name };
|
|
3935
|
+
}
|
|
3936
|
+
function paints(list) {
|
|
3937
|
+
if (!list || list === figma.mixed)
|
|
3938
|
+
return [];
|
|
3939
|
+
const out = [];
|
|
3940
|
+
for (let i = 0;i < list.length; i++) {
|
|
3941
|
+
const pt = list[i];
|
|
3942
|
+
if (pt.visible === false)
|
|
3943
|
+
continue;
|
|
3944
|
+
const o = { type: pt.type, opacity: pt.opacity === undefined ? 1 : round(pt.opacity) };
|
|
3945
|
+
if (pt.type === "SOLID")
|
|
3946
|
+
o.color = toHex(pt.color);
|
|
3947
|
+
else if (pt.type.indexOf("GRADIENT") === 0) {
|
|
3948
|
+
const t = pt.gradientTransform;
|
|
3949
|
+
o.angle = round(Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI);
|
|
3950
|
+
o.stops = pt.gradientStops.map(function(s) {
|
|
3951
|
+
return { color: toHex({ r: s.color.r, g: s.color.g, b: s.color.b }), alpha: round(s.color.a), at: round(s.position) };
|
|
3952
|
+
});
|
|
3953
|
+
} else if (pt.type === "IMAGE") {
|
|
3954
|
+
o.imageHash = pt.imageHash;
|
|
3955
|
+
o.scaleMode = pt.scaleMode;
|
|
3956
|
+
}
|
|
3957
|
+
out.push(o);
|
|
3958
|
+
}
|
|
3959
|
+
return out;
|
|
3960
|
+
}
|
|
3961
|
+
function vectorOnly2(n) {
|
|
3962
|
+
if (VECTORS.indexOf(n.type) !== -1)
|
|
3963
|
+
return true;
|
|
3964
|
+
if (n.type === "TEXT" || !("children" in n) || !n.children.length)
|
|
3965
|
+
return false;
|
|
3966
|
+
for (let i = 0;i < n.children.length; i++)
|
|
3967
|
+
if (n.children[i].visible && !vectorOnly2(n.children[i]))
|
|
3968
|
+
return false;
|
|
3969
|
+
return true;
|
|
3970
|
+
}
|
|
3971
|
+
async function walk2(n, parent, ctx) {
|
|
3972
|
+
ctx.count++;
|
|
3973
|
+
const box = n.absoluteBoundingBox || { x: 0, y: 0, width: n.width || 0, height: n.height || 0 };
|
|
3974
|
+
const pbox = parent && parent.absoluteBoundingBox;
|
|
3975
|
+
const out = {
|
|
3976
|
+
id: n.id,
|
|
3977
|
+
name: n.name,
|
|
3978
|
+
type: n.type,
|
|
3979
|
+
x: pbox ? round(box.x - pbox.x) : 0,
|
|
3980
|
+
y: pbox ? round(box.y - pbox.y) : 0,
|
|
3981
|
+
w: round(box.width),
|
|
3982
|
+
h: round(box.height)
|
|
3983
|
+
};
|
|
3984
|
+
if (n.visible === false)
|
|
3985
|
+
out.hidden = true;
|
|
3986
|
+
if (typeof n.opacity === "number" && n.opacity < 1)
|
|
3987
|
+
out.opacity = round(n.opacity);
|
|
3988
|
+
if (n.rotation && Math.abs(n.rotation) > 0.01)
|
|
3989
|
+
out.rotation = round(-n.rotation);
|
|
3990
|
+
if (n.clipsContent)
|
|
3991
|
+
out.clip = true;
|
|
3992
|
+
if (n.constraints)
|
|
3993
|
+
out.constraints = { h: n.constraints.horizontal, v: n.constraints.vertical };
|
|
3994
|
+
if (n.layoutPositioning === "ABSOLUTE")
|
|
3995
|
+
out.absolute = true;
|
|
3996
|
+
if (n.layoutGrow === 1)
|
|
3997
|
+
out.grow = true;
|
|
3998
|
+
if (n.layoutAlign === "STRETCH")
|
|
3999
|
+
out.stretch = true;
|
|
4000
|
+
if ("layoutSizingHorizontal" in n)
|
|
4001
|
+
out.sizing = { h: n.layoutSizingHorizontal, v: n.layoutSizingVertical };
|
|
4002
|
+
const rb = n.absoluteRenderBounds;
|
|
4003
|
+
if (rb && (Math.abs(rb.x - box.x) > 0.5 || Math.abs(rb.y - box.y) > 0.5 || Math.abs(rb.width - box.width) > 0.5 || Math.abs(rb.height - box.height) > 0.5)) {
|
|
4004
|
+
out.render = { x: round(rb.x - box.x), y: round(rb.y - box.y), w: round(rb.width), h: round(rb.height) };
|
|
4005
|
+
}
|
|
4006
|
+
if ("fills" in n)
|
|
4007
|
+
out.fills = paints(n.fills);
|
|
4008
|
+
if ("strokes" in n && n.strokes.length) {
|
|
4009
|
+
out.strokes = paints(n.strokes);
|
|
4010
|
+
out.strokeWeight = n.strokeWeight === figma.mixed ? round(n.strokeTopWeight || 1) : round(n.strokeWeight);
|
|
4011
|
+
out.strokeAlign = n.strokeAlign;
|
|
4012
|
+
if (n.dashPattern && n.dashPattern.length)
|
|
4013
|
+
out.dashed = true;
|
|
4014
|
+
}
|
|
4015
|
+
if ("topLeftRadius" in n) {
|
|
4016
|
+
const r = [n.topLeftRadius, n.topRightRadius, n.bottomRightRadius, n.bottomLeftRadius].map(round);
|
|
4017
|
+
if (r[0] || r[1] || r[2] || r[3])
|
|
4018
|
+
out.radius = r;
|
|
4019
|
+
} else if (typeof n.cornerRadius === "number" && n.cornerRadius)
|
|
4020
|
+
out.radius = [round(n.cornerRadius), round(n.cornerRadius), round(n.cornerRadius), round(n.cornerRadius)];
|
|
4021
|
+
if ("effects" in n && n.effects.length) {
|
|
4022
|
+
out.effects = n.effects.filter(function(e) {
|
|
4023
|
+
return e.visible !== false;
|
|
4024
|
+
}).map(function(e) {
|
|
4025
|
+
return e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW" ? { type: e.type, x: round(e.offset.x), y: round(e.offset.y), blur: round(e.radius), spread: round(e.spread || 0), color: toHex({ r: e.color.r, g: e.color.g, b: e.color.b }), alpha: round(e.color.a) } : { type: e.type, blur: round(e.radius) };
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
if (n.reactions && n.reactions.length) {
|
|
4029
|
+
out.clickable = n.reactions.some(function(r) {
|
|
4030
|
+
return r.trigger && (r.trigger.type === "ON_CLICK" || r.trigger.type === "ON_PRESS");
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
if (n.type !== "TEXT" && vectorOnly2(n))
|
|
4034
|
+
out.vector = true;
|
|
4035
|
+
const fills = out.fills || [];
|
|
4036
|
+
for (let i = 0;i < fills.length; i++) {
|
|
4037
|
+
const h = fills[i].imageHash;
|
|
4038
|
+
if (!h || ctx.images[h] !== undefined)
|
|
4039
|
+
continue;
|
|
4040
|
+
const image = figma.getImageByHash(h);
|
|
4041
|
+
if (!image)
|
|
4042
|
+
continue;
|
|
4043
|
+
const bytes = await image.getBytesAsync();
|
|
4044
|
+
if (ctx.imageBytes + bytes.length > 40 << 20)
|
|
4045
|
+
continue;
|
|
4046
|
+
ctx.imageBytes += bytes.length;
|
|
4047
|
+
ctx.images[h] = figma.base64Encode(bytes);
|
|
4048
|
+
}
|
|
4049
|
+
if (n.type === "TEXT") {
|
|
4050
|
+
out.text = textOf2(n);
|
|
4051
|
+
return out;
|
|
4052
|
+
}
|
|
4053
|
+
if (n.layoutMode && n.layoutMode !== "NONE") {
|
|
4054
|
+
const l = {
|
|
4055
|
+
mode: n.layoutMode,
|
|
4056
|
+
padding: [n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft].map(round),
|
|
4057
|
+
gap: round(n.itemSpacing || 0),
|
|
4058
|
+
primary: n.primaryAxisAlignItems,
|
|
4059
|
+
counter: n.counterAxisAlignItems
|
|
4060
|
+
};
|
|
4061
|
+
if (n.layoutWrap === "WRAP") {
|
|
4062
|
+
l.wrap = true;
|
|
4063
|
+
l.counterGap = round(n.counterAxisSpacing || 0);
|
|
4064
|
+
}
|
|
4065
|
+
if (n.layoutMode === "GRID") {
|
|
4066
|
+
l.columns = n.gridColumnCount;
|
|
4067
|
+
l.rows = n.gridRowCount;
|
|
4068
|
+
l.columnGap = round(n.gridColumnGap || 0);
|
|
4069
|
+
l.rowGap = round(n.gridRowGap || 0);
|
|
4070
|
+
}
|
|
4071
|
+
out.layout = l;
|
|
4072
|
+
}
|
|
4073
|
+
if ("children" in n && n.children.length && !out.vector) {
|
|
4074
|
+
out.children = [];
|
|
4075
|
+
for (let i = 0;i < n.children.length; i++) {
|
|
4076
|
+
if (ctx.count >= MAX_NODES5) {
|
|
4077
|
+
ctx.truncated = true;
|
|
4078
|
+
break;
|
|
4079
|
+
}
|
|
4080
|
+
out.children.push(await walk2(n.children[i], n, ctx));
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
return out;
|
|
4084
|
+
}
|
|
4085
|
+
function textOf2(t) {
|
|
4086
|
+
const segs = t.getStyledTextSegments(["fontName", "fontSize", "fontWeight", "fills", "textDecoration", "textCase", "letterSpacing", "lineHeight"]);
|
|
4087
|
+
return {
|
|
4088
|
+
characters: t.characters,
|
|
4089
|
+
align: t.textAlignHorizontal,
|
|
4090
|
+
valign: t.textAlignVertical,
|
|
4091
|
+
autoResize: t.textAutoResize,
|
|
4092
|
+
maxLines: t.textTruncation === "ENDING" ? t.maxLines || 1 : 0,
|
|
4093
|
+
segments: segs.map(function(s) {
|
|
4094
|
+
const fill = s.fills.filter(function(f) {
|
|
4095
|
+
return f.visible !== false;
|
|
4096
|
+
})[0];
|
|
4097
|
+
const seg = { text: s.characters, family: s.fontName.family, style: s.fontName.style, weight: s.fontWeight, size: round(s.fontSize) };
|
|
4098
|
+
if (fill && fill.type === "SOLID") {
|
|
4099
|
+
seg.color = toHex(fill.color);
|
|
4100
|
+
if (fill.opacity !== undefined && fill.opacity < 1)
|
|
4101
|
+
seg.alpha = round(fill.opacity);
|
|
4102
|
+
}
|
|
4103
|
+
if (s.textDecoration !== "NONE")
|
|
4104
|
+
seg.decoration = s.textDecoration;
|
|
4105
|
+
if (s.textCase && s.textCase !== "ORIGINAL")
|
|
4106
|
+
seg.textCase = s.textCase;
|
|
4107
|
+
if (s.letterSpacing && s.letterSpacing.value)
|
|
4108
|
+
seg.letterSpacing = round(s.letterSpacing.unit === "PERCENT" ? s.letterSpacing.value / 100 * s.fontSize : s.letterSpacing.value);
|
|
4109
|
+
if (s.lineHeight && s.lineHeight.unit !== "AUTO")
|
|
4110
|
+
seg.lineHeight = round(s.lineHeight.unit === "PERCENT" ? s.lineHeight.value / 100 * s.fontSize : s.lineHeight.value);
|
|
4111
|
+
return seg;
|
|
4112
|
+
})
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
async function robloxImages(p) {
|
|
4116
|
+
const items = Array.isArray(p.items) ? p.items : [];
|
|
4117
|
+
const out = [];
|
|
4118
|
+
for (let i = 0;i < items.length; i++) {
|
|
4119
|
+
const it = items[i];
|
|
4120
|
+
const scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
|
|
4121
|
+
let temp = null;
|
|
4122
|
+
try {
|
|
4123
|
+
let node = await getNode(it.id);
|
|
4124
|
+
if (it.mode === "panel" || it.mode === "shadow") {
|
|
4125
|
+
temp = node.clone();
|
|
4126
|
+
if ("children" in temp)
|
|
4127
|
+
for (let k = temp.children.length - 1;k >= 0; k--)
|
|
4128
|
+
temp.children[k].remove();
|
|
4129
|
+
if (it.mode === "panel")
|
|
4130
|
+
temp.effects = [];
|
|
4131
|
+
else {
|
|
4132
|
+
temp.effects = node.effects.filter(function(e) {
|
|
4133
|
+
return e.type === "DROP_SHADOW" && e.visible !== false;
|
|
4134
|
+
});
|
|
4135
|
+
temp.strokes = [];
|
|
4136
|
+
}
|
|
4137
|
+
node = temp;
|
|
4138
|
+
}
|
|
4139
|
+
const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } });
|
|
4140
|
+
const rb = node.absoluteRenderBounds || node.absoluteBoundingBox;
|
|
4141
|
+
const bb = node.absoluteBoundingBox;
|
|
4142
|
+
out.push({
|
|
4143
|
+
id: it.id,
|
|
4144
|
+
mode: it.mode || "full",
|
|
4145
|
+
scale,
|
|
4146
|
+
b64: figma.base64Encode(bytes),
|
|
4147
|
+
offset: rb && bb ? { x: round(rb.x - bb.x), y: round(rb.y - bb.y) } : { x: 0, y: 0 },
|
|
4148
|
+
size: rb ? { w: round(rb.width), h: round(rb.height) } : { w: round(node.width), h: round(node.height) }
|
|
4149
|
+
});
|
|
4150
|
+
} catch (e) {
|
|
4151
|
+
out.push({ id: it.id, mode: it.mode || "full", error: String(e.message || e) });
|
|
4152
|
+
} finally {
|
|
4153
|
+
if (temp)
|
|
4154
|
+
temp.remove();
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
return { images: out };
|
|
4158
|
+
}
|
|
4159
|
+
|
|
2765
4160
|
// plugin/lib/selection.ts
|
|
2766
4161
|
var MAX_WAIT_MS = 120000;
|
|
2767
4162
|
var waiters = {};
|
|
@@ -2831,7 +4226,7 @@
|
|
|
2831
4226
|
finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
|
|
2832
4227
|
}
|
|
2833
4228
|
// package.json
|
|
2834
|
-
var version = "1.
|
|
4229
|
+
var version = "1.12.0";
|
|
2835
4230
|
|
|
2836
4231
|
// plugin/code.ts
|
|
2837
4232
|
var DEFAULT_SIZE = { width: 340, height: 540 };
|
|
@@ -2853,7 +4248,11 @@
|
|
|
2853
4248
|
figma.ui.postMessage(msg);
|
|
2854
4249
|
}
|
|
2855
4250
|
function sessionInfo() {
|
|
2856
|
-
|
|
4251
|
+
let fileKey;
|
|
4252
|
+
try {
|
|
4253
|
+
fileKey = figma.fileKey;
|
|
4254
|
+
} catch (e) {}
|
|
4255
|
+
return { id: sessionId, fileName: figma.root.name, page: figma.currentPage.name, pageId: figma.currentPage.id, editorType: figma.editorType, fileKey };
|
|
2857
4256
|
}
|
|
2858
4257
|
function clampSize(w, h) {
|
|
2859
4258
|
return {
|
|
@@ -2942,6 +4341,7 @@
|
|
|
2942
4341
|
find: find2,
|
|
2943
4342
|
get_design_system: getDesignSystem,
|
|
2944
4343
|
design_tokens: designTokens,
|
|
4344
|
+
export_tokens: exportTokens,
|
|
2945
4345
|
audit,
|
|
2946
4346
|
get_css: getCss,
|
|
2947
4347
|
checkpoint,
|
|
@@ -2954,14 +4354,25 @@
|
|
|
2954
4354
|
prototype,
|
|
2955
4355
|
annotate,
|
|
2956
4356
|
export_tree: exportTree,
|
|
4357
|
+
node_info: nodeInfo,
|
|
4358
|
+
roblox_tree: robloxTree,
|
|
4359
|
+
roblox_images: robloxImages,
|
|
2957
4360
|
ping: function() {
|
|
2958
4361
|
return Promise.resolve({ pong: true, session: sessionInfo() });
|
|
2959
4362
|
}
|
|
2960
4363
|
};
|
|
4364
|
+
var EDITORS2 = {
|
|
4365
|
+
prototype: ["figma"],
|
|
4366
|
+
annotate: ["figma", "dev"],
|
|
4367
|
+
design_tokens: ["figma", "slides"],
|
|
4368
|
+
export_tokens: ["figma", "slides", "dev"]
|
|
4369
|
+
};
|
|
4370
|
+
var EDITOR_NAMES2 = { figma: "Figma Design", figjam: "FigJam", slides: "Figma Slides", dev: "Dev Mode" };
|
|
2961
4371
|
var READ_ONLY = {
|
|
2962
4372
|
describe: true,
|
|
2963
4373
|
find: true,
|
|
2964
4374
|
get_design_system: true,
|
|
4375
|
+
export_tokens: true,
|
|
2965
4376
|
audit: true,
|
|
2966
4377
|
get_css: true,
|
|
2967
4378
|
screenshot: true,
|
|
@@ -2969,11 +4380,13 @@
|
|
|
2969
4380
|
list_fonts: true,
|
|
2970
4381
|
wait_for_selection: true,
|
|
2971
4382
|
export_tree: true,
|
|
4383
|
+
node_info: true,
|
|
4384
|
+
roblox_tree: true,
|
|
2972
4385
|
ping: true
|
|
2973
4386
|
};
|
|
2974
4387
|
async function handleRequest(msg) {
|
|
2975
4388
|
const started = Date.now();
|
|
2976
|
-
const mutates = !READ_ONLY[msg.method];
|
|
4389
|
+
const mutates = !READ_ONLY[msg.method] || !!(msg.params && (msg.params.fix === true || msg.params.fixes && msg.params.fixes.length));
|
|
2977
4390
|
let reply;
|
|
2978
4391
|
if (mutates)
|
|
2979
4392
|
commitUndo();
|
|
@@ -2981,6 +4394,14 @@
|
|
|
2981
4394
|
const handler = HANDLERS[msg.method];
|
|
2982
4395
|
if (!handler)
|
|
2983
4396
|
throw codeError("Unknown method: " + msg.method + " (reopen the plugin after updating it)", "UNKNOWN_METHOD");
|
|
4397
|
+
const editors = EDITORS2[msg.method];
|
|
4398
|
+
if (editors && editors.indexOf(figma.editorType) === -1) {
|
|
4399
|
+
throw codeError(msg.method + " is not available in " + (EDITOR_NAMES2[figma.editorType] || figma.editorType) + " (works in " + editors.map(function(e) {
|
|
4400
|
+
return EDITOR_NAMES2[e];
|
|
4401
|
+
}).join(", ") + ").", "WRONG_EDITOR");
|
|
4402
|
+
}
|
|
4403
|
+
if (mutates && figma.editorType === "dev")
|
|
4404
|
+
throw codeError("Dev Mode is read-only: switch the file to Design mode to let the agent edit it.", "WRONG_EDITOR");
|
|
2984
4405
|
const result = await handler(msg.params || {}, Math.min(Number(msg.timeoutMs) || 30000, 120000), String(msg.id));
|
|
2985
4406
|
reply = { t: "res", id: msg.id, ok: true, result };
|
|
2986
4407
|
} catch (e) {
|
|
@@ -3282,6 +4703,18 @@
|
|
|
3282
4703
|
}
|
|
3283
4704
|
};
|
|
3284
4705
|
}
|
|
4706
|
+
async function nodeInfo(p) {
|
|
4707
|
+
const ids = Array.isArray(p.ids) ? p.ids.slice(0, 500) : [];
|
|
4708
|
+
const out = {};
|
|
4709
|
+
for (let i = 0;i < ids.length; i++) {
|
|
4710
|
+
const n = await figma.getNodeByIdAsync(String(ids[i]));
|
|
4711
|
+
if (!n)
|
|
4712
|
+
continue;
|
|
4713
|
+
const pg = pageOf(n);
|
|
4714
|
+
out[ids[i]] = { name: n.name, type: n.type, page: pg ? pg.name : undefined };
|
|
4715
|
+
}
|
|
4716
|
+
return out;
|
|
4717
|
+
}
|
|
3285
4718
|
var fontCache = null;
|
|
3286
4719
|
async function listFonts(p) {
|
|
3287
4720
|
if (!fontCache)
|