ts-visio 1.0.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +343 -0
  3. package/dist/Layer.d.ts +12 -0
  4. package/dist/Layer.js +35 -0
  5. package/dist/Page.d.ts +30 -0
  6. package/dist/Page.js +169 -0
  7. package/dist/PageManager.d.ts +8 -0
  8. package/dist/PageManager.js +35 -0
  9. package/dist/SchemaDiagram.d.ts +22 -0
  10. package/dist/SchemaDiagram.js +36 -0
  11. package/dist/Shape.d.ts +68 -0
  12. package/dist/Shape.js +203 -0
  13. package/dist/ShapeModifier.d.ts +66 -0
  14. package/dist/ShapeModifier.js +889 -0
  15. package/dist/ShapeReader.d.ts +9 -0
  16. package/dist/ShapeReader.js +51 -0
  17. package/dist/VisioDocument.d.ts +21 -0
  18. package/dist/VisioDocument.js +119 -0
  19. package/dist/VisioPackage.d.ts +10 -0
  20. package/dist/VisioPackage.js +112 -0
  21. package/dist/core/MasterManager.d.ts +15 -0
  22. package/dist/core/MasterManager.js +43 -0
  23. package/dist/core/MediaConstants.d.ts +5 -0
  24. package/dist/core/MediaConstants.js +16 -0
  25. package/dist/core/MediaManager.d.ts +13 -0
  26. package/dist/core/MediaManager.js +88 -0
  27. package/dist/core/PageManager.d.ts +28 -0
  28. package/dist/core/PageManager.js +244 -0
  29. package/dist/core/RelsManager.d.ts +11 -0
  30. package/dist/core/RelsManager.js +81 -0
  31. package/dist/core/VisioConstants.d.ts +38 -0
  32. package/dist/core/VisioConstants.js +41 -0
  33. package/dist/core/VisioValidator.d.ts +27 -0
  34. package/dist/core/VisioValidator.js +362 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.js +32 -0
  37. package/dist/shapes/ConnectorBuilder.d.ts +37 -0
  38. package/dist/shapes/ConnectorBuilder.js +173 -0
  39. package/dist/shapes/ContainerBuilder.d.ts +6 -0
  40. package/dist/shapes/ContainerBuilder.js +103 -0
  41. package/dist/shapes/ForeignShapeBuilder.d.ts +4 -0
  42. package/dist/shapes/ForeignShapeBuilder.js +47 -0
  43. package/dist/shapes/ShapeBuilder.d.ts +4 -0
  44. package/dist/shapes/ShapeBuilder.js +68 -0
  45. package/dist/templates/MinimalVsdx.d.ts +10 -0
  46. package/dist/templates/MinimalVsdx.js +66 -0
  47. package/dist/types/VisioTypes.d.ts +85 -0
  48. package/dist/types/VisioTypes.js +14 -0
  49. package/dist/utils/StubHelpers.d.ts +7 -0
  50. package/dist/utils/StubHelpers.js +16 -0
  51. package/dist/utils/StyleHelpers.d.ts +30 -0
  52. package/dist/utils/StyleHelpers.js +95 -0
  53. package/dist/utils/VisioParsers.d.ts +6 -0
  54. package/dist/utils/VisioParsers.js +45 -0
  55. package/package.json +27 -0
@@ -0,0 +1,889 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShapeModifier = void 0;
4
+ const fast_xml_parser_1 = require("fast-xml-parser");
5
+ const RelsManager_1 = require("./core/RelsManager");
6
+ const StyleHelpers_1 = require("./utils/StyleHelpers");
7
+ const VisioConstants_1 = require("./core/VisioConstants");
8
+ const ForeignShapeBuilder_1 = require("./shapes/ForeignShapeBuilder");
9
+ const ShapeBuilder_1 = require("./shapes/ShapeBuilder");
10
+ const ConnectorBuilder_1 = require("./shapes/ConnectorBuilder");
11
+ const ContainerBuilder_1 = require("./shapes/ContainerBuilder");
12
+ class ShapeModifier {
13
+ // ...
14
+ async addContainer(pageId, props) {
15
+ const pagePath = this.getPagePath(pageId);
16
+ let content = this.pkg.getFileText(pagePath);
17
+ const parsed = this.parser.parse(content);
18
+ // Ensure Shapes container...
19
+ if (!parsed.PageContents.Shapes)
20
+ parsed.PageContents.Shapes = { Shape: [] };
21
+ let topLevelShapes = parsed.PageContents.Shapes.Shape;
22
+ if (!Array.isArray(topLevelShapes)) {
23
+ topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
24
+ parsed.PageContents.Shapes.Shape = topLevelShapes;
25
+ }
26
+ let newId = props.id || this.getNextId(parsed);
27
+ const containerShape = ContainerBuilder_1.ContainerBuilder.createContainerShape(newId, props);
28
+ topLevelShapes.push(containerShape);
29
+ const newXml = this.builder.build(parsed);
30
+ this.pkg.updateFile(pagePath, newXml);
31
+ return newId;
32
+ this.pkg.updateFile(pagePath, newXml);
33
+ return newId;
34
+ }
35
+ async addList(pageId, props, direction = 'vertical') {
36
+ const pagePath = this.getPagePath(pageId);
37
+ let content = this.pkg.getFileText(pagePath);
38
+ const parsed = this.parser.parse(content);
39
+ // Ensure Shapes container...
40
+ if (!parsed.PageContents.Shapes)
41
+ parsed.PageContents.Shapes = { Shape: [] };
42
+ let topLevelShapes = parsed.PageContents.Shapes.Shape;
43
+ if (!Array.isArray(topLevelShapes)) {
44
+ topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
45
+ parsed.PageContents.Shapes.Shape = topLevelShapes;
46
+ }
47
+ let newId = props.id || this.getNextId(parsed);
48
+ const listShape = ContainerBuilder_1.ContainerBuilder.createContainerShape(newId, props);
49
+ ContainerBuilder_1.ContainerBuilder.makeList(listShape, direction);
50
+ topLevelShapes.push(listShape);
51
+ const newXml = this.builder.build(parsed);
52
+ this.pkg.updateFile(pagePath, newXml);
53
+ return newId;
54
+ }
55
+ constructor(pkg) {
56
+ this.pkg = pkg;
57
+ this.pageCache = new Map();
58
+ this.dirtyPages = new Set();
59
+ this.autoSave = true;
60
+ this.parser = new fast_xml_parser_1.XMLParser({
61
+ ignoreAttributes: false,
62
+ attributeNamePrefix: "@_"
63
+ });
64
+ this.builder = new fast_xml_parser_1.XMLBuilder({
65
+ ignoreAttributes: false,
66
+ attributeNamePrefix: "@_",
67
+ format: true
68
+ });
69
+ this.relsManager = new RelsManager_1.RelsManager(pkg);
70
+ }
71
+ getPagePath(pageId) {
72
+ return `visio/pages/page${pageId}.xml`;
73
+ }
74
+ getAllShapes(parsed) {
75
+ let topLevelShapes = parsed.PageContents.Shapes ? parsed.PageContents.Shapes.Shape : [];
76
+ if (!Array.isArray(topLevelShapes)) {
77
+ topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
78
+ }
79
+ const all = [];
80
+ const gather = (shapeList) => {
81
+ for (const s of shapeList) {
82
+ all.push(s);
83
+ if (s.Shapes && s.Shapes.Shape) {
84
+ const children = Array.isArray(s.Shapes.Shape) ? s.Shapes.Shape : [s.Shapes.Shape];
85
+ gather(children);
86
+ }
87
+ }
88
+ };
89
+ gather(topLevelShapes);
90
+ return all;
91
+ }
92
+ getNextId(parsed) {
93
+ // Updates PageSheet.NextShapeID to prevent ID conflicts.
94
+ // Calculates the next ID from existing shapes and increments the counter.
95
+ const allShapes = this.getAllShapes(parsed);
96
+ let maxId = 0;
97
+ for (const s of allShapes) {
98
+ const id = parseInt(s['@_ID']);
99
+ if (!isNaN(id) && id > maxId)
100
+ maxId = id;
101
+ }
102
+ const nextId = maxId + 1;
103
+ // Update PageSheet so that NextShapeID always points to the next available shape ID (store nextId + 1)
104
+ this.updateNextShapeId(parsed, nextId + 1);
105
+ return nextId.toString();
106
+ }
107
+ ensurePageSheet(parsed) {
108
+ if (!parsed.PageContents.PageSheet) {
109
+ // Enforce order: PageSheet, Shapes, Connects
110
+ // Save existing elements
111
+ const shapes = parsed.PageContents.Shapes;
112
+ const connects = parsed.PageContents.Connects;
113
+ const rels = parsed.PageContents.Relationships;
114
+ // Delete to re-insert in order
115
+ if (shapes)
116
+ delete parsed.PageContents.Shapes;
117
+ if (connects)
118
+ delete parsed.PageContents.Connects;
119
+ if (rels)
120
+ delete parsed.PageContents.Relationships;
121
+ // Insert PageSheet first
122
+ parsed.PageContents.PageSheet = { Cell: [] };
123
+ // Restore others in order
124
+ if (shapes)
125
+ parsed.PageContents.Shapes = shapes;
126
+ if (connects)
127
+ parsed.PageContents.Connects = connects;
128
+ if (rels)
129
+ parsed.PageContents.Relationships = rels;
130
+ }
131
+ if (!Array.isArray(parsed.PageContents.PageSheet.Cell)) {
132
+ parsed.PageContents.PageSheet.Cell = parsed.PageContents.PageSheet.Cell ? [parsed.PageContents.PageSheet.Cell] : [];
133
+ }
134
+ }
135
+ updateNextShapeId(parsed, nextVal) {
136
+ this.ensurePageSheet(parsed);
137
+ const cells = parsed.PageContents.PageSheet.Cell;
138
+ const cell = cells.find((c) => c['@_N'] === 'NextShapeID');
139
+ if (cell) {
140
+ cell['@_V'] = nextVal.toString();
141
+ }
142
+ else {
143
+ cells.push({ '@_N': 'NextShapeID', '@_V': nextVal.toString() });
144
+ }
145
+ }
146
+ getParsed(pageId) {
147
+ const pagePath = this.getPagePath(pageId);
148
+ let content;
149
+ try {
150
+ content = this.pkg.getFileText(pagePath);
151
+ }
152
+ catch {
153
+ throw new Error(`Could not find page file for ID ${pageId}. Expected at ${pagePath}`);
154
+ }
155
+ const cached = this.pageCache.get(pagePath);
156
+ if (cached && cached.content === content) {
157
+ return cached.parsed;
158
+ }
159
+ const parsed = this.parser.parse(content);
160
+ this.pageCache.set(pagePath, { content, parsed });
161
+ return parsed;
162
+ }
163
+ saveParsed(pageId, parsed) {
164
+ const pagePath = this.getPagePath(pageId);
165
+ if (!this.autoSave) {
166
+ this.dirtyPages.add(pagePath);
167
+ return;
168
+ }
169
+ this.performSave(pagePath, parsed);
170
+ }
171
+ performSave(pagePath, parsed) {
172
+ const newXml = this.builder.build(parsed);
173
+ this.pkg.updateFile(pagePath, newXml);
174
+ this.pageCache.set(pagePath, { content: newXml, parsed });
175
+ }
176
+ flush() {
177
+ for (const pagePath of this.dirtyPages) {
178
+ const cached = this.pageCache.get(pagePath);
179
+ if (cached && cached.parsed) {
180
+ this.performSave(pagePath, cached.parsed);
181
+ }
182
+ }
183
+ this.dirtyPages.clear();
184
+ }
185
+ async addConnector(pageId, fromShapeId, toShapeId, beginArrow, endArrow) {
186
+ const parsed = this.getParsed(pageId);
187
+ // Ensure Shapes collection exists
188
+ if (!parsed.PageContents.Shapes) {
189
+ parsed.PageContents.Shapes = { Shape: [] };
190
+ }
191
+ if (!Array.isArray(parsed.PageContents.Shapes.Shape)) {
192
+ parsed.PageContents.Shapes.Shape = [parsed.PageContents.Shapes.Shape];
193
+ }
194
+ const newId = this.getNextId(parsed);
195
+ const shapeHierarchy = ConnectorBuilder_1.ConnectorBuilder.buildShapeHierarchy(parsed);
196
+ // Validate arrow values (Visio supports 0-45)
197
+ const validateArrow = (val) => {
198
+ if (!val)
199
+ return '0';
200
+ const num = parseInt(val);
201
+ if (isNaN(num) || num < 0 || num > 45)
202
+ return '0';
203
+ return val;
204
+ };
205
+ const layout = ConnectorBuilder_1.ConnectorBuilder.calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy);
206
+ const connectorShape = ConnectorBuilder_1.ConnectorBuilder.createConnectorShapeObject(newId, layout, validateArrow(beginArrow), validateArrow(endArrow));
207
+ const topLevelShapes = parsed.PageContents.Shapes.Shape;
208
+ topLevelShapes.push(connectorShape);
209
+ ConnectorBuilder_1.ConnectorBuilder.addConnectorToConnects(parsed, newId, fromShapeId, toShapeId);
210
+ this.saveParsed(pageId, parsed);
211
+ return newId;
212
+ }
213
+ async addShape(pageId, props, parentId) {
214
+ const parsed = this.getParsed(pageId);
215
+ // Ensure Shapes container exists
216
+ if (!parsed.PageContents.Shapes) {
217
+ parsed.PageContents.Shapes = { Shape: [] };
218
+ }
219
+ let topLevelShapes = parsed.PageContents.Shapes.Shape;
220
+ if (!Array.isArray(topLevelShapes)) {
221
+ topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
222
+ parsed.PageContents.Shapes.Shape = topLevelShapes;
223
+ }
224
+ const allShapes = this.getAllShapes(parsed);
225
+ // Auto-generate ID if not provided
226
+ let newId = props.id;
227
+ if (!newId) {
228
+ newId = this.getNextId(parsed);
229
+ }
230
+ let newShape;
231
+ if (props.type === 'Foreign' && props.imgRelId) {
232
+ newShape = ForeignShapeBuilder_1.ForeignShapeBuilder.createImageShapeObject(newId, props.imgRelId, props);
233
+ // Text for foreign shapes? Usually none, but we can support it.
234
+ if (props.text !== undefined && props.text !== null) {
235
+ newShape.Text = { '#text': props.text };
236
+ }
237
+ }
238
+ else {
239
+ // Standard Shape creation logic
240
+ newShape = ShapeBuilder_1.ShapeBuilder.createStandardShape(newId, props);
241
+ if (props.masterId) {
242
+ // Phase 3: Ensure Relationship
243
+ await this.relsManager.ensureRelationship(`visio/pages/page${pageId}.xml`, '../masters/masters.xml', VisioConstants_1.RELATIONSHIP_TYPES.MASTERS);
244
+ }
245
+ }
246
+ if (parentId) {
247
+ // Add to Parent Group
248
+ const parent = allShapes.find((s) => s['@_ID'] == parentId);
249
+ if (!parent) {
250
+ throw new Error(`Parent shape ${parentId} not found`);
251
+ }
252
+ // Ensure Parent has Shapes collection
253
+ if (!parent.Shapes) {
254
+ parent.Shapes = { Shape: [] };
255
+ }
256
+ if (!Array.isArray(parent.Shapes.Shape)) {
257
+ parent.Shapes.Shape = parent.Shapes.Shape ? [parent.Shapes.Shape] : [];
258
+ }
259
+ // Mark parent as Group if not already
260
+ if (parent['@_Type'] !== 'Group') {
261
+ parent['@_Type'] = 'Group';
262
+ }
263
+ parent.Shapes.Shape.push(newShape);
264
+ }
265
+ else {
266
+ // Add to Page
267
+ topLevelShapes.push(newShape);
268
+ }
269
+ this.saveParsed(pageId, parsed);
270
+ return newId;
271
+ }
272
+ async updateShapeText(pageId, shapeId, newText) {
273
+ const parsed = this.getParsed(pageId);
274
+ let found = false;
275
+ // Helper to recursively find and update shape
276
+ const findAndUpdate = (shapes) => {
277
+ for (const shape of shapes) {
278
+ if (shape['@_ID'] == shapeId) {
279
+ shape.Text = {
280
+ '#text': newText
281
+ };
282
+ found = true;
283
+ return;
284
+ }
285
+ // If shapes can be nested (Groups), check for sub-shapes - future improvement
286
+ // But typically basic Text is on top level or group level.
287
+ }
288
+ };
289
+ const shapesData = parsed.PageContents?.Shapes?.Shape;
290
+ if (shapesData) {
291
+ const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
292
+ findAndUpdate(shapesArray);
293
+ }
294
+ if (!found) {
295
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
296
+ }
297
+ this.saveParsed(pageId, parsed);
298
+ }
299
+ async updateShapeStyle(pageId, shapeId, style) {
300
+ const parsed = this.getParsed(pageId);
301
+ let found = false;
302
+ const findAndUpdate = (shapes) => {
303
+ for (const shape of shapes) {
304
+ if (shape['@_ID'] == shapeId) {
305
+ found = true;
306
+ // Ensure Section array exists
307
+ if (!shape.Section) {
308
+ shape.Section = [];
309
+ }
310
+ else if (!Array.isArray(shape.Section)) {
311
+ shape.Section = [shape.Section];
312
+ }
313
+ // Update/Add Fill
314
+ if (style.fillColor) {
315
+ // Remove existing Fill section if any (simplified: assuming IX=0)
316
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Fill');
317
+ shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
318
+ }
319
+ // Update/Add Character (Font/Text Style)
320
+ if (style.fontColor || style.bold !== undefined) {
321
+ // Remove existing Character section if any
322
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Character');
323
+ shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
324
+ bold: style.bold,
325
+ color: style.fontColor
326
+ }));
327
+ }
328
+ return;
329
+ }
330
+ }
331
+ };
332
+ const shapesData = parsed.PageContents?.Shapes?.Shape;
333
+ if (shapesData) {
334
+ const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
335
+ findAndUpdate(shapesArray);
336
+ }
337
+ if (!found) {
338
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
339
+ }
340
+ this.saveParsed(pageId, parsed);
341
+ }
342
+ async updateShapeDimensions(pageId, shapeId, w, h) {
343
+ const parsed = this.getParsed(pageId);
344
+ const shapes = this.getAllShapes(parsed);
345
+ const shape = shapes.find((s) => s['@_ID'] == shapeId);
346
+ if (!shape)
347
+ throw new Error(`Shape ${shapeId} not found`);
348
+ // Ensure Cell array
349
+ if (!shape.Cell)
350
+ shape.Cell = [];
351
+ if (!Array.isArray(shape.Cell))
352
+ shape.Cell = [shape.Cell];
353
+ const updateCell = (name, val) => {
354
+ const cell = shape.Cell.find((c) => c['@_N'] === name);
355
+ if (cell)
356
+ cell['@_V'] = val;
357
+ else
358
+ shape.Cell.push({ '@_N': name, '@_V': val });
359
+ };
360
+ updateCell('Width', w.toString());
361
+ updateCell('Height', h.toString());
362
+ this.saveParsed(pageId, parsed);
363
+ }
364
+ async updateShapePosition(pageId, shapeId, x, y) {
365
+ const parsed = this.getParsed(pageId);
366
+ let found = false;
367
+ const findAndUpdate = (shapes) => {
368
+ for (const shape of shapes) {
369
+ if (shape['@_ID'] == shapeId) {
370
+ found = true;
371
+ // Ensure Cell array exists
372
+ if (!shape.Cell) {
373
+ shape.Cell = [];
374
+ }
375
+ else if (!Array.isArray(shape.Cell)) {
376
+ shape.Cell = [shape.Cell];
377
+ }
378
+ // Helper to update specific cell
379
+ const updateCell = (name, value) => {
380
+ const cell = shape.Cell.find((c) => c['@_N'] === name);
381
+ if (cell) {
382
+ cell['@_V'] = value;
383
+ }
384
+ else {
385
+ shape.Cell.push({ '@_N': name, '@_V': value });
386
+ }
387
+ };
388
+ updateCell('PinX', x.toString());
389
+ updateCell('PinY', y.toString());
390
+ return;
391
+ }
392
+ }
393
+ };
394
+ const shapesData = parsed.PageContents?.Shapes?.Shape;
395
+ if (shapesData) {
396
+ const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
397
+ findAndUpdate(shapesArray);
398
+ }
399
+ if (!found) {
400
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
401
+ }
402
+ this.saveParsed(pageId, parsed);
403
+ }
404
+ addPropertyDefinition(pageId, shapeId, name, type, options = {}) {
405
+ const parsed = this.getParsed(pageId);
406
+ let found = false;
407
+ const findAndUpdate = (shapes) => {
408
+ for (const shape of shapes) {
409
+ if (shape['@_ID'] == shapeId) {
410
+ found = true;
411
+ // Ensure Section array exists
412
+ if (!shape.Section)
413
+ shape.Section = [];
414
+ if (!Array.isArray(shape.Section))
415
+ shape.Section = [shape.Section];
416
+ // Find or Create Property Section
417
+ let propSection = shape.Section.find((s) => s['@_N'] === 'Property');
418
+ if (!propSection) {
419
+ propSection = { '@_N': 'Property', Row: [] };
420
+ shape.Section.push(propSection);
421
+ }
422
+ // Ensure Row array exists
423
+ if (!propSection.Row)
424
+ propSection.Row = [];
425
+ if (!Array.isArray(propSection.Row))
426
+ propSection.Row = [propSection.Row];
427
+ // Check if property already exists
428
+ const existingRow = propSection.Row.find((r) => r['@_N'] === `Prop.${name}`);
429
+ if (existingRow) {
430
+ // Update existing Definition
431
+ const updateCell = (n, v) => {
432
+ let c = existingRow.Cell.find((x) => x['@_N'] === n);
433
+ if (c)
434
+ c['@_V'] = v;
435
+ else
436
+ existingRow.Cell.push({ '@_N': n, '@_V': v });
437
+ };
438
+ if (options.label !== undefined)
439
+ updateCell('Label', options.label);
440
+ updateCell('Type', type.toString());
441
+ if (options.invisible !== undefined)
442
+ updateCell('Invisible', options.invisible ? '1' : '0');
443
+ }
444
+ else {
445
+ // Create New Row
446
+ propSection.Row.push({
447
+ '@_N': `Prop.${name}`,
448
+ Cell: [
449
+ { '@_N': 'Label', '@_V': options.label || name }, // Default label to name
450
+ { '@_N': 'Type', '@_V': type.toString() },
451
+ { '@_N': 'Invisible', '@_V': options.invisible ? '1' : '0' },
452
+ { '@_N': 'Value', '@_V': '0' } // Initialize with default
453
+ ]
454
+ });
455
+ }
456
+ return;
457
+ }
458
+ // Recurse into nested shapes (groups/containers)
459
+ if (shape.Shapes?.Shape) {
460
+ const children = Array.isArray(shape.Shapes.Shape) ? shape.Shapes.Shape : [shape.Shapes.Shape];
461
+ findAndUpdate(children);
462
+ }
463
+ }
464
+ };
465
+ const shapesData = parsed.PageContents?.Shapes?.Shape;
466
+ if (shapesData) {
467
+ const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
468
+ findAndUpdate(shapesArray);
469
+ }
470
+ if (!found) {
471
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
472
+ }
473
+ this.saveParsed(pageId, parsed);
474
+ }
475
+ dateToVisioString(date) {
476
+ // Visio typically accepts ISO 8601 strings for Type 5
477
+ // Example: 2022-01-01T00:00:00
478
+ return date.toISOString().split('.')[0]; // remove milliseconds
479
+ }
480
+ setPropertyValue(pageId, shapeId, name, value) {
481
+ const parsed = this.getParsed(pageId);
482
+ let found = false;
483
+ const findAndUpdate = (shapes) => {
484
+ for (const shape of shapes) {
485
+ if (shape['@_ID'] == shapeId) {
486
+ found = true;
487
+ // Ensure Section array exists
488
+ const sections = shape.Section ? (Array.isArray(shape.Section) ? shape.Section : [shape.Section]) : [];
489
+ const propSection = sections.find((s) => s['@_N'] === 'Property');
490
+ if (!propSection) {
491
+ throw new Error(`Property definition 'Prop.${name}' does not exist on shape ${shapeId}. Call addPropertyDefinition first.`);
492
+ }
493
+ const rows = propSection.Row ? (Array.isArray(propSection.Row) ? propSection.Row : [propSection.Row]) : [];
494
+ const row = rows.find((r) => r['@_N'] === `Prop.${name}`);
495
+ if (!row) {
496
+ throw new Error(`Property definition 'Prop.${name}' does not exist on shape ${shapeId}. Call addPropertyDefinition first.`);
497
+ }
498
+ // Determine Visio Value String
499
+ let visioValue = '';
500
+ if (value instanceof Date) {
501
+ visioValue = this.dateToVisioString(value);
502
+ }
503
+ else if (typeof value === 'boolean') {
504
+ visioValue = value ? '1' : '0'; // Should boolean be V='TRUE' or 1? Standard practice is often 1/0 or TRUE/FALSE. Cells are formulaic.
505
+ // However, if the Type is 3 (Boolean), Visio often expects 0/1 or TRUE/FALSE.
506
+ // Let's stick to '1'/'0' for safety in formulas if generic.
507
+ }
508
+ else {
509
+ visioValue = value.toString();
510
+ }
511
+ // Update or Add Value Cell
512
+ // Note: If Type is String (0), V="String". If Number (2), V="123".
513
+ // Visio often puts string values in formulae as "String", but in XML V attribute it's raw text?
514
+ // Actually, for String props, V usually contains the string.
515
+ let valCell = row.Cell.find((c) => c['@_N'] === 'Value');
516
+ if (valCell) {
517
+ valCell['@_V'] = visioValue;
518
+ }
519
+ else {
520
+ row.Cell.push({ '@_N': 'Value', '@_V': visioValue });
521
+ }
522
+ return;
523
+ }
524
+ // Recurse into nested shapes (groups/containers)
525
+ if (shape.Shapes?.Shape) {
526
+ const children = Array.isArray(shape.Shapes.Shape) ? shape.Shapes.Shape : [shape.Shapes.Shape];
527
+ findAndUpdate(children);
528
+ }
529
+ }
530
+ };
531
+ const shapesData = parsed.PageContents?.Shapes?.Shape;
532
+ if (shapesData) {
533
+ const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
534
+ findAndUpdate(shapesArray);
535
+ }
536
+ if (!found) {
537
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
538
+ }
539
+ this.saveParsed(pageId, parsed);
540
+ }
541
+ getShapeGeometry(pageId, shapeId) {
542
+ const parsed = this.getParsed(pageId);
543
+ const shapes = this.getAllShapes(parsed);
544
+ const shape = shapes.find((s) => s['@_ID'] == shapeId);
545
+ if (!shape)
546
+ throw new Error(`Shape ${shapeId} not found`);
547
+ const getCellVal = (name) => {
548
+ // Ensure Cell is array
549
+ if (!shape.Cell)
550
+ return 0;
551
+ const cells = Array.isArray(shape.Cell) ? shape.Cell : [shape.Cell];
552
+ const c = cells.find((cell) => cell['@_N'] === name);
553
+ return c ? Number(c['@_V']) : 0;
554
+ };
555
+ return {
556
+ x: getCellVal('PinX'),
557
+ y: getCellVal('PinY'),
558
+ width: getCellVal('Width'),
559
+ height: getCellVal('Height')
560
+ };
561
+ }
562
+ async addRelationship(pageId, shapeId, relatedShapeId, type) {
563
+ const parsed = this.getParsed(pageId);
564
+ // Ensure Relationships collection exists in PageContents
565
+ if (!parsed.PageContents.Relationships) {
566
+ parsed.PageContents.Relationships = { Relationship: [] };
567
+ }
568
+ // Ensure Relationship is an array (Standard robustness pattern)
569
+ if (!Array.isArray(parsed.PageContents.Relationships.Relationship)) {
570
+ parsed.PageContents.Relationships.Relationship = parsed.PageContents.Relationships.Relationship
571
+ ? [parsed.PageContents.Relationships.Relationship]
572
+ : [];
573
+ }
574
+ const relationships = parsed.PageContents.Relationships.Relationship;
575
+ // Check definition: Type, ShapeID (Container), RelatedShapeID (Member)
576
+ // Avoid duplicates?
577
+ const exists = relationships.find((r) => r['@_Type'] === type &&
578
+ r['@_ShapeID'] === shapeId &&
579
+ r['@_RelatedShapeID'] === relatedShapeId);
580
+ if (!exists) {
581
+ relationships.push({
582
+ '@_Type': type,
583
+ '@_ShapeID': shapeId,
584
+ '@_RelatedShapeID': relatedShapeId
585
+ });
586
+ this.saveParsed(pageId, parsed);
587
+ }
588
+ }
589
+ getContainerMembers(pageId, containerId) {
590
+ const parsed = this.getParsed(pageId);
591
+ const rels = parsed.PageContents?.Relationships?.Relationship;
592
+ if (!rels)
593
+ return [];
594
+ const relsArray = Array.isArray(rels) ? rels : [rels];
595
+ return relsArray
596
+ .filter((r) => r['@_Type'] === 'Container' && r['@_ShapeID'] === containerId)
597
+ .map((r) => r['@_RelatedShapeID']);
598
+ }
599
+ async reorderShape(pageId, shapeId, position) {
600
+ const parsed = this.getParsed(pageId);
601
+ const shapesContainer = parsed.PageContents?.Shapes;
602
+ if (!shapesContainer || !shapesContainer.Shape)
603
+ return;
604
+ let shapes = shapesContainer.Shape;
605
+ if (!Array.isArray(shapes))
606
+ shapes = [shapes];
607
+ const idx = shapes.findIndex((s) => s['@_ID'] == shapeId);
608
+ if (idx === -1)
609
+ return;
610
+ const shape = shapes[idx];
611
+ shapes.splice(idx, 1); // Remove
612
+ if (position === 'back') {
613
+ shapes.unshift(shape); // Add to start (Back of Z-Order)
614
+ }
615
+ else {
616
+ shapes.push(shape); // Add to end (Front of Z-Order)
617
+ }
618
+ // Update array in object
619
+ shapesContainer.Shape = shapes;
620
+ this.saveParsed(pageId, parsed);
621
+ }
622
+ async addListItem(pageId, listId, itemId) {
623
+ // 1. Get List Properties (Direction, Spacing)
624
+ const parsed = this.getParsed(pageId);
625
+ const shapes = this.getAllShapes(parsed);
626
+ const listShape = shapes.find((s) => s['@_ID'] == listId);
627
+ if (!listShape)
628
+ throw new Error(`List ${listId} not found`);
629
+ const getUserVal = (name, def) => {
630
+ if (!listShape.Section)
631
+ return def;
632
+ const userSec = listShape.Section.find((s) => s['@_N'] === 'User');
633
+ if (!userSec || !userSec.Row)
634
+ return def;
635
+ const rows = Array.isArray(userSec.Row) ? userSec.Row : [userSec.Row];
636
+ const row = rows.find((r) => r['@_N'] === name);
637
+ if (!row || !row.Cell)
638
+ return def;
639
+ // Value cell
640
+ const valCell = Array.isArray(row.Cell) ? row.Cell.find((c) => c['@_N'] === 'Value') : row.Cell;
641
+ return valCell ? valCell['@_V'] : def;
642
+ };
643
+ const direction = parseInt(getUserVal('msvSDListDirection', '1')); // 1=Vert, 0=Horiz
644
+ const spacing = parseFloat(getUserVal('msvSDListSpacing', '0.125').replace(/[^0-9.]/g, '')); // Crude parse if unit included
645
+ // 2. Determine Position
646
+ const memberIds = this.getContainerMembers(pageId, listId);
647
+ const itemGeo = this.getShapeGeometry(pageId, itemId);
648
+ const listGeo = this.getShapeGeometry(pageId, listId);
649
+ let newX = listGeo.x;
650
+ let newY = listGeo.y;
651
+ if (memberIds.length === 0) {
652
+ // First Item: Place at Top/Left of Container (with some internal margin/padding)
653
+ // For simplicity, center on Container center or rely on resizeToFit to adjust container AROUND it later.
654
+ // Let's place it at current container PinX/PinY
655
+ newX = listGeo.x;
656
+ newY = listGeo.y;
657
+ }
658
+ else {
659
+ const lastId = memberIds[memberIds.length - 1];
660
+ const lastGeo = this.getShapeGeometry(pageId, lastId);
661
+ if (direction === 1) { // Vertical (Stack Down)
662
+ // Last Bottom - Spacing - ItemHalfHeight
663
+ const lastBottom = lastGeo.y - (lastGeo.height / 2);
664
+ newY = lastBottom - spacing - (itemGeo.height / 2);
665
+ newX = lastGeo.x; // Align Centers
666
+ }
667
+ else { // Horizontal (Stack Right)
668
+ // Last Right + Spacing + ItemHalfWidth
669
+ const lastRight = lastGeo.x + (lastGeo.width / 2);
670
+ newX = lastRight + spacing + (itemGeo.width / 2);
671
+ newY = lastGeo.y; // Align Centers
672
+ }
673
+ }
674
+ // 3. Update Item Position
675
+ await this.updateShapePosition(pageId, itemId, newX, newY);
676
+ // 4. Add Relationship
677
+ await this.addRelationship(pageId, listId, itemId, 'Container');
678
+ // 5. Resize List Container
679
+ await this.resizeContainerToFit(pageId, listId, 0.25);
680
+ }
681
+ async resizeContainerToFit(pageId, containerId, padding = 0.25) {
682
+ const memberIds = this.getContainerMembers(pageId, containerId);
683
+ if (memberIds.length === 0)
684
+ return;
685
+ // Calculate Bounding Box
686
+ let minX = Infinity, minY = Infinity;
687
+ let maxX = -Infinity, maxY = -Infinity;
688
+ for (const mid of memberIds) {
689
+ const geo = this.getShapeGeometry(pageId, mid);
690
+ // Visio PinX/PinY is center. Bounding box needs Left/Bottom/Right/Top
691
+ const left = geo.x - (geo.width / 2);
692
+ const right = geo.x + (geo.width / 2);
693
+ const bottom = geo.y - (geo.height / 2);
694
+ const top = geo.y + (geo.height / 2);
695
+ if (left < minX)
696
+ minX = left;
697
+ if (right > maxX)
698
+ maxX = right;
699
+ if (bottom < minY)
700
+ minY = bottom;
701
+ if (top > maxY)
702
+ maxY = top;
703
+ }
704
+ // Apply Padding
705
+ minX -= padding;
706
+ maxX += padding;
707
+ minY -= padding;
708
+ maxY += padding;
709
+ const newWidth = maxX - minX;
710
+ const newHeight = maxY - minY;
711
+ const newPinX = minX + (newWidth / 2);
712
+ const newPinY = minY + (newHeight / 2);
713
+ // Update Geometry
714
+ await this.updateShapePosition(pageId, containerId, newPinX, newPinY);
715
+ await this.updateShapeDimensions(pageId, containerId, newWidth, newHeight);
716
+ // Update Z-Order (Send to Back)
717
+ await this.reorderShape(pageId, containerId, 'back');
718
+ }
719
+ async addHyperlink(pageId, shapeId, details) {
720
+ const parsed = this.getParsed(pageId);
721
+ const shapes = this.getAllShapes(parsed);
722
+ const shape = shapes.find((s) => s['@_ID'] == shapeId);
723
+ if (!shape)
724
+ throw new Error(`Shape ${shapeId} not found`);
725
+ // Ensure Section array
726
+ if (!shape.Section)
727
+ shape.Section = [];
728
+ if (!Array.isArray(shape.Section))
729
+ shape.Section = [shape.Section];
730
+ // Find or Create Hyperlink Section
731
+ let linkSection = shape.Section.find((s) => s['@_N'] === 'Hyperlink');
732
+ if (!linkSection) {
733
+ linkSection = { '@_N': 'Hyperlink', Row: [] };
734
+ shape.Section.push(linkSection);
735
+ }
736
+ // Ensure Row array
737
+ if (!linkSection.Row)
738
+ linkSection.Row = [];
739
+ if (!Array.isArray(linkSection.Row))
740
+ linkSection.Row = [linkSection.Row];
741
+ // Determine next Row ID (Hyperlink.Row_1, Hyperlink.Row_2, etc.)
742
+ const nextIdx = linkSection.Row.length + 1;
743
+ const rowName = `Hyperlink.Row_${nextIdx}`;
744
+ const newRow = {
745
+ '@_N': rowName,
746
+ Cell: []
747
+ };
748
+ if (details.address !== undefined) {
749
+ // XMLBuilder handles XML escaping automatically
750
+ newRow.Cell.push({ '@_N': 'Address', '@_V': details.address });
751
+ }
752
+ if (details.subAddress !== undefined) {
753
+ newRow.Cell.push({ '@_N': 'SubAddress', '@_V': details.subAddress });
754
+ }
755
+ if (details.description !== undefined) {
756
+ newRow.Cell.push({ '@_N': 'Description', '@_V': details.description });
757
+ }
758
+ // Default NewWindow to 0 (false)
759
+ newRow.Cell.push({ '@_N': 'NewWindow', '@_V': '0' });
760
+ linkSection.Row.push(newRow);
761
+ this.saveParsed(pageId, parsed);
762
+ }
763
+ async addLayer(pageId, name, options = {}) {
764
+ const parsed = this.getParsed(pageId);
765
+ // Ensure PageSheet
766
+ this.ensurePageSheet(parsed);
767
+ const pageSheet = parsed.PageContents.PageSheet;
768
+ // Ensure Section array
769
+ if (!pageSheet.Section)
770
+ pageSheet.Section = [];
771
+ if (!Array.isArray(pageSheet.Section))
772
+ pageSheet.Section = [pageSheet.Section];
773
+ // Find or Create Layer Section
774
+ let layerSection = pageSheet.Section.find((s) => s['@_N'] === 'Layer');
775
+ if (!layerSection) {
776
+ layerSection = { '@_N': 'Layer', Row: [] };
777
+ pageSheet.Section.push(layerSection);
778
+ }
779
+ // Ensure Row array
780
+ if (!layerSection.Row)
781
+ layerSection.Row = [];
782
+ if (!Array.isArray(layerSection.Row))
783
+ layerSection.Row = [layerSection.Row];
784
+ // Verify name uniqueness (Visio allows duplicates but it's bad practice)
785
+ // For simplicity, we create a new layer even if name matches.
786
+ // Determine Index (IX)
787
+ let maxIx = -1;
788
+ for (const row of layerSection.Row) {
789
+ const ix = parseInt(row['@_IX']);
790
+ if (!isNaN(ix) && ix > maxIx)
791
+ maxIx = ix;
792
+ }
793
+ const newIndex = maxIx + 1;
794
+ const newRow = {
795
+ '@_IX': newIndex.toString(),
796
+ Cell: [
797
+ { '@_N': 'Name', '@_V': name },
798
+ { '@_N': 'Visible', '@_V': (options.visible ?? true) ? '1' : '0' },
799
+ { '@_N': 'Lock', '@_V': (options.lock ?? false) ? '1' : '0' },
800
+ { '@_N': 'Print', '@_V': (options.print ?? true) ? '1' : '0' }
801
+ ]
802
+ };
803
+ layerSection.Row.push(newRow);
804
+ this.saveParsed(pageId, parsed);
805
+ return { name, index: newIndex };
806
+ }
807
+ async assignLayer(pageId, shapeId, layerIndex) {
808
+ const parsed = this.getParsed(pageId);
809
+ const shapes = this.getAllShapes(parsed);
810
+ const shape = shapes.find((s) => s['@_ID'] == shapeId);
811
+ if (!shape)
812
+ throw new Error(`Shape ${shapeId} not found`);
813
+ // Ensure Section array
814
+ if (!shape.Section)
815
+ shape.Section = [];
816
+ if (!Array.isArray(shape.Section))
817
+ shape.Section = [shape.Section];
818
+ // Find or Create LayerMem Section
819
+ let memSection = shape.Section.find((s) => s['@_N'] === 'LayerMem');
820
+ if (!memSection) {
821
+ memSection = { '@_N': 'LayerMem', Row: [] };
822
+ shape.Section.push(memSection);
823
+ }
824
+ // Ensure Row array
825
+ if (!memSection.Row)
826
+ memSection.Row = [];
827
+ if (!Array.isArray(memSection.Row))
828
+ memSection.Row = [memSection.Row];
829
+ // Ensure Row exists (LayerMem usually has 1 row)
830
+ if (memSection.Row.length === 0) {
831
+ memSection.Row.push({ Cell: [] });
832
+ }
833
+ const row = memSection.Row[0];
834
+ // Ensure Cell array
835
+ if (!row.Cell)
836
+ row.Cell = [];
837
+ if (!Array.isArray(row.Cell))
838
+ row.Cell = [row.Cell];
839
+ // Find LayerMember Cell
840
+ let cell = row.Cell.find((c) => c['@_N'] === 'LayerMember');
841
+ if (!cell) {
842
+ cell = { '@_N': 'LayerMember', '@_V': '' };
843
+ row.Cell.push(cell);
844
+ }
845
+ // Update Value
846
+ const currentVal = cell['@_V'] || '';
847
+ const indices = currentVal.split(';').filter((s) => s.length > 0);
848
+ const idxStr = layerIndex.toString();
849
+ if (!indices.includes(idxStr)) {
850
+ indices.push(idxStr);
851
+ // Sort optionally? Visio doesn't strictly require sorting but it's cleaner.
852
+ // Let's keep insertion order or sort numeric. Visio usually semicolon separates.
853
+ cell['@_V'] = indices.join(';');
854
+ this.saveParsed(pageId, parsed);
855
+ }
856
+ }
857
+ async updateLayerProperty(pageId, layerIndex, propName, value) {
858
+ const parsed = this.getParsed(pageId);
859
+ this.ensurePageSheet(parsed);
860
+ const pageSheet = parsed.PageContents.PageSheet;
861
+ // Find Layer Section
862
+ if (!pageSheet.Section)
863
+ return;
864
+ const sections = Array.isArray(pageSheet.Section) ? pageSheet.Section : [pageSheet.Section];
865
+ const layerSection = sections.find((s) => s['@_N'] === 'Layer');
866
+ if (!layerSection || !layerSection.Row)
867
+ return;
868
+ const rows = Array.isArray(layerSection.Row) ? layerSection.Row : [layerSection.Row];
869
+ const row = rows.find((r) => r['@_IX'] == layerIndex.toString());
870
+ if (!row)
871
+ return;
872
+ // Ensure Cell array
873
+ if (!row.Cell)
874
+ row.Cell = [];
875
+ if (!Array.isArray(row.Cell))
876
+ row.Cell = [row.Cell];
877
+ // Find or Create Cell
878
+ let cell = row.Cell.find((c) => c['@_N'] === propName);
879
+ if (!cell) {
880
+ cell = { '@_N': propName, '@_V': value };
881
+ row.Cell.push(cell);
882
+ }
883
+ else {
884
+ cell['@_V'] = value;
885
+ }
886
+ this.saveParsed(pageId, parsed);
887
+ }
888
+ }
889
+ exports.ShapeModifier = ShapeModifier;