openfig-cli 0.3.23 → 0.3.25
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/lib/core/fig-deck.mjs +86 -25
- package/manifest.json +1 -1
- package/package.json +1 -1
package/lib/core/fig-deck.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { join, resolve } from 'path';
|
|
|
20
20
|
import { tmpdir } from 'os';
|
|
21
21
|
import { nid } from './node-helpers.mjs';
|
|
22
22
|
import { deepClone } from './deep-clone.mjs';
|
|
23
|
+
import { hashToHex } from './image-helpers.mjs';
|
|
23
24
|
|
|
24
25
|
export class FigDeck {
|
|
25
26
|
constructor() {
|
|
@@ -366,6 +367,39 @@ export class FigDeck {
|
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
// Remap blob indices (commandsBlob, vectorNetworkBlob, fillGeometry etc.)
|
|
371
|
+
// Blobs in source deck reference positions in sourceDeck.message.blobs.
|
|
372
|
+
// Copy each referenced blob to target and update the index.
|
|
373
|
+
const remapBlobIndex = (idx) => {
|
|
374
|
+
if (idx == null || idx < 0) return idx;
|
|
375
|
+
if (!sourceDeck.message.blobs || idx >= sourceDeck.message.blobs.length) return idx;
|
|
376
|
+
if (!this._blobRemap) this._blobRemap = new Map();
|
|
377
|
+
if (this._blobRemap.has(idx)) return this._blobRemap.get(idx);
|
|
378
|
+
const blob = sourceDeck.message.blobs[idx];
|
|
379
|
+
if (!this.message.blobs) this.message.blobs = [];
|
|
380
|
+
const newIdx = this.message.blobs.length;
|
|
381
|
+
this.message.blobs.push(deepClone(blob));
|
|
382
|
+
this._blobRemap.set(idx, newIdx);
|
|
383
|
+
return newIdx;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// fillGeometry[].commandsBlob
|
|
387
|
+
if (clone.fillGeometry) {
|
|
388
|
+
for (const fg of clone.fillGeometry) {
|
|
389
|
+
if (fg.commandsBlob != null) fg.commandsBlob = remapBlobIndex(fg.commandsBlob);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// strokeGeometry[].commandsBlob
|
|
393
|
+
if (clone.strokeGeometry) {
|
|
394
|
+
for (const sg of clone.strokeGeometry) {
|
|
395
|
+
if (sg.commandsBlob != null) sg.commandsBlob = remapBlobIndex(sg.commandsBlob);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// vectorNetworkBlob (VECTOR nodes)
|
|
399
|
+
if (clone.vectorNetworkBlob != null) {
|
|
400
|
+
clone.vectorNetworkBlob = remapBlobIndex(clone.vectorNetworkBlob);
|
|
401
|
+
}
|
|
402
|
+
|
|
369
403
|
clone.phase = 'CREATED';
|
|
370
404
|
delete clone.slideThumbnailHash;
|
|
371
405
|
delete clone.editInfo;
|
|
@@ -374,6 +408,9 @@ export class FigDeck {
|
|
|
374
408
|
return clone;
|
|
375
409
|
});
|
|
376
410
|
|
|
411
|
+
// Clear blob remap cache after processing this symbol's subtree
|
|
412
|
+
delete this._blobRemap;
|
|
413
|
+
|
|
377
414
|
// Copy referenced images from source deck
|
|
378
415
|
if (sourceDeck.imagesDir) {
|
|
379
416
|
if (!this.imagesDir) {
|
|
@@ -384,15 +421,28 @@ export class FigDeck {
|
|
|
384
421
|
mkdirSync(this.imagesDir, { recursive: true });
|
|
385
422
|
}
|
|
386
423
|
|
|
424
|
+
const copyImagePaint = (paint) => {
|
|
425
|
+
if (!paint) return;
|
|
426
|
+
if (paint.type === 'IMAGE' || paint.image) {
|
|
427
|
+
const name = paint.image?.name || (paint.image?.hash?.length ? hashToHex(paint.image.hash) : null);
|
|
428
|
+
if (name) this._copyImageAsset(sourceDeck.imagesDir, name);
|
|
429
|
+
}
|
|
430
|
+
if (paint.imageThumbnail) {
|
|
431
|
+
const tName = paint.imageThumbnail.name || (paint.imageThumbnail.hash?.length ? hashToHex(paint.imageThumbnail.hash) : null);
|
|
432
|
+
if (tName) this._copyImageAsset(sourceDeck.imagesDir, tName);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
387
436
|
for (const clone of clonedNodes) {
|
|
388
|
-
// Copy IMAGE fills
|
|
437
|
+
// Copy IMAGE fills from node fillPaints
|
|
389
438
|
if (clone.fillPaints) {
|
|
390
|
-
for (const paint of clone.fillPaints)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
439
|
+
for (const paint of clone.fillPaints) copyImagePaint(paint);
|
|
440
|
+
}
|
|
441
|
+
// Copy images from symbolOverrides (INSTANCE nodes)
|
|
442
|
+
if (clone.symbolData?.symbolOverrides) {
|
|
443
|
+
for (const ov of clone.symbolData.symbolOverrides) {
|
|
444
|
+
if (ov.fillPaints) {
|
|
445
|
+
for (const paint of ov.fillPaints) copyImagePaint(paint);
|
|
396
446
|
}
|
|
397
447
|
}
|
|
398
448
|
}
|
|
@@ -555,25 +605,36 @@ export class FigDeck {
|
|
|
555
605
|
const bgColor = extractColor(bgPaints) || { r: 1, g: 1, b: 1 };
|
|
556
606
|
const bgLum = luminance(bgColor.r, bgColor.g, bgColor.b);
|
|
557
607
|
|
|
558
|
-
// Walk all descendants looking for TEXT nodes
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
608
|
+
// Walk all descendants looking for TEXT nodes — follow INSTANCE → SYMBOL
|
|
609
|
+
// links so we reach TEXT inside components (walkTree alone stops at INSTANCE).
|
|
610
|
+
const walkIntoSymbols = (rootId, visited = new Set()) => {
|
|
611
|
+
this.walkTree(rootId, (node) => {
|
|
612
|
+
if (node.type === 'TEXT') {
|
|
613
|
+
const textPaints = node.fillPaints;
|
|
614
|
+
const textColor = extractColor(textPaints);
|
|
615
|
+
if (!textColor) return;
|
|
616
|
+
const textLum = luminance(textColor.r, textColor.g, textColor.b);
|
|
617
|
+
const ratio = contrastRatio(bgLum, textLum);
|
|
618
|
+
if (ratio < 2) {
|
|
619
|
+
const nodeId = nid(node);
|
|
620
|
+
let msg = `TEXT ${nodeId} "${node.name || ''}": contrast ratio ${ratio.toFixed(2)}:1 against slide background is below 2:1`;
|
|
621
|
+
if (node.colorVar) {
|
|
622
|
+
msg += ` (uses colorVar "${node.colorVar}" — may resolve differently in Figma)`;
|
|
623
|
+
}
|
|
624
|
+
warnings.push(msg);
|
|
625
|
+
}
|
|
573
626
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
627
|
+
if (node.type === 'INSTANCE' && node.symbolData?.symbolID) {
|
|
628
|
+
const sid = node.symbolData.symbolID;
|
|
629
|
+
const symNid = `${sid.sessionID}:${sid.localID}`;
|
|
630
|
+
if (!visited.has(symNid)) {
|
|
631
|
+
visited.add(symNid);
|
|
632
|
+
walkIntoSymbols(symNid, visited);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
walkIntoSymbols(slideId);
|
|
577
638
|
}
|
|
578
639
|
|
|
579
640
|
for (const w of warnings) console.warn(`⚠️ ${w}`);
|
package/manifest.json
CHANGED