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.
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/Layer.d.ts +12 -0
- package/dist/Layer.js +35 -0
- package/dist/Page.d.ts +30 -0
- package/dist/Page.js +169 -0
- package/dist/PageManager.d.ts +8 -0
- package/dist/PageManager.js +35 -0
- package/dist/SchemaDiagram.d.ts +22 -0
- package/dist/SchemaDiagram.js +36 -0
- package/dist/Shape.d.ts +68 -0
- package/dist/Shape.js +203 -0
- package/dist/ShapeModifier.d.ts +66 -0
- package/dist/ShapeModifier.js +889 -0
- package/dist/ShapeReader.d.ts +9 -0
- package/dist/ShapeReader.js +51 -0
- package/dist/VisioDocument.d.ts +21 -0
- package/dist/VisioDocument.js +119 -0
- package/dist/VisioPackage.d.ts +10 -0
- package/dist/VisioPackage.js +112 -0
- package/dist/core/MasterManager.d.ts +15 -0
- package/dist/core/MasterManager.js +43 -0
- package/dist/core/MediaConstants.d.ts +5 -0
- package/dist/core/MediaConstants.js +16 -0
- package/dist/core/MediaManager.d.ts +13 -0
- package/dist/core/MediaManager.js +88 -0
- package/dist/core/PageManager.d.ts +28 -0
- package/dist/core/PageManager.js +244 -0
- package/dist/core/RelsManager.d.ts +11 -0
- package/dist/core/RelsManager.js +81 -0
- package/dist/core/VisioConstants.d.ts +38 -0
- package/dist/core/VisioConstants.js +41 -0
- package/dist/core/VisioValidator.d.ts +27 -0
- package/dist/core/VisioValidator.js +362 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +32 -0
- package/dist/shapes/ConnectorBuilder.d.ts +37 -0
- package/dist/shapes/ConnectorBuilder.js +173 -0
- package/dist/shapes/ContainerBuilder.d.ts +6 -0
- package/dist/shapes/ContainerBuilder.js +103 -0
- package/dist/shapes/ForeignShapeBuilder.d.ts +4 -0
- package/dist/shapes/ForeignShapeBuilder.js +47 -0
- package/dist/shapes/ShapeBuilder.d.ts +4 -0
- package/dist/shapes/ShapeBuilder.js +68 -0
- package/dist/templates/MinimalVsdx.d.ts +10 -0
- package/dist/templates/MinimalVsdx.js +66 -0
- package/dist/types/VisioTypes.d.ts +85 -0
- package/dist/types/VisioTypes.js +14 -0
- package/dist/utils/StubHelpers.d.ts +7 -0
- package/dist/utils/StubHelpers.js +16 -0
- package/dist/utils/StyleHelpers.d.ts +30 -0
- package/dist/utils/StyleHelpers.js +95 -0
- package/dist/utils/VisioParsers.d.ts +6 -0
- package/dist/utils/VisioParsers.js +45 -0
- 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;
|