@clovis500c/figma-bridge 1.5.0 → 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/README.md +14 -7
- package/dist/server.js +4267 -525
- package/package.json +5 -2
- package/plugin/code.js +907 -28
- package/plugin/ui.html +4 -2
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)
|
|
@@ -1109,7 +1757,7 @@
|
|
|
1109
1757
|
}
|
|
1110
1758
|
|
|
1111
1759
|
// plugin/lib/build.ts
|
|
1112
|
-
var
|
|
1760
|
+
var MAX_NODES3 = 3000;
|
|
1113
1761
|
var SHADOW_DEFAULT = { x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" };
|
|
1114
1762
|
async function build(p, _timeoutMs, requestId) {
|
|
1115
1763
|
const spec = p.spec;
|
|
@@ -1264,8 +1912,8 @@
|
|
|
1264
1912
|
ctx.warnings.push(path + ": not an object, skipped");
|
|
1265
1913
|
return null;
|
|
1266
1914
|
}
|
|
1267
|
-
if (ctx.count >=
|
|
1268
|
-
throw codeError("Spec is too large (max " +
|
|
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");
|
|
1269
1917
|
checkCancelled(ctx.requestId);
|
|
1270
1918
|
const type = nodeType(s);
|
|
1271
1919
|
let node;
|
|
@@ -1586,7 +2234,7 @@
|
|
|
1586
2234
|
if (typeof color === "string" && color.indexOf("style:") === 0) {
|
|
1587
2235
|
await t.setRangeFillStyleIdAsync(at, end, (await findStyle("paint", stripPrefix(color))).id);
|
|
1588
2236
|
} else if (color !== undefined) {
|
|
1589
|
-
t.setRangeFills(at, end, [await toPaint(color)]);
|
|
2237
|
+
t.setRangeFills(at, end, [await toPaint(color, ctx.images)]);
|
|
1590
2238
|
}
|
|
1591
2239
|
const deco = { underline: "UNDERLINE", strike: "STRIKETHROUGH", strikethrough: "STRIKETHROUGH", none: "NONE" };
|
|
1592
2240
|
if (sp.link) {
|
|
@@ -1596,7 +2244,7 @@
|
|
|
1596
2244
|
}
|
|
1597
2245
|
if (sp.decoration && deco[sp.decoration])
|
|
1598
2246
|
t.setRangeTextDecoration(at, end, deco[sp.decoration]);
|
|
1599
|
-
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE" };
|
|
2247
|
+
const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE", none: "ORIGINAL" };
|
|
1600
2248
|
if (sp.case && cases[sp.case])
|
|
1601
2249
|
t.setRangeTextCase(at, end, cases[sp.case]);
|
|
1602
2250
|
} catch (e) {
|
|
@@ -1782,10 +2430,20 @@
|
|
|
1782
2430
|
async function applyVisuals(node, s, ctx, type) {
|
|
1783
2431
|
const fill = s.fill !== undefined ? s.fill : type === "text" ? s.color : undefined;
|
|
1784
2432
|
if (fill !== undefined && "fills" in node && type !== "image")
|
|
1785
|
-
await setPaints(node, "fills", fill);
|
|
2433
|
+
await setPaints(node, "fills", fill, ctx.images);
|
|
1786
2434
|
if (s.stroke !== undefined && "strokes" in node) {
|
|
1787
|
-
await setPaints(node, "strokes", s.stroke);
|
|
1788
|
-
|
|
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);
|
|
1789
2447
|
if ("strokeAlign" in node && type !== "line" && type !== "text") {
|
|
1790
2448
|
node.strokeAlign = String(s.strokeAlign || "inside").toUpperCase();
|
|
1791
2449
|
}
|
|
@@ -1843,7 +2501,7 @@
|
|
|
1843
2501
|
const c = parseHex(hex);
|
|
1844
2502
|
return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
|
|
1845
2503
|
}
|
|
1846
|
-
async function setPaints(node, field, value) {
|
|
2504
|
+
async function setPaints(node, field, value, images) {
|
|
1847
2505
|
if (typeof value === "string" && value.indexOf("style:") === 0) {
|
|
1848
2506
|
const style = await findStyle("paint", stripPrefix(value));
|
|
1849
2507
|
if (field === "fills")
|
|
@@ -1855,10 +2513,11 @@
|
|
|
1855
2513
|
const list = value === null || value === "none" ? [] : Array.isArray(value) ? value : [value];
|
|
1856
2514
|
const paints = [];
|
|
1857
2515
|
for (let i = 0;i < list.length; i++)
|
|
1858
|
-
paints.push(await toPaint(list[i]));
|
|
2516
|
+
paints.push(await toPaint(list[i], images));
|
|
1859
2517
|
node[field] = paints;
|
|
1860
2518
|
}
|
|
1861
|
-
|
|
2519
|
+
var FITS = { fill: "FILL", cover: "FILL", fit: "FIT", contain: "FIT", crop: "CROP", tile: "TILE" };
|
|
2520
|
+
async function toPaint(v, images) {
|
|
1862
2521
|
if (typeof v === "string") {
|
|
1863
2522
|
if (v.indexOf("var:") === 0) {
|
|
1864
2523
|
const variable = await findVariable(stripPrefix(v));
|
|
@@ -1886,6 +2545,15 @@
|
|
|
1886
2545
|
];
|
|
1887
2546
|
return { type: radial ? "GRADIENT_RADIAL" : "GRADIENT_LINEAR", gradientTransform: transform, gradientStops: stops };
|
|
1888
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
|
+
}
|
|
1889
2557
|
if (v && typeof v.type === "string")
|
|
1890
2558
|
return v;
|
|
1891
2559
|
throw codeError("Invalid paint: " + JSON.stringify(v), "BAD_ARGS");
|
|
@@ -2313,7 +2981,7 @@
|
|
|
2313
2981
|
}
|
|
2314
2982
|
|
|
2315
2983
|
// plugin/lib/export.ts
|
|
2316
|
-
var
|
|
2984
|
+
var MAX_NODES4 = 1500;
|
|
2317
2985
|
var MAX_ASSET_BYTES = 40 << 20;
|
|
2318
2986
|
var VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "ELLIPSE", "LINE"];
|
|
2319
2987
|
var VISUAL_CSS = [
|
|
@@ -2423,6 +3091,11 @@
|
|
|
2423
3091
|
}
|
|
2424
3092
|
if (n.rotation && Math.abs(n.rotation) > 0.01 && n.type !== "TEXT")
|
|
2425
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
|
+
}
|
|
2426
3099
|
let css = {};
|
|
2427
3100
|
try {
|
|
2428
3101
|
css = await n.getCSSAsync();
|
|
@@ -2487,7 +3160,7 @@
|
|
|
2487
3160
|
const c = n.children[i];
|
|
2488
3161
|
if (!c.visible)
|
|
2489
3162
|
continue;
|
|
2490
|
-
if (ctx.count >=
|
|
3163
|
+
if (ctx.count >= MAX_NODES4) {
|
|
2491
3164
|
ctx.truncated = true;
|
|
2492
3165
|
break;
|
|
2493
3166
|
}
|
|
@@ -2530,6 +3203,33 @@
|
|
|
2530
3203
|
})
|
|
2531
3204
|
};
|
|
2532
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
|
+
}
|
|
2533
3233
|
|
|
2534
3234
|
// plugin/lib/design-system.ts
|
|
2535
3235
|
var ALL = ["colors", "text", "effects", "variables", "components"];
|
|
@@ -2650,6 +3350,183 @@
|
|
|
2650
3350
|
return v;
|
|
2651
3351
|
}
|
|
2652
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
|
+
|
|
2653
3530
|
// plugin/lib/find.ts
|
|
2654
3531
|
var MAX_LIMIT = 500;
|
|
2655
3532
|
function matcher(q) {
|
|
@@ -2831,7 +3708,7 @@
|
|
|
2831
3708
|
finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
|
|
2832
3709
|
}
|
|
2833
3710
|
// package.json
|
|
2834
|
-
var version = "1.
|
|
3711
|
+
var version = "1.8.0";
|
|
2835
3712
|
|
|
2836
3713
|
// plugin/code.ts
|
|
2837
3714
|
var DEFAULT_SIZE = { width: 340, height: 540 };
|
|
@@ -2942,6 +3819,7 @@
|
|
|
2942
3819
|
find: find2,
|
|
2943
3820
|
get_design_system: getDesignSystem,
|
|
2944
3821
|
design_tokens: designTokens,
|
|
3822
|
+
export_tokens: exportTokens,
|
|
2945
3823
|
audit,
|
|
2946
3824
|
get_css: getCss,
|
|
2947
3825
|
checkpoint,
|
|
@@ -2962,6 +3840,7 @@
|
|
|
2962
3840
|
describe: true,
|
|
2963
3841
|
find: true,
|
|
2964
3842
|
get_design_system: true,
|
|
3843
|
+
export_tokens: true,
|
|
2965
3844
|
audit: true,
|
|
2966
3845
|
get_css: true,
|
|
2967
3846
|
screenshot: true,
|
|
@@ -2973,7 +3852,7 @@
|
|
|
2973
3852
|
};
|
|
2974
3853
|
async function handleRequest(msg) {
|
|
2975
3854
|
const started = Date.now();
|
|
2976
|
-
const mutates = !READ_ONLY[msg.method];
|
|
3855
|
+
const mutates = !READ_ONLY[msg.method] || !!(msg.params && (msg.params.fix === true || msg.params.fixes && msg.params.fixes.length));
|
|
2977
3856
|
let reply;
|
|
2978
3857
|
if (mutates)
|
|
2979
3858
|
commitUndo();
|