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.
- package/dist/Connector.d.ts +8 -8
- package/dist/ShapeModifier.d.ts +2 -0
- package/dist/ShapeModifier.js +211 -41
- package/dist/ShapeReader.js +6 -7
- package/dist/VisioDocument.js +1 -1
- package/dist/core/ContainerEditor.js +1 -1
- package/dist/core/LayerEditor.d.ts +2 -1
- package/dist/core/LayerEditor.js +22 -7
- package/dist/core/PageManager.d.ts +1 -1
- package/dist/core/PageManager.js +7 -6
- package/dist/core/PageXmlCache.js +8 -0
- package/dist/shapes/ConnectionPointBuilder.d.ts +2 -2
- package/dist/shapes/ConnectionPointBuilder.js +5 -5
- package/dist/shapes/ConnectorBuilder.d.ts +3 -0
- package/dist/shapes/ConnectorBuilder.js +3 -0
- package/dist/shapes/ForeignShapeBuilder.js +3 -1
- package/dist/shapes/GeometryBuilder.js +4 -1
- package/dist/shapes/ShapeBuilder.js +2 -2
- package/dist/templates/MinimalVsdx.js +12 -0
- package/package.json +1 -1
package/dist/Connector.d.ts
CHANGED
|
@@ -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 }`). */
|
package/dist/ShapeModifier.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/ShapeModifier.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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']
|
|
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']
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
}
|
package/dist/ShapeReader.js
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
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;
|
package/dist/VisioDocument.js
CHANGED
|
@@ -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
|
|
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']
|
|
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
|
|
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
|
/**
|
package/dist/core/LayerEditor.js
CHANGED
|
@@ -139,21 +139,35 @@ class LayerEditor {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
|
-
* Delete a layer by index
|
|
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
|
-
|
|
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
|
-
//
|
|
156
|
-
const
|
|
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
|
|
186
|
+
const updated = memberCell['@_V']
|
|
173
187
|
.split(';')
|
|
174
|
-
.filter((s) => s.length > 0 && s !==
|
|
175
|
-
|
|
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
|
}
|
package/dist/core/PageManager.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
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':
|
|
34
|
-
{ '@_N': 'Y', '@_V':
|
|
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' },
|
|
@@ -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)
|
|
@@ -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 (
|
|
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">
|