openfig-cli 0.3.23 → 0.3.24

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.
@@ -366,6 +366,39 @@ export class FigDeck {
366
366
  }
367
367
  }
368
368
 
369
+ // Remap blob indices (commandsBlob, vectorNetworkBlob, fillGeometry etc.)
370
+ // Blobs in source deck reference positions in sourceDeck.message.blobs.
371
+ // Copy each referenced blob to target and update the index.
372
+ const remapBlobIndex = (idx) => {
373
+ if (idx == null || idx < 0) return idx;
374
+ if (!sourceDeck.message.blobs || idx >= sourceDeck.message.blobs.length) return idx;
375
+ if (!this._blobRemap) this._blobRemap = new Map();
376
+ if (this._blobRemap.has(idx)) return this._blobRemap.get(idx);
377
+ const blob = sourceDeck.message.blobs[idx];
378
+ if (!this.message.blobs) this.message.blobs = [];
379
+ const newIdx = this.message.blobs.length;
380
+ this.message.blobs.push(deepClone(blob));
381
+ this._blobRemap.set(idx, newIdx);
382
+ return newIdx;
383
+ };
384
+
385
+ // fillGeometry[].commandsBlob
386
+ if (clone.fillGeometry) {
387
+ for (const fg of clone.fillGeometry) {
388
+ if (fg.commandsBlob != null) fg.commandsBlob = remapBlobIndex(fg.commandsBlob);
389
+ }
390
+ }
391
+ // strokeGeometry[].commandsBlob
392
+ if (clone.strokeGeometry) {
393
+ for (const sg of clone.strokeGeometry) {
394
+ if (sg.commandsBlob != null) sg.commandsBlob = remapBlobIndex(sg.commandsBlob);
395
+ }
396
+ }
397
+ // vectorNetworkBlob (VECTOR nodes)
398
+ if (clone.vectorNetworkBlob != null) {
399
+ clone.vectorNetworkBlob = remapBlobIndex(clone.vectorNetworkBlob);
400
+ }
401
+
369
402
  clone.phase = 'CREATED';
370
403
  delete clone.slideThumbnailHash;
371
404
  delete clone.editInfo;
@@ -374,6 +407,9 @@ export class FigDeck {
374
407
  return clone;
375
408
  });
376
409
 
410
+ // Clear blob remap cache after processing this symbol's subtree
411
+ delete this._blobRemap;
412
+
377
413
  // Copy referenced images from source deck
378
414
  if (sourceDeck.imagesDir) {
379
415
  if (!this.imagesDir) {
@@ -555,25 +591,36 @@ export class FigDeck {
555
591
  const bgColor = extractColor(bgPaints) || { r: 1, g: 1, b: 1 };
556
592
  const bgLum = luminance(bgColor.r, bgColor.g, bgColor.b);
557
593
 
558
- // Walk all descendants looking for TEXT nodes
559
- this.walkTree(slideId, (node) => {
560
- if (node.type !== 'TEXT') return;
561
- const textPaints = node.fillPaints;
562
- const textColor = extractColor(textPaints);
563
- if (!textColor) return;
564
-
565
- const textLum = luminance(textColor.r, textColor.g, textColor.b);
566
- const ratio = contrastRatio(bgLum, textLum);
567
-
568
- if (ratio < 2) {
569
- const nodeId = nid(node);
570
- let msg = `TEXT ${nodeId} "${node.name || ''}": contrast ratio ${ratio.toFixed(2)}:1 against slide background is below 2:1`;
571
- if (node.colorVar) {
572
- msg += ` (uses colorVar "${node.colorVar}" — may resolve differently in Figma)`;
594
+ // Walk all descendants looking for TEXT nodes — follow INSTANCE → SYMBOL
595
+ // links so we reach TEXT inside components (walkTree alone stops at INSTANCE).
596
+ const walkIntoSymbols = (rootId, visited = new Set()) => {
597
+ this.walkTree(rootId, (node) => {
598
+ if (node.type === 'TEXT') {
599
+ const textPaints = node.fillPaints;
600
+ const textColor = extractColor(textPaints);
601
+ if (!textColor) return;
602
+ const textLum = luminance(textColor.r, textColor.g, textColor.b);
603
+ const ratio = contrastRatio(bgLum, textLum);
604
+ if (ratio < 2) {
605
+ const nodeId = nid(node);
606
+ let msg = `TEXT ${nodeId} "${node.name || ''}": contrast ratio ${ratio.toFixed(2)}:1 against slide background is below 2:1`;
607
+ if (node.colorVar) {
608
+ msg += ` (uses colorVar "${node.colorVar}" — may resolve differently in Figma)`;
609
+ }
610
+ warnings.push(msg);
611
+ }
573
612
  }
574
- warnings.push(msg);
575
- }
576
- });
613
+ if (node.type === 'INSTANCE' && node.symbolData?.symbolID) {
614
+ const sid = node.symbolData.symbolID;
615
+ const symNid = `${sid.sessionID}:${sid.localID}`;
616
+ if (!visited.has(symNid)) {
617
+ visited.add(symNid);
618
+ walkIntoSymbols(symNid, visited);
619
+ }
620
+ }
621
+ });
622
+ };
623
+ walkIntoSymbols(slideId);
577
624
  }
578
625
 
579
626
  for (const w of warnings) console.warn(`⚠️ ${w}`);
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.2",
3
3
  "name": "openfig",
4
- "version": "0.3.23",
4
+ "version": "0.3.24",
5
5
  "description": "Open-source tools for Figma file parsing and rendering",
6
6
  "author": {
7
7
  "name": "OpenFig Contributors"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfig-cli",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
4
4
  "description": "OpenFig — Open-source tools for Figma file parsing and rendering",
5
5
  "type": "module",
6
6
  "bin": {