openfig-cli 0.3.22 → 0.3.23
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 +202 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
package/lib/core/fig-deck.mjs
CHANGED
|
@@ -14,11 +14,12 @@ import { decompress } from 'fzstd';
|
|
|
14
14
|
import { inflateRaw, deflateRaw } from 'pako';
|
|
15
15
|
import { ZstdCodec } from 'zstd-codec';
|
|
16
16
|
import yazl from 'yazl';
|
|
17
|
-
import { readFileSync, createWriteStream, existsSync, mkdtempSync, readdirSync } from 'fs';
|
|
17
|
+
import { readFileSync, createWriteStream, existsSync, mkdtempSync, readdirSync, copyFileSync, mkdirSync } from 'fs';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
19
|
import { join, resolve } from 'path';
|
|
20
20
|
import { tmpdir } from 'os';
|
|
21
21
|
import { nid } from './node-helpers.mjs';
|
|
22
|
+
import { deepClone } from './deep-clone.mjs';
|
|
22
23
|
|
|
23
24
|
export class FigDeck {
|
|
24
25
|
constructor() {
|
|
@@ -231,6 +232,206 @@ export class FigDeck {
|
|
|
231
232
|
}
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Import SYMBOL nodes (and their full subtrees) from another FigDeck into this one.
|
|
237
|
+
*
|
|
238
|
+
* Handles:
|
|
239
|
+
* - Deep cloning with full GUID remapping
|
|
240
|
+
* - parentIndex.guid rebinding
|
|
241
|
+
* - symbolData.symbolID remapping for nested INSTANCE nodes
|
|
242
|
+
* - overrideKey remapping
|
|
243
|
+
* - Image/blob file copying between decks
|
|
244
|
+
* - Deduplication by componentKey
|
|
245
|
+
*
|
|
246
|
+
* @param {FigDeck} sourceDeck - The FigDeck to copy symbols from
|
|
247
|
+
* @param {string[]} symbolIds - Array of symbol node IDs in "s:l" format (e.g., ['1:500', '1:600'])
|
|
248
|
+
* @returns {Map<string, string>} Map of old ID → new ID for every remapped node
|
|
249
|
+
*/
|
|
250
|
+
importSymbols(sourceDeck, symbolIds) {
|
|
251
|
+
if (!sourceDeck || !symbolIds?.length) return new Map();
|
|
252
|
+
|
|
253
|
+
// Build componentKey index for dedup in the target deck
|
|
254
|
+
const existingByKey = new Map();
|
|
255
|
+
for (const sym of this.getSymbols()) {
|
|
256
|
+
if (sym.phase === 'REMOVED' || !sym.componentKey) continue;
|
|
257
|
+
existingByKey.set(sym.componentKey, nid(sym));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Find the Internal Only Canvas to parent imported symbols under
|
|
261
|
+
const internalCanvas = this.message.nodeChanges.find(
|
|
262
|
+
n => n.type === 'CANVAS' && n.name === 'Internal Only Canvas' && n.phase !== 'REMOVED'
|
|
263
|
+
);
|
|
264
|
+
if (!internalCanvas) {
|
|
265
|
+
throw new Error('No "Internal Only Canvas" found in target deck');
|
|
266
|
+
}
|
|
267
|
+
const canvasId = nid(internalCanvas);
|
|
268
|
+
|
|
269
|
+
let nextId = this.maxLocalID() + 1;
|
|
270
|
+
const sessionId = 1;
|
|
271
|
+
const globalIdMap = new Map(); // old "s:l" → new "s:l" across all imported symbols
|
|
272
|
+
|
|
273
|
+
for (const symId of symbolIds) {
|
|
274
|
+
const sourceSymbol = sourceDeck.getNode(symId);
|
|
275
|
+
if (!sourceSymbol || sourceSymbol.type !== 'SYMBOL') {
|
|
276
|
+
throw new Error(`SYMBOL not found in source deck: ${symId}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Dedup: if target already has a symbol with the same componentKey, skip
|
|
280
|
+
if (sourceSymbol.componentKey && existingByKey.has(sourceSymbol.componentKey)) {
|
|
281
|
+
globalIdMap.set(symId, existingByKey.get(sourceSymbol.componentKey));
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Collect the full subtree (DFS)
|
|
286
|
+
const subtreeNodes = [];
|
|
287
|
+
sourceDeck.walkTree(symId, node => {
|
|
288
|
+
if (node.phase !== 'REMOVED') subtreeNodes.push(node);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Build ID remap table for this subtree
|
|
292
|
+
const idMap = new Map(); // old "s:l" → new guid { sessionID, localID }
|
|
293
|
+
for (const node of subtreeNodes) {
|
|
294
|
+
idMap.set(nid(node), { sessionID: sessionId, localID: nextId++ });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Clone and remap each node
|
|
298
|
+
const clonedNodes = subtreeNodes.map(node => {
|
|
299
|
+
const clone = deepClone(node);
|
|
300
|
+
const oldId = nid(node);
|
|
301
|
+
const newGuid = idMap.get(oldId);
|
|
302
|
+
if (newGuid) clone.guid = newGuid;
|
|
303
|
+
|
|
304
|
+
// Root symbol → parent under Internal Only Canvas
|
|
305
|
+
if (oldId === symId) {
|
|
306
|
+
clone.parentIndex = {
|
|
307
|
+
guid: deepClone(internalCanvas.guid),
|
|
308
|
+
position: String.fromCharCode(0x21 + this.getChildren(canvasId).length),
|
|
309
|
+
};
|
|
310
|
+
} else if (clone.parentIndex?.guid) {
|
|
311
|
+
// Remap parent reference
|
|
312
|
+
const parentOldId = `${clone.parentIndex.guid.sessionID}:${clone.parentIndex.guid.localID}`;
|
|
313
|
+
const remappedParent = idMap.get(parentOldId);
|
|
314
|
+
if (remappedParent) {
|
|
315
|
+
clone.parentIndex = { ...clone.parentIndex, guid: deepClone(remappedParent) };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Remap symbolData.symbolID on INSTANCE nodes
|
|
320
|
+
if (clone.type === 'INSTANCE' && clone.symbolData?.symbolID) {
|
|
321
|
+
const sid = clone.symbolData.symbolID;
|
|
322
|
+
const symRef = `${sid.sessionID}:${sid.localID}`;
|
|
323
|
+
const remappedSym = idMap.get(symRef);
|
|
324
|
+
if (remappedSym) {
|
|
325
|
+
clone.symbolData.symbolID = deepClone(remappedSym);
|
|
326
|
+
} else if (globalIdMap.has(symRef)) {
|
|
327
|
+
// Reference to a previously imported symbol
|
|
328
|
+
const newRef = globalIdMap.get(symRef);
|
|
329
|
+
const [s, l] = newRef.split(':').map(Number);
|
|
330
|
+
clone.symbolData.symbolID = { sessionID: s, localID: l };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Remap overrideKey
|
|
335
|
+
if (clone.overrideKey) {
|
|
336
|
+
const okOld = `${clone.overrideKey.sessionID}:${clone.overrideKey.localID}`;
|
|
337
|
+
const remappedOk = idMap.get(okOld);
|
|
338
|
+
if (remappedOk) {
|
|
339
|
+
clone.overrideKey = deepClone(remappedOk);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Remap symbolOverrides guid paths
|
|
344
|
+
if (clone.symbolOverrides) {
|
|
345
|
+
for (const ov of clone.symbolOverrides) {
|
|
346
|
+
if (ov.guidPath?.guids) {
|
|
347
|
+
ov.guidPath.guids = ov.guidPath.guids.map(g => {
|
|
348
|
+
const gOld = `${g.sessionID}:${g.localID}`;
|
|
349
|
+
const remapped = idMap.get(gOld);
|
|
350
|
+
return remapped ? deepClone(remapped) : g;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Remap derivedSymbolData references
|
|
357
|
+
if (clone.derivedSymbolData) {
|
|
358
|
+
for (const dsd of clone.derivedSymbolData) {
|
|
359
|
+
if (dsd.symbolID) {
|
|
360
|
+
const dsdOld = `${dsd.symbolID.sessionID}:${dsd.symbolID.localID}`;
|
|
361
|
+
const remappedDsd = idMap.get(dsdOld);
|
|
362
|
+
if (remappedDsd) {
|
|
363
|
+
dsd.symbolID = deepClone(remappedDsd);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
clone.phase = 'CREATED';
|
|
370
|
+
delete clone.slideThumbnailHash;
|
|
371
|
+
delete clone.editInfo;
|
|
372
|
+
delete clone.prototypeInteractions;
|
|
373
|
+
|
|
374
|
+
return clone;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Copy referenced images from source deck
|
|
378
|
+
if (sourceDeck.imagesDir) {
|
|
379
|
+
if (!this.imagesDir) {
|
|
380
|
+
// Create a temp images dir for the target deck
|
|
381
|
+
const tmp = this._tempDir || mkdtempSync(join(tmpdir(), 'openfig_'));
|
|
382
|
+
if (!this._tempDir) this._tempDir = tmp;
|
|
383
|
+
this.imagesDir = join(tmp, 'images');
|
|
384
|
+
mkdirSync(this.imagesDir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const clone of clonedNodes) {
|
|
388
|
+
// Copy IMAGE fills
|
|
389
|
+
if (clone.fillPaints) {
|
|
390
|
+
for (const paint of clone.fillPaints) {
|
|
391
|
+
if (paint.type === 'IMAGE' && paint.image?.name) {
|
|
392
|
+
this._copyImageAsset(sourceDeck.imagesDir, paint.image.name);
|
|
393
|
+
}
|
|
394
|
+
if (paint.imageThumbnail?.name) {
|
|
395
|
+
this._copyImageAsset(sourceDeck.imagesDir, paint.imageThumbnail.name);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Add cloned nodes to target deck
|
|
403
|
+
this.message.nodeChanges.push(...clonedNodes);
|
|
404
|
+
|
|
405
|
+
// Record mappings
|
|
406
|
+
for (const [oldId, newGuid] of idMap.entries()) {
|
|
407
|
+
globalIdMap.set(oldId, `${newGuid.sessionID}:${newGuid.localID}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Register componentKey for dedup of subsequent symbols in the same call
|
|
411
|
+
if (sourceSymbol.componentKey) {
|
|
412
|
+
const newSymId = `${idMap.get(symId).sessionID}:${idMap.get(symId).localID}`;
|
|
413
|
+
existingByKey.set(sourceSymbol.componentKey, newSymId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
this.rebuildMaps();
|
|
418
|
+
return globalIdMap;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Copy an image asset file from a source images directory to this deck's images directory.
|
|
423
|
+
* @param {string} srcImagesDir - Source images directory
|
|
424
|
+
* @param {string} fileName - Image file name (hash)
|
|
425
|
+
*/
|
|
426
|
+
_copyImageAsset(srcImagesDir, fileName) {
|
|
427
|
+
if (!fileName || !this.imagesDir) return;
|
|
428
|
+
const srcPath = join(srcImagesDir, fileName);
|
|
429
|
+
const destPath = join(this.imagesDir, fileName);
|
|
430
|
+
if (existsSync(srcPath) && !existsSync(destPath)) {
|
|
431
|
+
copyFileSync(srcPath, destPath);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
234
435
|
/**
|
|
235
436
|
* Encode message to canvas.fig binary.
|
|
236
437
|
* Returns a Promise<Uint8Array> because zstd-codec uses callbacks.
|
package/manifest.json
CHANGED