ts-visio 1.0.2 → 1.2.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.
- package/README.md +76 -3
- package/dist/Layer.d.ts +3 -1
- package/dist/Layer.js +6 -7
- package/dist/Page.d.ts +12 -0
- package/dist/Page.js +35 -12
- package/dist/Shape.d.ts +47 -2
- package/dist/Shape.js +119 -78
- package/dist/ShapeModifier.d.ts +53 -0
- package/dist/ShapeModifier.js +451 -267
- package/dist/ShapeReader.d.ts +12 -0
- package/dist/ShapeReader.js +59 -3
- package/dist/VisioDocument.d.ts +10 -0
- package/dist/VisioDocument.js +20 -1
- package/dist/VisioPackage.d.ts +1 -0
- package/dist/VisioPackage.js +7 -0
- package/dist/core/MasterManager.js +1 -1
- package/dist/core/MediaConstants.js +11 -2
- package/dist/core/MediaManager.js +8 -16
- package/dist/core/PageManager.d.ts +8 -1
- package/dist/core/PageManager.js +75 -16
- package/dist/core/RelsManager.js +4 -11
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/shapes/ConnectorBuilder.js +8 -9
- package/dist/shapes/ContainerBuilder.js +8 -6
- package/dist/shapes/ForeignShapeBuilder.js +9 -6
- package/dist/shapes/ShapeBuilder.js +15 -7
- package/dist/types/VisioTypes.d.ts +12 -0
- package/dist/utils/StyleHelpers.d.ts +21 -4
- package/dist/utils/StyleHelpers.js +52 -27
- package/dist/utils/XmlHelper.d.ts +39 -0
- package/dist/utils/XmlHelper.js +56 -0
- package/package.json +2 -2
package/dist/ShapeModifier.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ShapeModifier = void 0;
|
|
4
|
-
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
5
4
|
const RelsManager_1 = require("./core/RelsManager");
|
|
6
5
|
const StyleHelpers_1 = require("./utils/StyleHelpers");
|
|
7
6
|
const VisioConstants_1 = require("./core/VisioConstants");
|
|
7
|
+
const VisioTypes_1 = require("./types/VisioTypes");
|
|
8
8
|
const ForeignShapeBuilder_1 = require("./shapes/ForeignShapeBuilder");
|
|
9
9
|
const ShapeBuilder_1 = require("./shapes/ShapeBuilder");
|
|
10
10
|
const ConnectorBuilder_1 = require("./shapes/ConnectorBuilder");
|
|
11
11
|
const ContainerBuilder_1 = require("./shapes/ContainerBuilder");
|
|
12
|
+
const XmlHelper_1 = require("./utils/XmlHelper");
|
|
12
13
|
class ShapeModifier {
|
|
13
14
|
// ...
|
|
14
15
|
async addContainer(pageId, props) {
|
|
15
|
-
const
|
|
16
|
-
let content = this.pkg.getFileText(pagePath);
|
|
17
|
-
const parsed = this.parser.parse(content);
|
|
18
|
-
// Ensure Shapes container...
|
|
16
|
+
const parsed = this.getParsed(pageId);
|
|
19
17
|
if (!parsed.PageContents.Shapes)
|
|
20
18
|
parsed.PageContents.Shapes = { Shape: [] };
|
|
21
19
|
let topLevelShapes = parsed.PageContents.Shapes.Shape;
|
|
@@ -23,20 +21,15 @@ class ShapeModifier {
|
|
|
23
21
|
topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
|
|
24
22
|
parsed.PageContents.Shapes.Shape = topLevelShapes;
|
|
25
23
|
}
|
|
26
|
-
|
|
24
|
+
const newId = props.id || this.getNextId(parsed);
|
|
27
25
|
const containerShape = ContainerBuilder_1.ContainerBuilder.createContainerShape(newId, props);
|
|
28
26
|
topLevelShapes.push(containerShape);
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
return newId;
|
|
32
|
-
this.pkg.updateFile(pagePath, newXml);
|
|
27
|
+
this.getShapeMap(parsed).set(newId, containerShape);
|
|
28
|
+
this.saveParsed(pageId, parsed);
|
|
33
29
|
return newId;
|
|
34
30
|
}
|
|
35
31
|
async addList(pageId, props, direction = 'vertical') {
|
|
36
|
-
const
|
|
37
|
-
let content = this.pkg.getFileText(pagePath);
|
|
38
|
-
const parsed = this.parser.parse(content);
|
|
39
|
-
// Ensure Shapes container...
|
|
32
|
+
const parsed = this.getParsed(pageId);
|
|
40
33
|
if (!parsed.PageContents.Shapes)
|
|
41
34
|
parsed.PageContents.Shapes = { Shape: [] };
|
|
42
35
|
let topLevelShapes = parsed.PageContents.Shapes.Shape;
|
|
@@ -44,57 +37,66 @@ class ShapeModifier {
|
|
|
44
37
|
topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
|
|
45
38
|
parsed.PageContents.Shapes.Shape = topLevelShapes;
|
|
46
39
|
}
|
|
47
|
-
|
|
40
|
+
const newId = props.id || this.getNextId(parsed);
|
|
48
41
|
const listShape = ContainerBuilder_1.ContainerBuilder.createContainerShape(newId, props);
|
|
49
42
|
ContainerBuilder_1.ContainerBuilder.makeList(listShape, direction);
|
|
50
43
|
topLevelShapes.push(listShape);
|
|
51
|
-
|
|
52
|
-
this.
|
|
44
|
+
this.getShapeMap(parsed).set(newId, listShape);
|
|
45
|
+
this.saveParsed(pageId, parsed);
|
|
53
46
|
return newId;
|
|
54
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Register the resolved OPC part path for a page ID.
|
|
50
|
+
* Must be called before any operation on a loaded file to ensure the
|
|
51
|
+
* correct file is targeted rather than the ID-derived fallback name.
|
|
52
|
+
*/
|
|
53
|
+
registerPage(pageId, xmlPath) {
|
|
54
|
+
this.pagePathRegistry.set(pageId, xmlPath);
|
|
55
|
+
}
|
|
55
56
|
constructor(pkg) {
|
|
56
57
|
this.pkg = pkg;
|
|
57
58
|
this.pageCache = new Map();
|
|
58
59
|
this.dirtyPages = new Set();
|
|
60
|
+
this.shapeCache = new WeakMap();
|
|
61
|
+
this.pagePathRegistry = new Map();
|
|
59
62
|
this.autoSave = true;
|
|
60
|
-
this.parser =
|
|
61
|
-
|
|
62
|
-
attributeNamePrefix: "@_"
|
|
63
|
-
});
|
|
64
|
-
this.builder = new fast_xml_parser_1.XMLBuilder({
|
|
65
|
-
ignoreAttributes: false,
|
|
66
|
-
attributeNamePrefix: "@_",
|
|
67
|
-
format: true
|
|
68
|
-
});
|
|
63
|
+
this.parser = (0, XmlHelper_1.createXmlParser)();
|
|
64
|
+
this.builder = (0, XmlHelper_1.createXmlBuilder)();
|
|
69
65
|
this.relsManager = new RelsManager_1.RelsManager(pkg);
|
|
70
66
|
}
|
|
71
67
|
getPagePath(pageId) {
|
|
72
|
-
return `visio/pages/page${pageId}.xml`;
|
|
68
|
+
return this.pagePathRegistry.get(pageId) ?? `visio/pages/page${pageId}.xml`;
|
|
73
69
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
topLevelShapes =
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
}
|
|
70
|
+
getShapeMap(parsed) {
|
|
71
|
+
if (!this.shapeCache.has(parsed)) {
|
|
72
|
+
const map = new Map();
|
|
73
|
+
let topLevelShapes = parsed.PageContents.Shapes ? parsed.PageContents.Shapes.Shape : [];
|
|
74
|
+
if (!Array.isArray(topLevelShapes)) {
|
|
75
|
+
topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
|
|
87
76
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
const gather = (shapeList) => {
|
|
78
|
+
for (const s of shapeList) {
|
|
79
|
+
map.set(s['@_ID'], s);
|
|
80
|
+
if (s.Shapes && s.Shapes.Shape) {
|
|
81
|
+
const children = Array.isArray(s.Shapes.Shape) ? s.Shapes.Shape : [s.Shapes.Shape];
|
|
82
|
+
gather(children);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
gather(topLevelShapes);
|
|
87
|
+
this.shapeCache.set(parsed, map);
|
|
88
|
+
}
|
|
89
|
+
return this.shapeCache.get(parsed);
|
|
90
|
+
}
|
|
91
|
+
getAllShapes(parsed) {
|
|
92
|
+
return Array.from(this.getShapeMap(parsed).values());
|
|
91
93
|
}
|
|
92
94
|
getNextId(parsed) {
|
|
93
95
|
// Updates PageSheet.NextShapeID to prevent ID conflicts.
|
|
94
96
|
// Calculates the next ID from existing shapes and increments the counter.
|
|
95
|
-
const
|
|
97
|
+
const shapeMap = this.getShapeMap(parsed);
|
|
96
98
|
let maxId = 0;
|
|
97
|
-
for (const s of
|
|
99
|
+
for (const s of shapeMap.values()) {
|
|
98
100
|
const id = parseInt(s['@_ID']);
|
|
99
101
|
if (!isNaN(id) && id > maxId)
|
|
100
102
|
maxId = id;
|
|
@@ -169,7 +171,7 @@ class ShapeModifier {
|
|
|
169
171
|
this.performSave(pagePath, parsed);
|
|
170
172
|
}
|
|
171
173
|
performSave(pagePath, parsed) {
|
|
172
|
-
const newXml = this.builder
|
|
174
|
+
const newXml = (0, XmlHelper_1.buildXml)(this.builder, parsed);
|
|
173
175
|
this.pkg.updateFile(pagePath, newXml);
|
|
174
176
|
this.pageCache.set(pagePath, { content: newXml, parsed });
|
|
175
177
|
}
|
|
@@ -189,7 +191,7 @@ class ShapeModifier {
|
|
|
189
191
|
parsed.PageContents.Shapes = { Shape: [] };
|
|
190
192
|
}
|
|
191
193
|
if (!Array.isArray(parsed.PageContents.Shapes.Shape)) {
|
|
192
|
-
parsed.PageContents.Shapes.Shape = [parsed.PageContents.Shapes.Shape];
|
|
194
|
+
parsed.PageContents.Shapes.Shape = parsed.PageContents.Shapes.Shape ? [parsed.PageContents.Shapes.Shape] : [];
|
|
193
195
|
}
|
|
194
196
|
const newId = this.getNextId(parsed);
|
|
195
197
|
const shapeHierarchy = ConnectorBuilder_1.ConnectorBuilder.buildShapeHierarchy(parsed);
|
|
@@ -206,6 +208,7 @@ class ShapeModifier {
|
|
|
206
208
|
const connectorShape = ConnectorBuilder_1.ConnectorBuilder.createConnectorShapeObject(newId, layout, validateArrow(beginArrow), validateArrow(endArrow));
|
|
207
209
|
const topLevelShapes = parsed.PageContents.Shapes.Shape;
|
|
208
210
|
topLevelShapes.push(connectorShape);
|
|
211
|
+
this.getShapeMap(parsed).set(newId, connectorShape);
|
|
209
212
|
ConnectorBuilder_1.ConnectorBuilder.addConnectorToConnects(parsed, newId, fromShapeId, toShapeId);
|
|
210
213
|
this.saveParsed(pageId, parsed);
|
|
211
214
|
return newId;
|
|
@@ -221,7 +224,6 @@ class ShapeModifier {
|
|
|
221
224
|
topLevelShapes = topLevelShapes ? [topLevelShapes] : [];
|
|
222
225
|
parsed.PageContents.Shapes.Shape = topLevelShapes;
|
|
223
226
|
}
|
|
224
|
-
const allShapes = this.getAllShapes(parsed);
|
|
225
227
|
// Auto-generate ID if not provided
|
|
226
228
|
let newId = props.id;
|
|
227
229
|
if (!newId) {
|
|
@@ -245,7 +247,7 @@ class ShapeModifier {
|
|
|
245
247
|
}
|
|
246
248
|
if (parentId) {
|
|
247
249
|
// Add to Parent Group
|
|
248
|
-
const parent =
|
|
250
|
+
const parent = this.getShapeMap(parsed).get(parentId);
|
|
249
251
|
if (!parent) {
|
|
250
252
|
throw new Error(`Parent shape ${parentId} not found`);
|
|
251
253
|
}
|
|
@@ -266,83 +268,120 @@ class ShapeModifier {
|
|
|
266
268
|
// Add to Page
|
|
267
269
|
topLevelShapes.push(newShape);
|
|
268
270
|
}
|
|
271
|
+
this.getShapeMap(parsed).set(newId, newShape);
|
|
269
272
|
this.saveParsed(pageId, parsed);
|
|
270
273
|
return newId;
|
|
271
274
|
}
|
|
272
|
-
async
|
|
275
|
+
async deleteShape(pageId, shapeId) {
|
|
273
276
|
const parsed = this.getParsed(pageId);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
277
|
+
// 1. Remove shape from the shape tree (handles top-level and nested groups)
|
|
278
|
+
const removed = this.removeShapeFromTree(parsed.PageContents.Shapes, shapeId);
|
|
279
|
+
if (!removed) {
|
|
280
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
281
|
+
}
|
|
282
|
+
// 2. Remove any Connect elements referencing this shape
|
|
283
|
+
if (parsed.PageContents.Connects?.Connect) {
|
|
284
|
+
let connects = parsed.PageContents.Connects.Connect;
|
|
285
|
+
if (!Array.isArray(connects))
|
|
286
|
+
connects = [connects];
|
|
287
|
+
const filtered = connects.filter((c) => c['@_FromSheet'] !== shapeId && c['@_ToSheet'] !== shapeId);
|
|
288
|
+
parsed.PageContents.Connects.Connect = filtered;
|
|
289
|
+
}
|
|
290
|
+
// 3. Remove any Relationship elements referencing this shape (container membership, etc.)
|
|
291
|
+
if (parsed.PageContents.Relationships?.Relationship) {
|
|
292
|
+
let rels = parsed.PageContents.Relationships.Relationship;
|
|
293
|
+
if (!Array.isArray(rels))
|
|
294
|
+
rels = [rels];
|
|
295
|
+
parsed.PageContents.Relationships.Relationship = rels.filter((r) => r['@_ShapeID'] !== shapeId && r['@_RelatedShapeID'] !== shapeId);
|
|
296
|
+
}
|
|
297
|
+
// 4. Invalidate the shape cache so the map is rebuilt on next access
|
|
298
|
+
this.shapeCache.delete(parsed);
|
|
299
|
+
this.saveParsed(pageId, parsed);
|
|
300
|
+
}
|
|
301
|
+
removeShapeFromTree(shapesContainer, shapeId) {
|
|
302
|
+
if (!shapesContainer?.Shape)
|
|
303
|
+
return false;
|
|
304
|
+
let shapes = shapesContainer.Shape;
|
|
305
|
+
if (!Array.isArray(shapes))
|
|
306
|
+
shapes = [shapes];
|
|
307
|
+
const idx = shapes.findIndex((s) => s['@_ID'] === shapeId);
|
|
308
|
+
if (idx !== -1) {
|
|
309
|
+
shapes.splice(idx, 1);
|
|
310
|
+
shapesContainer.Shape = shapes;
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
// Recurse into nested group children
|
|
314
|
+
for (const shape of shapes) {
|
|
315
|
+
if (shape.Shapes && this.removeShapeFromTree(shape.Shapes, shapeId)) {
|
|
316
|
+
return true;
|
|
287
317
|
}
|
|
288
|
-
};
|
|
289
|
-
const shapesData = parsed.PageContents?.Shapes?.Shape;
|
|
290
|
-
if (shapesData) {
|
|
291
|
-
const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
|
|
292
|
-
findAndUpdate(shapesArray);
|
|
293
318
|
}
|
|
294
|
-
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
async updateShapeText(pageId, shapeId, newText) {
|
|
322
|
+
const parsed = this.getParsed(pageId);
|
|
323
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
324
|
+
if (!shape) {
|
|
295
325
|
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
296
326
|
}
|
|
327
|
+
shape.Text = {
|
|
328
|
+
'#text': newText
|
|
329
|
+
};
|
|
297
330
|
this.saveParsed(pageId, parsed);
|
|
298
331
|
}
|
|
299
332
|
async updateShapeStyle(pageId, shapeId, style) {
|
|
300
333
|
const parsed = this.getParsed(pageId);
|
|
301
|
-
|
|
302
|
-
|
|
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) {
|
|
334
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
335
|
+
if (!shape) {
|
|
338
336
|
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
339
337
|
}
|
|
338
|
+
// Ensure Section array exists
|
|
339
|
+
if (!shape.Section) {
|
|
340
|
+
shape.Section = [];
|
|
341
|
+
}
|
|
342
|
+
else if (!Array.isArray(shape.Section)) {
|
|
343
|
+
shape.Section = [shape.Section];
|
|
344
|
+
}
|
|
345
|
+
// Update/Add Fill
|
|
346
|
+
if (style.fillColor) {
|
|
347
|
+
// Remove existing Fill section if any (simplified: assuming IX=0)
|
|
348
|
+
shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Fill');
|
|
349
|
+
shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
|
|
350
|
+
}
|
|
351
|
+
// Update/Add Character (Font/Text Style)
|
|
352
|
+
if (style.fontColor || style.bold !== undefined || style.fontSize !== undefined || style.fontFamily !== undefined) {
|
|
353
|
+
shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Character');
|
|
354
|
+
shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
|
|
355
|
+
bold: style.bold,
|
|
356
|
+
color: style.fontColor,
|
|
357
|
+
fontSize: style.fontSize,
|
|
358
|
+
fontFamily: style.fontFamily,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
// Update/Add Paragraph (Horizontal Alignment)
|
|
362
|
+
if (style.horzAlign !== undefined) {
|
|
363
|
+
shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Paragraph');
|
|
364
|
+
shape.Section.push((0, StyleHelpers_1.createParagraphSection)(style.horzAlign));
|
|
365
|
+
}
|
|
366
|
+
// Update/Add VerticalAlign (top-level shape Cell)
|
|
367
|
+
if (style.verticalAlign !== undefined) {
|
|
368
|
+
if (!shape.Cell)
|
|
369
|
+
shape.Cell = [];
|
|
370
|
+
if (!Array.isArray(shape.Cell))
|
|
371
|
+
shape.Cell = [shape.Cell];
|
|
372
|
+
const existingCell = shape.Cell.find((c) => c['@_N'] === 'VerticalAlign');
|
|
373
|
+
if (existingCell) {
|
|
374
|
+
existingCell['@_V'] = (0, StyleHelpers_1.vertAlignValue)(style.verticalAlign);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(style.verticalAlign) });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
340
380
|
this.saveParsed(pageId, parsed);
|
|
341
381
|
}
|
|
342
382
|
async updateShapeDimensions(pageId, shapeId, w, h) {
|
|
343
383
|
const parsed = this.getParsed(pageId);
|
|
344
|
-
const
|
|
345
|
-
const shape = shapes.find((s) => s['@_ID'] == shapeId);
|
|
384
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
346
385
|
if (!shape)
|
|
347
386
|
throw new Error(`Shape ${shapeId} not found`);
|
|
348
387
|
// Ensure Cell array
|
|
@@ -361,115 +400,175 @@ class ShapeModifier {
|
|
|
361
400
|
updateCell('Height', h.toString());
|
|
362
401
|
this.saveParsed(pageId, parsed);
|
|
363
402
|
}
|
|
364
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Set the rotation angle of a shape. Degrees are converted to radians
|
|
405
|
+
* for storage in the Angle cell (Visio's native unit).
|
|
406
|
+
*/
|
|
407
|
+
async rotateShape(pageId, shapeId, degrees) {
|
|
365
408
|
const parsed = this.getParsed(pageId);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
409
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
410
|
+
if (!shape)
|
|
411
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
412
|
+
if (!shape.Cell)
|
|
413
|
+
shape.Cell = [];
|
|
414
|
+
if (!Array.isArray(shape.Cell))
|
|
415
|
+
shape.Cell = [shape.Cell];
|
|
416
|
+
const radians = (degrees * Math.PI) / 180;
|
|
417
|
+
const existing = shape.Cell.find((c) => c['@_N'] === 'Angle');
|
|
418
|
+
if (existing)
|
|
419
|
+
existing['@_V'] = radians.toString();
|
|
420
|
+
else
|
|
421
|
+
shape.Cell.push({ '@_N': 'Angle', '@_V': radians.toString() });
|
|
422
|
+
this.saveParsed(pageId, parsed);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Set the flip state for a shape along the X or Y axis.
|
|
426
|
+
* FlipX mirrors left-to-right; FlipY mirrors top-to-bottom.
|
|
427
|
+
*/
|
|
428
|
+
setShapeFlip(pageId, shapeId, axis, enabled) {
|
|
429
|
+
const parsed = this.getParsed(pageId);
|
|
430
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
431
|
+
if (!shape)
|
|
432
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
433
|
+
if (!shape.Cell)
|
|
434
|
+
shape.Cell = [];
|
|
435
|
+
if (!Array.isArray(shape.Cell))
|
|
436
|
+
shape.Cell = [shape.Cell];
|
|
437
|
+
const cellName = axis === 'x' ? 'FlipX' : 'FlipY';
|
|
438
|
+
const value = enabled ? '1' : '0';
|
|
439
|
+
const existing = shape.Cell.find((c) => c['@_N'] === cellName);
|
|
440
|
+
if (existing)
|
|
441
|
+
existing['@_V'] = value;
|
|
442
|
+
else
|
|
443
|
+
shape.Cell.push({ '@_N': cellName, '@_V': value });
|
|
444
|
+
this.saveParsed(pageId, parsed);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Resize a shape, keeping it centred on its current PinX/PinY.
|
|
448
|
+
* Updates Width, Height, LocPinX, LocPinY, and the cached @_V on any
|
|
449
|
+
* Geometry cells whose @_F formula references Width or Height, so that
|
|
450
|
+
* non-Visio renderers see consistent values.
|
|
451
|
+
*/
|
|
452
|
+
async resizeShape(pageId, shapeId, width, height) {
|
|
453
|
+
const parsed = this.getParsed(pageId);
|
|
454
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
455
|
+
if (!shape)
|
|
456
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
457
|
+
if (!shape.Cell)
|
|
458
|
+
shape.Cell = [];
|
|
459
|
+
if (!Array.isArray(shape.Cell))
|
|
460
|
+
shape.Cell = [shape.Cell];
|
|
461
|
+
const upsert = (name, val) => {
|
|
462
|
+
const cell = shape.Cell.find((c) => c['@_N'] === name);
|
|
463
|
+
if (cell)
|
|
464
|
+
cell['@_V'] = val;
|
|
465
|
+
else
|
|
466
|
+
shape.Cell.push({ '@_N': name, '@_V': val });
|
|
467
|
+
};
|
|
468
|
+
upsert('Width', width.toString());
|
|
469
|
+
upsert('Height', height.toString());
|
|
470
|
+
upsert('LocPinX', (width / 2).toString());
|
|
471
|
+
upsert('LocPinY', (height / 2).toString());
|
|
472
|
+
// Keep cached @_V consistent for renderers that don't evaluate formulas
|
|
473
|
+
if (shape.Section) {
|
|
474
|
+
const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
|
|
475
|
+
for (const section of sections) {
|
|
476
|
+
if (section['@_N'] !== 'Geometry' || !section.Row)
|
|
477
|
+
continue;
|
|
478
|
+
const rows = Array.isArray(section.Row) ? section.Row : [section.Row];
|
|
479
|
+
for (const row of rows) {
|
|
480
|
+
if (!row.Cell)
|
|
481
|
+
continue;
|
|
482
|
+
const cells = Array.isArray(row.Cell) ? row.Cell : [row.Cell];
|
|
483
|
+
for (const cell of cells) {
|
|
484
|
+
if (cell['@_F'] === 'Width')
|
|
485
|
+
cell['@_V'] = width.toString();
|
|
486
|
+
if (cell['@_F'] === 'Height')
|
|
487
|
+
cell['@_V'] = height.toString();
|
|
377
488
|
}
|
|
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
489
|
}
|
|
392
490
|
}
|
|
393
|
-
};
|
|
394
|
-
const shapesData = parsed.PageContents?.Shapes?.Shape;
|
|
395
|
-
if (shapesData) {
|
|
396
|
-
const shapesArray = Array.isArray(shapesData) ? shapesData : [shapesData];
|
|
397
|
-
findAndUpdate(shapesArray);
|
|
398
491
|
}
|
|
399
|
-
|
|
492
|
+
this.saveParsed(pageId, parsed);
|
|
493
|
+
}
|
|
494
|
+
async updateShapePosition(pageId, shapeId, x, y) {
|
|
495
|
+
const parsed = this.getParsed(pageId);
|
|
496
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
497
|
+
if (!shape) {
|
|
400
498
|
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
401
499
|
}
|
|
500
|
+
// Ensure Cell array exists
|
|
501
|
+
if (!shape.Cell) {
|
|
502
|
+
shape.Cell = [];
|
|
503
|
+
}
|
|
504
|
+
else if (!Array.isArray(shape.Cell)) {
|
|
505
|
+
shape.Cell = [shape.Cell];
|
|
506
|
+
}
|
|
507
|
+
// Helper to update specific cell
|
|
508
|
+
const updateCell = (name, value) => {
|
|
509
|
+
const cell = shape.Cell.find((c) => c['@_N'] === name);
|
|
510
|
+
if (cell) {
|
|
511
|
+
cell['@_V'] = value;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
shape.Cell.push({ '@_N': name, '@_V': value });
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
updateCell('PinX', x.toString());
|
|
518
|
+
updateCell('PinY', y.toString());
|
|
402
519
|
this.saveParsed(pageId, parsed);
|
|
403
520
|
}
|
|
404
521
|
addPropertyDefinition(pageId, shapeId, name, type, options = {}) {
|
|
405
522
|
const parsed = this.getParsed(pageId);
|
|
406
|
-
|
|
407
|
-
|
|
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) {
|
|
523
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
524
|
+
if (!shape) {
|
|
471
525
|
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
472
526
|
}
|
|
527
|
+
// Ensure Section array exists
|
|
528
|
+
if (!shape.Section)
|
|
529
|
+
shape.Section = [];
|
|
530
|
+
if (!Array.isArray(shape.Section))
|
|
531
|
+
shape.Section = [shape.Section];
|
|
532
|
+
// Find or Create Property Section
|
|
533
|
+
let propSection = shape.Section.find((s) => s['@_N'] === 'Property');
|
|
534
|
+
if (!propSection) {
|
|
535
|
+
propSection = { '@_N': 'Property', Row: [] };
|
|
536
|
+
shape.Section.push(propSection);
|
|
537
|
+
}
|
|
538
|
+
// Ensure Row array exists
|
|
539
|
+
if (!propSection.Row)
|
|
540
|
+
propSection.Row = [];
|
|
541
|
+
if (!Array.isArray(propSection.Row))
|
|
542
|
+
propSection.Row = [propSection.Row];
|
|
543
|
+
// Check if property already exists
|
|
544
|
+
const existingRow = propSection.Row.find((r) => r['@_N'] === `Prop.${name}`);
|
|
545
|
+
if (existingRow) {
|
|
546
|
+
// Update existing Definition
|
|
547
|
+
const updateCell = (n, v) => {
|
|
548
|
+
let c = existingRow.Cell.find((x) => x['@_N'] === n);
|
|
549
|
+
if (c)
|
|
550
|
+
c['@_V'] = v;
|
|
551
|
+
else
|
|
552
|
+
existingRow.Cell.push({ '@_N': n, '@_V': v });
|
|
553
|
+
};
|
|
554
|
+
if (options.label !== undefined)
|
|
555
|
+
updateCell('Label', options.label);
|
|
556
|
+
updateCell('Type', type.toString());
|
|
557
|
+
if (options.invisible !== undefined)
|
|
558
|
+
updateCell('Invisible', options.invisible ? '1' : '0');
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
// Create New Row
|
|
562
|
+
propSection.Row.push({
|
|
563
|
+
'@_N': `Prop.${name}`,
|
|
564
|
+
Cell: [
|
|
565
|
+
{ '@_N': 'Label', '@_V': options.label || name }, // Default label to name
|
|
566
|
+
{ '@_N': 'Type', '@_V': type.toString() },
|
|
567
|
+
{ '@_N': 'Invisible', '@_V': options.invisible ? '1' : '0' },
|
|
568
|
+
{ '@_N': 'Value', '@_V': '0' } // Initialize with default
|
|
569
|
+
]
|
|
570
|
+
});
|
|
571
|
+
}
|
|
473
572
|
this.saveParsed(pageId, parsed);
|
|
474
573
|
}
|
|
475
574
|
dateToVisioString(date) {
|
|
@@ -479,69 +578,50 @@ class ShapeModifier {
|
|
|
479
578
|
}
|
|
480
579
|
setPropertyValue(pageId, shapeId, name, value) {
|
|
481
580
|
const parsed = this.getParsed(pageId);
|
|
482
|
-
|
|
483
|
-
|
|
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) {
|
|
581
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
582
|
+
if (!shape) {
|
|
537
583
|
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
538
584
|
}
|
|
585
|
+
// Ensure Section array exists
|
|
586
|
+
const sections = shape.Section ? (Array.isArray(shape.Section) ? shape.Section : [shape.Section]) : [];
|
|
587
|
+
const propSection = sections.find((s) => s['@_N'] === 'Property');
|
|
588
|
+
if (!propSection) {
|
|
589
|
+
throw new Error(`Property definition 'Prop.${name}' does not exist on shape ${shapeId}. Call addPropertyDefinition first.`);
|
|
590
|
+
}
|
|
591
|
+
const rows = propSection.Row ? (Array.isArray(propSection.Row) ? propSection.Row : [propSection.Row]) : [];
|
|
592
|
+
const row = rows.find((r) => r['@_N'] === `Prop.${name}`);
|
|
593
|
+
if (!row) {
|
|
594
|
+
throw new Error(`Property definition 'Prop.${name}' does not exist on shape ${shapeId}. Call addPropertyDefinition first.`);
|
|
595
|
+
}
|
|
596
|
+
// Determine Visio Value String
|
|
597
|
+
let visioValue = '';
|
|
598
|
+
if (value instanceof Date) {
|
|
599
|
+
visioValue = this.dateToVisioString(value);
|
|
600
|
+
}
|
|
601
|
+
else if (typeof value === 'boolean') {
|
|
602
|
+
visioValue = value ? '1' : '0'; // Should boolean be V='TRUE' or 1? Standard practice is often 1/0 or TRUE/FALSE. Cells are formulaic.
|
|
603
|
+
// However, if the Type is 3 (Boolean), Visio often expects 0/1 or TRUE/FALSE.
|
|
604
|
+
// Let's stick to '1'/'0' for safety in formulas if generic.
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
visioValue = value.toString();
|
|
608
|
+
}
|
|
609
|
+
// Update or Add Value Cell
|
|
610
|
+
// Note: If Type is String (0), V="String". If Number (2), V="123".
|
|
611
|
+
// Visio often puts string values in formulae as "String", but in XML V attribute it's raw text?
|
|
612
|
+
// Actually, for String props, V usually contains the string.
|
|
613
|
+
let valCell = row.Cell.find((c) => c['@_N'] === 'Value');
|
|
614
|
+
if (valCell) {
|
|
615
|
+
valCell['@_V'] = visioValue;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
row.Cell.push({ '@_N': 'Value', '@_V': visioValue });
|
|
619
|
+
}
|
|
539
620
|
this.saveParsed(pageId, parsed);
|
|
540
621
|
}
|
|
541
622
|
getShapeGeometry(pageId, shapeId) {
|
|
542
623
|
const parsed = this.getParsed(pageId);
|
|
543
|
-
const
|
|
544
|
-
const shape = shapes.find((s) => s['@_ID'] == shapeId);
|
|
624
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
545
625
|
if (!shape)
|
|
546
626
|
throw new Error(`Shape ${shapeId} not found`);
|
|
547
627
|
const getCellVal = (name) => {
|
|
@@ -622,8 +702,7 @@ class ShapeModifier {
|
|
|
622
702
|
async addListItem(pageId, listId, itemId) {
|
|
623
703
|
// 1. Get List Properties (Direction, Spacing)
|
|
624
704
|
const parsed = this.getParsed(pageId);
|
|
625
|
-
const
|
|
626
|
-
const listShape = shapes.find((s) => s['@_ID'] == listId);
|
|
705
|
+
const listShape = this.getShapeMap(parsed).get(listId);
|
|
627
706
|
if (!listShape)
|
|
628
707
|
throw new Error(`List ${listId} not found`);
|
|
629
708
|
const getUserVal = (name, def) => {
|
|
@@ -718,8 +797,7 @@ class ShapeModifier {
|
|
|
718
797
|
}
|
|
719
798
|
async addHyperlink(pageId, shapeId, details) {
|
|
720
799
|
const parsed = this.getParsed(pageId);
|
|
721
|
-
const
|
|
722
|
-
const shape = shapes.find((s) => s['@_ID'] == shapeId);
|
|
800
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
723
801
|
if (!shape)
|
|
724
802
|
throw new Error(`Shape ${shapeId} not found`);
|
|
725
803
|
// Ensure Section array
|
|
@@ -806,8 +884,7 @@ class ShapeModifier {
|
|
|
806
884
|
}
|
|
807
885
|
async assignLayer(pageId, shapeId, layerIndex) {
|
|
808
886
|
const parsed = this.getParsed(pageId);
|
|
809
|
-
const
|
|
810
|
-
const shape = shapes.find((s) => s['@_ID'] == shapeId);
|
|
887
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
811
888
|
if (!shape)
|
|
812
889
|
throw new Error(`Shape ${shapeId} not found`);
|
|
813
890
|
// Ensure Section array
|
|
@@ -885,5 +962,112 @@ class ShapeModifier {
|
|
|
885
962
|
}
|
|
886
963
|
this.saveParsed(pageId, parsed);
|
|
887
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Read back all custom property (shape data) entries for a shape.
|
|
967
|
+
* Returns a map of property key → ShapeData, with values coerced to
|
|
968
|
+
* the declared type (Number, Boolean, Date, or String).
|
|
969
|
+
*/
|
|
970
|
+
getShapeProperties(pageId, shapeId) {
|
|
971
|
+
const parsed = this.getParsed(pageId);
|
|
972
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
973
|
+
if (!shape)
|
|
974
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
975
|
+
const result = {};
|
|
976
|
+
if (!shape.Section)
|
|
977
|
+
return result;
|
|
978
|
+
const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
|
|
979
|
+
const propSection = sections.find((s) => s['@_N'] === 'Property');
|
|
980
|
+
if (!propSection?.Row)
|
|
981
|
+
return result;
|
|
982
|
+
const rows = Array.isArray(propSection.Row) ? propSection.Row : [propSection.Row];
|
|
983
|
+
for (const row of rows) {
|
|
984
|
+
// Row names are "Prop.KeyName" — strip the prefix to recover the user-facing key
|
|
985
|
+
const rawKey = row['@_N'] ?? '';
|
|
986
|
+
const key = rawKey.startsWith('Prop.') ? rawKey.slice(5) : rawKey;
|
|
987
|
+
if (!key)
|
|
988
|
+
continue;
|
|
989
|
+
const cells = Array.isArray(row.Cell) ? row.Cell : (row.Cell ? [row.Cell] : []);
|
|
990
|
+
const getCell = (name) => cells.find((c) => c['@_N'] === name)?.['@_V'];
|
|
991
|
+
const rawValue = getCell('Value') ?? '';
|
|
992
|
+
const type = parseInt(getCell('Type') ?? '0');
|
|
993
|
+
const label = getCell('Label');
|
|
994
|
+
const hidden = getCell('Invisible') === '1';
|
|
995
|
+
let value;
|
|
996
|
+
switch (type) {
|
|
997
|
+
case VisioTypes_1.VisioPropType.Number:
|
|
998
|
+
case VisioTypes_1.VisioPropType.Currency:
|
|
999
|
+
case VisioTypes_1.VisioPropType.Duration:
|
|
1000
|
+
value = parseFloat(rawValue) || 0;
|
|
1001
|
+
break;
|
|
1002
|
+
case VisioTypes_1.VisioPropType.Boolean:
|
|
1003
|
+
value = rawValue === '1' || rawValue.toLowerCase() === 'true';
|
|
1004
|
+
break;
|
|
1005
|
+
case VisioTypes_1.VisioPropType.Date:
|
|
1006
|
+
value = new Date(rawValue);
|
|
1007
|
+
break;
|
|
1008
|
+
default:
|
|
1009
|
+
value = rawValue;
|
|
1010
|
+
}
|
|
1011
|
+
result[key] = { value, label, hidden, type };
|
|
1012
|
+
}
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Read back all hyperlinks attached to a shape.
|
|
1017
|
+
*/
|
|
1018
|
+
getShapeHyperlinks(pageId, shapeId) {
|
|
1019
|
+
const parsed = this.getParsed(pageId);
|
|
1020
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
1021
|
+
if (!shape)
|
|
1022
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
1023
|
+
const result = [];
|
|
1024
|
+
if (!shape.Section)
|
|
1025
|
+
return result;
|
|
1026
|
+
const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
|
|
1027
|
+
const linkSection = sections.find((s) => s['@_N'] === 'Hyperlink');
|
|
1028
|
+
if (!linkSection?.Row)
|
|
1029
|
+
return result;
|
|
1030
|
+
const rows = Array.isArray(linkSection.Row) ? linkSection.Row : [linkSection.Row];
|
|
1031
|
+
for (const row of rows) {
|
|
1032
|
+
const cells = Array.isArray(row.Cell) ? row.Cell : (row.Cell ? [row.Cell] : []);
|
|
1033
|
+
const getCell = (name) => cells.find((c) => c['@_N'] === name)?.['@_V'];
|
|
1034
|
+
result.push({
|
|
1035
|
+
address: getCell('Address'),
|
|
1036
|
+
subAddress: getCell('SubAddress'),
|
|
1037
|
+
description: getCell('Description'),
|
|
1038
|
+
newWindow: getCell('NewWindow') === '1',
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Read back the layer indices a shape is assigned to.
|
|
1045
|
+
* Returns an empty array if the shape has no layer assignment.
|
|
1046
|
+
*/
|
|
1047
|
+
getShapeLayerIndices(pageId, shapeId) {
|
|
1048
|
+
const parsed = this.getParsed(pageId);
|
|
1049
|
+
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
1050
|
+
if (!shape)
|
|
1051
|
+
throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
|
|
1052
|
+
if (!shape.Section)
|
|
1053
|
+
return [];
|
|
1054
|
+
const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
|
|
1055
|
+
const memSection = sections.find((s) => s['@_N'] === 'LayerMem');
|
|
1056
|
+
if (!memSection?.Row)
|
|
1057
|
+
return [];
|
|
1058
|
+
const rows = Array.isArray(memSection.Row) ? memSection.Row : [memSection.Row];
|
|
1059
|
+
const row = rows[0];
|
|
1060
|
+
if (!row)
|
|
1061
|
+
return [];
|
|
1062
|
+
const cells = Array.isArray(row.Cell) ? row.Cell : (row.Cell ? [row.Cell] : []);
|
|
1063
|
+
const memberCell = cells.find((c) => c['@_N'] === 'LayerMember');
|
|
1064
|
+
if (!memberCell?.['@_V'])
|
|
1065
|
+
return [];
|
|
1066
|
+
return memberCell['@_V']
|
|
1067
|
+
.split(';')
|
|
1068
|
+
.filter((s) => s.length > 0)
|
|
1069
|
+
.map((s) => parseInt(s))
|
|
1070
|
+
.filter((n) => !isNaN(n));
|
|
1071
|
+
}
|
|
888
1072
|
}
|
|
889
1073
|
exports.ShapeModifier = ShapeModifier;
|