openfig-cli 0.3.28 → 0.3.30

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.
@@ -124,6 +124,51 @@ export async function run(args, flags) {
124
124
  delete newInst.derivedSymbolDataLayoutVersion;
125
125
  delete newInst.editInfo;
126
126
 
127
+ // Clone sibling nodes (non-INSTANCE children of template slide, e.g. logo vectors)
128
+ const tmplSlideId = nid(tmplSlide);
129
+ const tmplInstId = nid(tmplInst);
130
+ const tmplChildren = deck.childrenMap.get(tmplSlideId) || [];
131
+ const siblingNodes = [];
132
+ const idRemap = new Map();
133
+
134
+ for (const child of tmplChildren) {
135
+ if (nid(child) === tmplInstId) continue;
136
+ if (child.phase === 'REMOVED') continue;
137
+
138
+ // Collect this node and all descendants
139
+ const subtree = [];
140
+ function collectSubtree(nodeId) {
141
+ const node = deck.getNode(nodeId);
142
+ if (!node || node.phase === 'REMOVED') return;
143
+ subtree.push(node);
144
+ const kids = deck.childrenMap.get(nodeId) || [];
145
+ for (const kid of kids) collectSubtree(nid(kid));
146
+ }
147
+ collectSubtree(nid(child));
148
+
149
+ // Clone each node with new IDs, re-parenting to new slide
150
+ for (const node of subtree) {
151
+ const oldId = nid(node);
152
+ const newLocalId = nextId++;
153
+ idRemap.set(oldId, newLocalId);
154
+
155
+ const cloned = deepClone(node);
156
+ cloned.guid = { sessionID: 1, localID: newLocalId };
157
+ cloned.phase = 'CREATED';
158
+ delete cloned.editInfo;
159
+
160
+ if (cloned.parentIndex?.guid) {
161
+ const parentOldId = `${cloned.parentIndex.guid.sessionID}:${cloned.parentIndex.guid.localID}`;
162
+ if (parentOldId === tmplSlideId) {
163
+ cloned.parentIndex.guid = { sessionID: 1, localID: slideId };
164
+ } else if (idRemap.has(parentOldId)) {
165
+ cloned.parentIndex.guid = { sessionID: 1, localID: idRemap.get(parentOldId) };
166
+ }
167
+ }
168
+ siblingNodes.push(cloned);
169
+ }
170
+ }
171
+
127
172
  // Apply text overrides
128
173
  for (const pair of sets) {
129
174
  const eqIdx = pair.indexOf('=');
@@ -176,10 +221,14 @@ export async function run(args, flags) {
176
221
  }
177
222
  deck.message.nodeChanges.push(newSlide);
178
223
  deck.message.nodeChanges.push(newInst);
224
+ for (const sib of siblingNodes) {
225
+ deck.message.nodeChanges.push(sib);
226
+ }
179
227
  deck.rebuildMaps();
180
228
 
181
229
  const moduleNote = newModule ? ` + MODULE 1:${moduleId}` : '';
182
- console.log(`Cloned slide "${tmplSlide.name}" "${newName}" (1:${slideId} + 1:${instId}${moduleNote})`);
230
+ const sibNote = siblingNodes.length ? `, ${siblingNodes.length} sibling node(s)` : '';
231
+ console.log(`Cloned slide "${tmplSlide.name}" → "${newName}" (1:${slideId} + 1:${instId}${moduleNote}${sibNote})`);
183
232
  console.log(` ${sets.length} text override(s), ${setImages.length} image override(s)`);
184
233
 
185
234
  const bytes = await deck.saveDeck(outPath);
@@ -271,6 +271,8 @@ function esc(s) {
271
271
  */
272
272
  function isStaleLayout(chars, baselines, glyphs) {
273
273
  if (!chars) return false;
274
+ // No derivedTextData at all (e.g. programmatically created text)
275
+ if (!baselines?.length && !glyphs?.length) return true;
274
276
  const len = chars.length;
275
277
 
276
278
  if (baselines?.length) {
@@ -356,9 +358,27 @@ function fallbackTextTspans(dispChars, fontSize, node) {
356
358
  : adjLineAscent;
357
359
  }
358
360
 
361
+ // List marker support: read lineType from textData.lines
362
+ const linesMeta = node.textData?.lines ?? [];
363
+ let orderedCounter = 0;
364
+
359
365
  const tspans = lines.map((line, i) => {
360
366
  const y = startY + i * adjLineHeight;
361
- return `<tspan x="${startX}" y="${y.toFixed(2)}" text-anchor="${anchor}">${esc(line) || ' '}</tspan>`;
367
+ const meta = linesMeta[i];
368
+ const lineType = meta?.lineType;
369
+ const indent = (meta?.indentationLevel ?? 0) * adjFontSize * 0.8;
370
+ let prefix = '';
371
+ if (lineType === 'UNORDERED_LIST') {
372
+ prefix = '\u2022 '; // bullet •
373
+ orderedCounter = 0;
374
+ } else if (lineType === 'ORDERED_LIST') {
375
+ orderedCounter++;
376
+ prefix = `${orderedCounter}. `;
377
+ } else {
378
+ orderedCounter = 0;
379
+ }
380
+ const x = startX + indent;
381
+ return `<tspan x="${x.toFixed(2)}" y="${y.toFixed(2)}" text-anchor="${anchor}">${esc(prefix + line) || ' '}</tspan>`;
362
382
  }).join('');
363
383
 
364
384
  return { tspans, fontSize: adjFontSize };
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.2",
3
3
  "name": "openfig",
4
- "version": "0.3.28",
4
+ "version": "0.3.30",
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.28",
3
+ "version": "0.3.30",
4
4
  "description": "OpenFig — Open-source tools for Figma file parsing and rendering",
5
5
  "type": "module",
6
6
  "bin": {