ts-visio 1.16.3 → 1.16.17

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.
@@ -7,10 +7,10 @@ import { ShapeModifier } from './ShapeModifier';
7
7
  export interface ConnectorData {
8
8
  /** Visio shape ID of the connector (1D shape). */
9
9
  id: string;
10
- /** ID of the shape at the connector's begin-point (BeginX). */
11
- fromShapeId: string;
12
- /** ID of the shape at the connector's end-point (EndX). */
13
- toShapeId: string;
10
+ /** ID of the shape at the connector's begin-point (BeginX). `undefined` if the begin endpoint is not connected to any shape. */
11
+ fromShapeId: string | undefined;
12
+ /** ID of the shape at the connector's end-point (EndX). `undefined` if the end endpoint is not connected to any shape. */
13
+ toShapeId: string | undefined;
14
14
  /** Connection point used on the from-shape. */
15
15
  fromPort: ConnectionTarget;
16
16
  /** Connection point used on the to-shape. */
@@ -31,10 +31,10 @@ export declare class Connector {
31
31
  private readonly modifier;
32
32
  /** Visio shape ID of this connector. */
33
33
  readonly id: string;
34
- /** ID of the shape this connector starts from. */
35
- readonly fromShapeId: string;
36
- /** ID of the shape this connector ends at. */
37
- readonly toShapeId: string;
34
+ /** ID of the shape this connector starts from. `undefined` if the begin endpoint is not connected. */
35
+ readonly fromShapeId: string | undefined;
36
+ /** ID of the shape this connector ends at. `undefined` if the end endpoint is not connected. */
37
+ readonly toShapeId: string | undefined;
38
38
  /** Connection point used on the from-shape ('center', `{ name }`, or `{ index }`). */
39
39
  readonly fromPort: ConnectionTarget;
40
40
  /** Connection point used on the to-shape ('center', `{ name }`, or `{ index }`). */
@@ -99,6 +99,8 @@ export declare class ShapeModifier {
99
99
  addShape(pageId: string, props: NewShapeProps, parentId?: string): Promise<string>;
100
100
  deleteShape(pageId: string, shapeId: string): Promise<void>;
101
101
  private removeShapeFromTree;
102
+ private findShapeInTree;
103
+ private collectShapeIds;
102
104
  updateShapeText(pageId: string, shapeId: string, newText: string): Promise<void>;
103
105
  updateShapeStyle(pageId: string, shapeId: string, style: ShapeStyle): Promise<void>;
104
106
  /**
@@ -153,7 +153,10 @@ class ShapeModifier {
153
153
  if (!Array.isArray(connSection.Row))
154
154
  connSection.Row = [connSection.Row];
155
155
  const ix = connSection.Row.length;
156
- connSection.Row.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildRow(point, ix));
156
+ const shapeCells = Array.isArray(shape.Cell) ? shape.Cell : shape.Cell ? [shape.Cell] : [];
157
+ const shapeWidth = parseFloat(shapeCells.find((c) => c['@_N'] === 'Width')?.['@_V'] ?? '1');
158
+ const shapeHeight = parseFloat(shapeCells.find((c) => c['@_N'] === 'Height')?.['@_V'] ?? '1');
159
+ connSection.Row.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildRow(point, ix, shapeWidth, shapeHeight));
157
160
  this.cache.saveParsed(pageId, parsed);
158
161
  return ix;
159
162
  }
@@ -225,20 +228,24 @@ class ShapeModifier {
225
228
  }
226
229
  async deleteShape(pageId, shapeId) {
227
230
  const parsed = this.cache.getParsed(pageId);
228
- const removed = this.removeShapeFromTree(parsed.PageContents.Shapes, shapeId);
229
- if (!removed)
231
+ // Collect the deleted shape's ID and all descendant IDs before removal
232
+ // so Connect/Relationship entries for child shapes are also cleaned up.
233
+ const shapeNode = this.findShapeInTree(parsed.PageContents.Shapes, shapeId);
234
+ if (!shapeNode)
230
235
  throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
236
+ const removedIds = this.collectShapeIds(shapeNode);
237
+ this.removeShapeFromTree(parsed.PageContents.Shapes, shapeId);
231
238
  if (parsed.PageContents.Connects?.Connect) {
232
239
  let connects = parsed.PageContents.Connects.Connect;
233
240
  if (!Array.isArray(connects))
234
241
  connects = [connects];
235
- parsed.PageContents.Connects.Connect = connects.filter((c) => c['@_FromSheet'] !== shapeId && c['@_ToSheet'] !== shapeId);
242
+ parsed.PageContents.Connects.Connect = connects.filter((c) => !removedIds.has(c['@_FromSheet']) && !removedIds.has(c['@_ToSheet']));
236
243
  }
237
244
  if (parsed.PageContents.Relationships?.Relationship) {
238
245
  let rels = parsed.PageContents.Relationships.Relationship;
239
246
  if (!Array.isArray(rels))
240
247
  rels = [rels];
241
- parsed.PageContents.Relationships.Relationship = rels.filter((r) => r['@_ShapeID'] !== shapeId && r['@_RelatedShapeID'] !== shapeId);
248
+ parsed.PageContents.Relationships.Relationship = rels.filter((r) => !removedIds.has(r['@_ShapeID']) && !removedIds.has(r['@_RelatedShapeID']));
242
249
  }
243
250
  // Invalidate the shape cache so the map is rebuilt on next access.
244
251
  this.cache.shapeCache.delete(parsed);
@@ -263,6 +270,34 @@ class ShapeModifier {
263
270
  }
264
271
  return false;
265
272
  }
273
+ findShapeInTree(shapesContainer, shapeId) {
274
+ if (!shapesContainer?.Shape)
275
+ return null;
276
+ const shapes = Array.isArray(shapesContainer.Shape) ? shapesContainer.Shape : [shapesContainer.Shape];
277
+ for (const s of shapes) {
278
+ if (s['@_ID'] === shapeId)
279
+ return s;
280
+ if (s.Shapes) {
281
+ const found = this.findShapeInTree(s.Shapes, shapeId);
282
+ if (found)
283
+ return found;
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ collectShapeIds(shape) {
289
+ const ids = new Set();
290
+ const gather = (s) => {
291
+ ids.add(s['@_ID']);
292
+ if (s.Shapes?.Shape) {
293
+ const children = Array.isArray(s.Shapes.Shape) ? s.Shapes.Shape : [s.Shapes.Shape];
294
+ for (const child of children)
295
+ gather(child);
296
+ }
297
+ };
298
+ gather(shape);
299
+ return ids;
300
+ }
266
301
  async updateShapeText(pageId, shapeId, newText) {
267
302
  const parsed = this.cache.getParsed(pageId);
268
303
  const shape = this.cache.getShapeMap(parsed).get(shapeId);
@@ -282,20 +317,73 @@ class ShapeModifier {
282
317
  else if (!Array.isArray(shape.Section)) {
283
318
  shape.Section = [shape.Section];
284
319
  }
320
+ // Upsert a single cell by name inside a flat Cell[].
321
+ const upsertCell = (cells, name, val, extra) => {
322
+ const existing = cells.find((c) => c['@_N'] === name);
323
+ if (existing) {
324
+ existing['@_V'] = val;
325
+ if (extra)
326
+ Object.assign(existing, extra);
327
+ }
328
+ else {
329
+ cells.push({ '@_N': name, '@_V': val, ...extra });
330
+ }
331
+ };
332
+ // Normalise and return the Cell[] of a flat-cell section.
333
+ const ensureCells = (section) => {
334
+ if (!section.Cell)
335
+ section.Cell = [];
336
+ else if (!Array.isArray(section.Cell))
337
+ section.Cell = [section.Cell];
338
+ return section.Cell;
339
+ };
340
+ // Return (creating if absent) the Cell[] of Row[0] in a row-based section.
341
+ const getOrCreateRow0Cells = (section, rowType) => {
342
+ if (!section.Row)
343
+ section.Row = [];
344
+ else if (!Array.isArray(section.Row))
345
+ section.Row = [section.Row];
346
+ let row0 = section.Row.find((r) => r['@_IX'] === '0' || r['@_IX'] === 0);
347
+ if (!row0) {
348
+ row0 = { '@_T': rowType, '@_IX': '0', Cell: [] };
349
+ section.Row.push(row0);
350
+ }
351
+ if (!row0.Cell)
352
+ row0.Cell = [];
353
+ else if (!Array.isArray(row0.Cell))
354
+ row0.Cell = [row0.Cell];
355
+ return row0.Cell;
356
+ };
285
357
  if (style.fillColor) {
286
- shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Fill);
287
- shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
358
+ const existing = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Fill);
359
+ if (existing) {
360
+ upsertCell(ensureCells(existing), 'FillForegnd', style.fillColor, { '@_F': (0, StyleHelpers_1.hexToRgb)(style.fillColor) });
361
+ }
362
+ else {
363
+ shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
364
+ }
288
365
  }
289
366
  const hasLineProps = style.lineColor !== undefined
290
367
  || style.lineWeight !== undefined
291
368
  || style.linePattern !== undefined;
292
369
  if (hasLineProps) {
293
- shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Line);
294
- shape.Section.push((0, StyleHelpers_1.createLineSection)({
295
- color: style.lineColor,
296
- weight: style.lineWeight !== undefined ? (style.lineWeight / 72).toString() : undefined,
297
- pattern: style.linePattern !== undefined ? style.linePattern.toString() : undefined,
298
- }));
370
+ const existing = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Line);
371
+ if (existing) {
372
+ const cells = ensureCells(existing);
373
+ if (style.lineColor !== undefined)
374
+ upsertCell(cells, 'LineColor', style.lineColor, { '@_F': (0, StyleHelpers_1.hexToRgb)(style.lineColor) });
375
+ if (style.lineWeight !== undefined)
376
+ upsertCell(cells, 'LineWeight', (style.lineWeight / 72).toString(), { '@_U': 'IN' });
377
+ if (style.linePattern !== undefined)
378
+ upsertCell(cells, 'LinePattern', style.linePattern.toString());
379
+ }
380
+ else {
381
+ shape.Section.push((0, StyleHelpers_1.createLineSection)({
382
+ color: style.lineColor,
383
+ weight: style.lineWeight !== undefined ? (style.lineWeight / 72).toString() : undefined,
384
+ pattern: style.linePattern !== undefined ? style.linePattern.toString() : undefined,
385
+ }));
386
+ }
299
387
  }
300
388
  const hasCharProps = style.fontColor !== undefined
301
389
  || style.bold !== undefined
@@ -305,42 +393,110 @@ class ShapeModifier {
305
393
  || style.fontSize !== undefined
306
394
  || style.fontFamily !== undefined;
307
395
  if (hasCharProps) {
308
- shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Character);
309
- shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
310
- bold: style.bold,
311
- italic: style.italic,
312
- underline: style.underline,
313
- strikethrough: style.strikethrough,
314
- color: style.fontColor,
315
- fontSize: style.fontSize,
316
- fontFamily: style.fontFamily,
317
- }));
396
+ const existing = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Character);
397
+ if (existing) {
398
+ const cells = getOrCreateRow0Cells(existing, 'Character');
399
+ const hasStyleBits = style.bold !== undefined || style.italic !== undefined
400
+ || style.underline !== undefined || style.strikethrough !== undefined;
401
+ if (hasStyleBits) {
402
+ const styleCell = cells.find((c) => c['@_N'] === 'Style');
403
+ let styleVal = styleCell ? (parseInt(styleCell['@_V'] || '0') || 0) : 0;
404
+ if (style.bold !== undefined) {
405
+ if (style.bold)
406
+ styleVal |= 1;
407
+ else
408
+ styleVal &= ~1;
409
+ }
410
+ if (style.italic !== undefined) {
411
+ if (style.italic)
412
+ styleVal |= 2;
413
+ else
414
+ styleVal &= ~2;
415
+ }
416
+ if (style.underline !== undefined) {
417
+ if (style.underline)
418
+ styleVal |= 4;
419
+ else
420
+ styleVal &= ~4;
421
+ }
422
+ if (style.strikethrough !== undefined) {
423
+ if (style.strikethrough)
424
+ styleVal |= 8;
425
+ else
426
+ styleVal &= ~8;
427
+ }
428
+ upsertCell(cells, 'Style', styleVal.toString());
429
+ }
430
+ if (style.fontColor !== undefined)
431
+ upsertCell(cells, 'Color', style.fontColor, { '@_F': (0, StyleHelpers_1.hexToRgb)(style.fontColor) });
432
+ if (style.fontSize !== undefined)
433
+ upsertCell(cells, 'Size', (style.fontSize / 72).toString(), { '@_U': 'PT' });
434
+ if (style.fontFamily !== undefined)
435
+ upsertCell(cells, 'Font', '0', { '@_F': `FONT("${style.fontFamily}")` });
436
+ }
437
+ else {
438
+ shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
439
+ bold: style.bold,
440
+ italic: style.italic,
441
+ underline: style.underline,
442
+ strikethrough: style.strikethrough,
443
+ color: style.fontColor,
444
+ fontSize: style.fontSize,
445
+ fontFamily: style.fontFamily,
446
+ }));
447
+ }
318
448
  }
319
449
  const hasParagraphProps = style.horzAlign !== undefined
320
450
  || style.spaceBefore !== undefined
321
451
  || style.spaceAfter !== undefined
322
452
  || style.lineSpacing !== undefined;
323
453
  if (hasParagraphProps) {
324
- shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Paragraph);
325
- shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
326
- horzAlign: style.horzAlign,
327
- spaceBefore: style.spaceBefore,
328
- spaceAfter: style.spaceAfter,
329
- lineSpacing: style.lineSpacing,
330
- }));
454
+ const existing = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Paragraph);
455
+ if (existing) {
456
+ const cells = getOrCreateRow0Cells(existing, 'Paragraph');
457
+ if (style.horzAlign !== undefined)
458
+ upsertCell(cells, 'HorzAlign', (0, StyleHelpers_1.horzAlignValue)(style.horzAlign));
459
+ if (style.spaceBefore !== undefined)
460
+ upsertCell(cells, 'SpBefore', (style.spaceBefore / 72).toString(), { '@_U': 'PT' });
461
+ if (style.spaceAfter !== undefined)
462
+ upsertCell(cells, 'SpAfter', (style.spaceAfter / 72).toString(), { '@_U': 'PT' });
463
+ if (style.lineSpacing !== undefined)
464
+ upsertCell(cells, 'SpLine', (-style.lineSpacing).toString());
465
+ }
466
+ else {
467
+ shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
468
+ horzAlign: style.horzAlign,
469
+ spaceBefore: style.spaceBefore,
470
+ spaceAfter: style.spaceAfter,
471
+ lineSpacing: style.lineSpacing,
472
+ }));
473
+ }
331
474
  }
332
475
  const hasTextBlockProps = style.textMarginTop !== undefined
333
476
  || style.textMarginBottom !== undefined
334
477
  || style.textMarginLeft !== undefined
335
478
  || style.textMarginRight !== undefined;
336
479
  if (hasTextBlockProps) {
337
- shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.TextBlock);
338
- shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
339
- topMargin: style.textMarginTop,
340
- bottomMargin: style.textMarginBottom,
341
- leftMargin: style.textMarginLeft,
342
- rightMargin: style.textMarginRight,
343
- }));
480
+ const existing = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.TextBlock);
481
+ if (existing) {
482
+ const cells = ensureCells(existing);
483
+ if (style.textMarginTop !== undefined)
484
+ upsertCell(cells, 'TopMargin', style.textMarginTop.toString(), { '@_U': 'IN' });
485
+ if (style.textMarginBottom !== undefined)
486
+ upsertCell(cells, 'BottomMargin', style.textMarginBottom.toString(), { '@_U': 'IN' });
487
+ if (style.textMarginLeft !== undefined)
488
+ upsertCell(cells, 'LeftMargin', style.textMarginLeft.toString(), { '@_U': 'IN' });
489
+ if (style.textMarginRight !== undefined)
490
+ upsertCell(cells, 'RightMargin', style.textMarginRight.toString(), { '@_U': 'IN' });
491
+ }
492
+ else {
493
+ shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
494
+ topMargin: style.textMarginTop,
495
+ bottomMargin: style.textMarginBottom,
496
+ leftMargin: style.textMarginLeft,
497
+ rightMargin: style.textMarginRight,
498
+ }));
499
+ }
344
500
  }
345
501
  if (style.verticalAlign !== undefined) {
346
502
  if (!shape.Cell)
@@ -427,6 +583,7 @@ class ShapeModifier {
427
583
  upsert('LocPinX', (width / 2).toString());
428
584
  upsert('LocPinY', (height / 2).toString());
429
585
  // Keep cached @_V consistent for renderers that don't evaluate formulas.
586
+ // Evaluate any formula that references Width or Height (e.g. 'Width*0.5').
430
587
  if (shape.Section) {
431
588
  const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
432
589
  for (const section of sections) {
@@ -438,10 +595,23 @@ class ShapeModifier {
438
595
  continue;
439
596
  const cells = Array.isArray(row.Cell) ? row.Cell : [row.Cell];
440
597
  for (const cell of cells) {
441
- if (cell['@_F'] === 'Width')
442
- cell['@_V'] = width.toString();
443
- if (cell['@_F'] === 'Height')
444
- cell['@_V'] = height.toString();
598
+ const formula = cell['@_F'];
599
+ if (typeof formula !== 'string')
600
+ continue;
601
+ if (!formula.includes('Width') && !formula.includes('Height'))
602
+ continue;
603
+ const expr = formula
604
+ .replace(/\bWidth\b/g, width.toString())
605
+ .replace(/\bHeight\b/g, height.toString());
606
+ // Validate expression contains only safe arithmetic characters
607
+ if (!/^[\d\s.+\-*/()]+$/.test(expr))
608
+ continue;
609
+ try {
610
+ // eslint-disable-next-line no-new-func
611
+ const result = Function(`"use strict"; return (${expr})`)();
612
+ cell['@_V'] = result.toString();
613
+ }
614
+ catch { /* ignore malformed formulas */ }
445
615
  }
446
616
  }
447
617
  }
@@ -108,13 +108,12 @@ class ShapeReader {
108
108
  for (const connShape of connectorShapes) {
109
109
  const connId = connShape['@_ID'];
110
110
  const entry = connectsByConnector.get(connId);
111
- if (!entry?.beginConnect || !entry?.endConnect)
112
- continue;
113
- const { beginConnect, endConnect } = entry;
114
- const fromShapeId = beginConnect['@_ToSheet'];
115
- const toShapeId = endConnect['@_ToSheet'];
116
- const fromPort = this.decodeToPart(beginConnect['@_ToPart']);
117
- const toPort = this.decodeToPart(endConnect['@_ToPart']);
111
+ const beginConnect = entry?.beginConnect;
112
+ const endConnect = entry?.endConnect;
113
+ const fromShapeId = beginConnect?.['@_ToSheet'];
114
+ const toShapeId = endConnect?.['@_ToSheet'];
115
+ const fromPort = this.decodeToPart(beginConnect?.['@_ToPart']);
116
+ const toPort = this.decodeToPart(endConnect?.['@_ToPart']);
118
117
  // Extract line style from the connector's Line section
119
118
  const sections = (0, VisioParsers_1.asArray)(connShape.Section);
120
119
  let lineColor;
@@ -106,7 +106,7 @@ class VisioDocument {
106
106
  const internalPages = this.pageManager.load();
107
107
  this._pageCache = internalPages.map(p => {
108
108
  const pageStub = {
109
- ID: p.id.toString(),
109
+ ID: p.id,
110
110
  Name: p.name,
111
111
  // Thread the relationship-resolved path so that loaded files
112
112
  // with non-sequential page filenames are handled correctly.
@@ -84,7 +84,7 @@ class ContainerEditor {
84
84
  let shapes = shapesContainer.Shape;
85
85
  if (!Array.isArray(shapes))
86
86
  shapes = [shapes];
87
- const idx = shapes.findIndex((s) => s['@_ID'] == shapeId);
87
+ const idx = shapes.findIndex((s) => s['@_ID'] === shapeId);
88
88
  if (idx === -1)
89
89
  return;
90
90
  const shape = shapes[idx];
@@ -25,7 +25,8 @@ export declare class LayerEditor {
25
25
  locked: boolean;
26
26
  }>;
27
27
  /**
28
- * Delete a layer by index and remove it from all shape LayerMember cells.
28
+ * Delete a layer by index, re-index remaining layers to close gaps, and
29
+ * update all shape LayerMember cells to use the new indices.
29
30
  */
30
31
  deleteLayer(pageId: string, layerIndex: number): void;
31
32
  /**
@@ -139,21 +139,35 @@ class LayerEditor {
139
139
  });
140
140
  }
141
141
  /**
142
- * Delete a layer by index and remove it from all shape LayerMember cells.
142
+ * Delete a layer by index, re-index remaining layers to close gaps, and
143
+ * update all shape LayerMember cells to use the new indices.
143
144
  */
144
145
  deleteLayer(pageId, layerIndex) {
145
146
  const parsed = this.cache.getParsed(pageId);
147
+ // Build old-index → new-index mapping while removing the deleted layer.
148
+ const indexRemap = new Map();
146
149
  const pageSheet = parsed.PageContents?.PageSheet;
147
150
  if (pageSheet?.Section) {
148
151
  const sections = Array.isArray(pageSheet.Section) ? pageSheet.Section : [pageSheet.Section];
149
152
  const layerSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Layer);
150
153
  if (layerSection?.Row) {
151
154
  const rows = Array.isArray(layerSection.Row) ? layerSection.Row : [layerSection.Row];
152
- layerSection.Row = rows.filter((r) => r['@_IX'] !== layerIndex.toString());
155
+ // Sort by numeric IX so re-indexing is deterministic.
156
+ const remaining = rows
157
+ .filter((r) => r['@_IX'] !== layerIndex.toString())
158
+ .sort((a, b) => parseInt(a['@_IX'], 10) - parseInt(b['@_IX'], 10));
159
+ remaining.forEach((row, newIx) => {
160
+ const oldIx = row['@_IX'];
161
+ if (oldIx !== newIx.toString()) {
162
+ indexRemap.set(oldIx, newIx.toString());
163
+ row['@_IX'] = newIx.toString();
164
+ }
165
+ });
166
+ layerSection.Row = remaining;
153
167
  }
154
168
  }
155
- // Remove this layer index from every shape's LayerMember cell.
156
- const idxStr = layerIndex.toString();
169
+ // Update every shape's LayerMember cell: remove deleted index, remap survivors.
170
+ const deletedStr = layerIndex.toString();
157
171
  for (const [, shape] of this.cache.getShapeMap(parsed)) {
158
172
  if (!shape.Section)
159
173
  continue;
@@ -169,10 +183,11 @@ class LayerEditor {
169
183
  const memberCell = cells.find((c) => c['@_N'] === 'LayerMember');
170
184
  if (!memberCell?.['@_V'])
171
185
  continue;
172
- const remaining = memberCell['@_V']
186
+ const updated = memberCell['@_V']
173
187
  .split(';')
174
- .filter((s) => s.length > 0 && s !== idxStr);
175
- memberCell['@_V'] = remaining.join(';');
188
+ .filter((s) => s.length > 0 && s !== deletedStr)
189
+ .map((s) => indexRemap.get(s) ?? s);
190
+ memberCell['@_V'] = updated.join(';');
176
191
  }
177
192
  this.cache.saveParsed(pageId, parsed);
178
193
  }
@@ -1,6 +1,6 @@
1
1
  import { VisioPackage } from '../VisioPackage';
2
2
  export interface PageEntry {
3
- id: number;
3
+ id: string;
4
4
  name: string;
5
5
  relId: string;
6
6
  xmlPath: string;
@@ -67,7 +67,7 @@ class PageManager {
67
67
  const backPageAttr = node['@_BackPage'];
68
68
  const backPageId = backPageAttr ? backPageAttr.toString() : undefined;
69
69
  return {
70
- id: parseInt(node['@_ID']),
70
+ id: String(node['@_ID']),
71
71
  name: node['@_Name'],
72
72
  relId: rId,
73
73
  xmlPath: fullPath,
@@ -89,8 +89,9 @@ class PageManager {
89
89
  this.load();
90
90
  let maxId = 0;
91
91
  for (const p of this.pages) {
92
- if (p.id > maxId)
93
- maxId = p.id;
92
+ const numId = Number(p.id);
93
+ if (numId > maxId)
94
+ maxId = numId;
94
95
  }
95
96
  const newId = maxId + 1;
96
97
  const fileName = `page${newId}.xml`;
@@ -150,7 +151,7 @@ class PageManager {
150
151
  */
151
152
  async deletePage(pageId) {
152
153
  this.load();
153
- const page = this.pages.find(p => p.id.toString() === pageId);
154
+ const page = this.pages.find(p => p.id === pageId);
154
155
  if (!page)
155
156
  throw new Error(`Page ${pageId} not found`);
156
157
  const pageFileName = page.xmlPath.split('/').pop();
@@ -242,11 +243,11 @@ class PageManager {
242
243
  */
243
244
  async duplicatePage(pageId, newName) {
244
245
  this.load();
245
- const source = this.pages.find(p => p.id.toString() === pageId);
246
+ const source = this.pages.find(p => p.id === pageId);
246
247
  if (!source)
247
248
  throw new Error(`Page ${pageId} not found`);
248
249
  // 1. Calculate new ID and paths
249
- const maxId = Math.max(...this.pages.map(p => p.id));
250
+ const maxId = Math.max(...this.pages.map(p => Number(p.id)));
250
251
  const newId = maxId + 1;
251
252
  const newFileName = `page${newId}.xml`;
252
253
  const newPath = `visio/pages/${newFileName}`;
@@ -91,6 +91,14 @@ class PageXmlCache {
91
91
  }
92
92
  getParsed(pageId) {
93
93
  const pagePath = this.getPagePath(pageId);
94
+ // If there are pending (unflushed) mutations, the in-memory cache is the
95
+ // authoritative source of truth. Skip the pkg-content comparison to avoid
96
+ // re-parsing stale pkg content and discarding in-flight mutations (Bug 6).
97
+ if (this.dirtyPages.has(pagePath)) {
98
+ const dirty = this.pageCache.get(pagePath);
99
+ if (dirty)
100
+ return dirty.parsed;
101
+ }
94
102
  let content;
95
103
  try {
96
104
  content = this.pkg.getFileText(pagePath);
@@ -12,9 +12,9 @@ import { ConnectionPointDef, ConnectionTarget } from '../types/VisioTypes';
12
12
  */
13
13
  export declare class ConnectionPointBuilder {
14
14
  /** Build the raw XML object for a Connection section from a list of point definitions. */
15
- static buildConnectionSection(points: ConnectionPointDef[]): any;
15
+ static buildConnectionSection(points: ConnectionPointDef[], width?: number, height?: number): any;
16
16
  /** Build a single Connection row XML object. */
17
- static buildRow(point: ConnectionPointDef, ix: number): any;
17
+ static buildRow(point: ConnectionPointDef, ix: number, width?: number, height?: number): any;
18
18
  /**
19
19
  * Resolve a `ConnectionTarget` against a raw shape XML object.
20
20
  *
@@ -19,19 +19,19 @@ const TYPE_VALUES = {
19
19
  */
20
20
  class ConnectionPointBuilder {
21
21
  /** Build the raw XML object for a Connection section from a list of point definitions. */
22
- static buildConnectionSection(points) {
22
+ static buildConnectionSection(points, width = 1, height = 1) {
23
23
  return {
24
24
  '@_N': 'Connection',
25
- Row: points.map((pt, ix) => this.buildRow(pt, ix)),
25
+ Row: points.map((pt, ix) => this.buildRow(pt, ix, width, height)),
26
26
  };
27
27
  }
28
28
  /** Build a single Connection row XML object. */
29
- static buildRow(point, ix) {
29
+ static buildRow(point, ix, width = 1, height = 1) {
30
30
  const row = {
31
31
  '@_IX': ix.toString(),
32
32
  Cell: [
33
- { '@_N': 'X', '@_V': '0', '@_F': `Width*${point.xFraction}` },
34
- { '@_N': 'Y', '@_V': '0', '@_F': `Height*${point.yFraction}` },
33
+ { '@_N': 'X', '@_V': (width * point.xFraction).toString(), '@_F': `Width*${point.xFraction}` },
34
+ { '@_N': 'Y', '@_V': (height * point.yFraction).toString(), '@_F': `Height*${point.yFraction}` },
35
35
  { '@_N': 'DirX', '@_V': point.direction ? point.direction.x.toString() : '0' },
36
36
  { '@_N': 'DirY', '@_V': point.direction ? point.direction.y.toString() : '0' },
37
37
  { '@_N': 'Type', '@_V': point.type ? TYPE_VALUES[point.type] : '0' },
@@ -29,6 +29,9 @@ export declare class ConnectorBuilder {
29
29
  '@_NameU': string;
30
30
  '@_Name': string;
31
31
  '@_Type': string;
32
+ '@_LineStyle': string;
33
+ '@_FillStyle': string;
34
+ '@_TextStyle': string;
32
35
  Cell: ({
33
36
  '@_N': string;
34
37
  '@_V': any;
@@ -153,6 +153,9 @@ class ConnectorBuilder {
153
153
  '@_NameU': 'Dynamic connector',
154
154
  '@_Name': 'Dynamic connector',
155
155
  '@_Type': 'Shape',
156
+ '@_LineStyle': '0',
157
+ '@_FillStyle': '0',
158
+ '@_TextStyle': '0',
156
159
  Cell: [
157
160
  { '@_N': 'BeginX', '@_V': beginX.toString() },
158
161
  { '@_N': 'BeginY', '@_V': beginY.toString() },
@@ -18,7 +18,9 @@ class ForeignShapeBuilder {
18
18
  { '@_N': 'Width', '@_V': props.width.toString() },
19
19
  { '@_N': 'Height', '@_V': props.height.toString() },
20
20
  { '@_N': 'LocPinX', '@_V': (props.width / 2).toString(), '@_F': 'Width*0.5' },
21
- { '@_N': 'LocPinY', '@_V': (props.height / 2).toString(), '@_F': 'Height*0.5' }
21
+ { '@_N': 'LocPinY', '@_V': (props.height / 2).toString(), '@_F': 'Height*0.5' },
22
+ { '@_N': 'ImgWidth', '@_V': props.width.toString(), '@_F': 'Width' },
23
+ { '@_N': 'ImgHeight', '@_V': props.height.toString(), '@_F': 'Height' }
22
24
  ],
23
25
  Section: [
24
26
  // Foreign shapes typically have no border (LinePattern=0)
@@ -49,7 +49,10 @@ function section(rows, noFill) {
49
49
  return {
50
50
  '@_N': 'Geometry',
51
51
  '@_IX': '0',
52
- Cell: [{ '@_N': 'NoFill', '@_V': noFill }],
52
+ Cell: [
53
+ { '@_N': 'NoShow', '@_V': '0' },
54
+ { '@_N': 'NoFill', '@_V': noFill },
55
+ ],
53
56
  Row: rows,
54
57
  };
55
58
  }
@@ -47,7 +47,7 @@ class ShapeBuilder {
47
47
  shape.Section.push((0, StyleHelpers_1.createFillSection)(props.fillColor));
48
48
  }
49
49
  const hasLineProps = props.lineColor !== undefined || props.linePattern !== undefined;
50
- if (props.fillColor || hasLineProps) {
50
+ if (hasLineProps) {
51
51
  shape.Section.push((0, StyleHelpers_1.createLineSection)({
52
52
  color: props.lineColor,
53
53
  pattern: props.linePattern !== undefined ? props.linePattern.toString() : undefined,
@@ -100,7 +100,7 @@ class ShapeBuilder {
100
100
  shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(props.verticalAlign) });
101
101
  }
102
102
  if (props.connectionPoints && props.connectionPoints.length > 0) {
103
- shape.Section.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildConnectionSection(props.connectionPoints));
103
+ shape.Section.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildConnectionSection(props.connectionPoints, props.width, props.height));
104
104
  }
105
105
  // Groups and master instances inherit geometry from their children / master definition.
106
106
  if (props.type !== VisioConstants_1.SHAPE_TYPES.Group && !props.masterId) {
@@ -54,7 +54,19 @@ exports.PAGES_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?
54
54
  </Relationships>`;
55
55
  exports.PAGE1_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
56
56
  <PageContents xmlns="${VisioConstants_1.XML_NAMESPACES.VISIO_MAIN}" xmlns:r="${VisioConstants_1.XML_NAMESPACES.RELATIONSHIPS_OFFICE}" xml:space="preserve">
57
+ <PageSheet LineStyle="0" FillStyle="0" TextStyle="0">
58
+ <Cell N="PageWidth" V="8.5"/>
59
+ <Cell N="PageHeight" V="11"/>
60
+ <Cell N="PageScale" V="1" Unit="MSG"/>
61
+ <Cell N="DrawingScale" V="1" Unit="MSG"/>
62
+ <Cell N="DrawingSizeType" V="0"/>
63
+ <Cell N="DrawingScaleType" V="0"/>
64
+ <Cell N="Inhibited" V="0"/>
65
+ <Cell N="UIVisibility" V="0"/>
66
+ <Cell N="PageDrawSizeType" V="0"/>
67
+ </PageSheet>
57
68
  <Shapes/>
69
+ <Connects/>
58
70
  </PageContents>`;
59
71
  exports.WINDOWS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
60
72
  <Windows xmlns="${VisioConstants_1.XML_NAMESPACES.VISIO_MAIN}" xmlns:r="${VisioConstants_1.XML_NAMESPACES.RELATIONSHIPS_OFFICE}" xml:space="preserve">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-visio",
3
- "version": "1.16.3",
3
+ "version": "1.16.17",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {