ts-visio 1.1.0 → 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/Page.d.ts +10 -0
- package/dist/Page.js +22 -1
- package/dist/Shape.d.ts +43 -0
- package/dist/Shape.js +75 -0
- package/dist/ShapeModifier.d.ts +44 -0
- package/dist/ShapeModifier.js +268 -3
- package/dist/ShapeReader.d.ts +12 -0
- package/dist/ShapeReader.js +59 -3
- package/dist/VisioDocument.d.ts +10 -0
- package/dist/VisioDocument.js +15 -0
- package/dist/VisioPackage.d.ts +1 -0
- package/dist/VisioPackage.js +7 -0
- package/dist/core/PageManager.d.ts +7 -0
- package/dist/core/PageManager.js +63 -0
- package/dist/index.d.ts +1 -0
- package/dist/shapes/ForeignShapeBuilder.js +2 -2
- package/dist/shapes/ShapeBuilder.js +12 -4
- package/dist/types/VisioTypes.d.ts +8 -0
- package/dist/utils/StyleHelpers.d.ts +21 -0
- package/dist/utils/StyleHelpers.js +51 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Built using specific schema-level abstractions to handle the complex internal st
|
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
- **Read VSDX**: Open and parse `.vsdx` files (
|
|
15
|
+
- **Read VSDX**: Open and parse `.vsdx` files (zipped XML).
|
|
16
16
|
- **Strict Typing**: Interact with `VisioPage`, `VisioShape`, and `VisioConnect` objects.
|
|
17
17
|
- **ShapeSheet Access**: Read `Cells`, `Rows`, and `Sections` directly.
|
|
18
18
|
- **Connections**: Analyze connectivity between shapes.
|
|
@@ -20,6 +20,11 @@ Built using specific schema-level abstractions to handle the complex internal st
|
|
|
20
20
|
- **Modify Content**: Update text content of shapes.
|
|
21
21
|
- **Create Shapes**: Add new rectangular shapes with text to pages.
|
|
22
22
|
- **Connect Shapes**: Create dynamic connectors between shapes.
|
|
23
|
+
- **Text Styling**: Font size, font family, bold, color, horizontal/vertical alignment.
|
|
24
|
+
- **Shape Transformations**: Rotate, flip (X/Y), and resize shapes via a fluent API.
|
|
25
|
+
- **Deletion**: Remove shapes and pages cleanly (including orphaned connectors and relationships).
|
|
26
|
+
- **Lookup API**: Find shapes by ID, predicate, or look up pages by name.
|
|
27
|
+
- **Read-Back API**: Read custom properties, hyperlinks, and layer assignments from existing shapes.
|
|
23
28
|
|
|
24
29
|
## Installation
|
|
25
30
|
|
|
@@ -63,9 +68,13 @@ const shape = await page.addShape({
|
|
|
63
68
|
y: 1,
|
|
64
69
|
width: 3,
|
|
65
70
|
height: 1,
|
|
66
|
-
fillColor: "#ff0000",
|
|
71
|
+
fillColor: "#ff0000", // Hex fill color
|
|
67
72
|
fontColor: "#ffffff",
|
|
68
|
-
bold: true
|
|
73
|
+
bold: true,
|
|
74
|
+
fontSize: 14, // Points
|
|
75
|
+
fontFamily: "Segoe UI",
|
|
76
|
+
horzAlign: "center", // "left" | "center" | "right" | "justify"
|
|
77
|
+
verticalAlign: "middle" // "top" | "middle" | "bottom"
|
|
69
78
|
});
|
|
70
79
|
|
|
71
80
|
// Modify text
|
|
@@ -345,6 +354,70 @@ await lane1.addMember(startShape);
|
|
|
345
354
|
await lane2.addMember(serverShape);
|
|
346
355
|
```
|
|
347
356
|
|
|
357
|
+
#### 19. Shape Transformations
|
|
358
|
+
Rotate, flip, and resize shapes using a fluent API.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const shape = await page.addShape({ text: "Widget", x: 3, y: 3, width: 2, height: 1 });
|
|
362
|
+
|
|
363
|
+
// Rotate 45 degrees (clockwise)
|
|
364
|
+
await shape.rotate(45);
|
|
365
|
+
console.log(shape.angle); // 45
|
|
366
|
+
|
|
367
|
+
// Mirror horizontally or vertically
|
|
368
|
+
await shape.flipX();
|
|
369
|
+
await shape.flipY(false); // un-flip
|
|
370
|
+
|
|
371
|
+
// Resize (keeps the pin point centred)
|
|
372
|
+
await shape.resize(4, 2);
|
|
373
|
+
console.log(shape.width, shape.height); // 4, 2
|
|
374
|
+
|
|
375
|
+
// Chainable
|
|
376
|
+
await shape.rotate(90).then(s => s.resize(3, 1));
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### 20. Deleting Shapes and Pages
|
|
380
|
+
Remove shapes or entire pages. Orphaned connectors and relationships are cleaned up automatically.
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// Delete a shape
|
|
384
|
+
await shape.delete();
|
|
385
|
+
|
|
386
|
+
// Delete a page (removes page file, rels, and all back-page references)
|
|
387
|
+
await doc.deletePage(page2);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### 21. Lookup API
|
|
391
|
+
Find shapes and pages without iterating manually.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Find a shape by its numeric ID (searches nested groups too)
|
|
395
|
+
const target = await page.getShapeById("42");
|
|
396
|
+
|
|
397
|
+
// Find all shapes matching a predicate
|
|
398
|
+
const servers = await page.findShapes(s => s.text.startsWith("Server"));
|
|
399
|
+
|
|
400
|
+
// Look up a page by name (exact, case-sensitive)
|
|
401
|
+
const detailPage = doc.getPage("Architecture Diagram");
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
#### 22. Reading Shape Data Back
|
|
405
|
+
Retrieve custom properties, hyperlinks, and layer assignments that were previously written.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Custom properties (shape data)
|
|
409
|
+
const props = shape.getProperties();
|
|
410
|
+
console.log(props["IP"].value); // "192.168.1.10"
|
|
411
|
+
console.log(props["Port"].type); // VisioPropType.Number
|
|
412
|
+
|
|
413
|
+
// Hyperlinks
|
|
414
|
+
const links = shape.getHyperlinks();
|
|
415
|
+
// [ { address: "https://example.com", description: "Docs", newWindow: false } ]
|
|
416
|
+
|
|
417
|
+
// Layer indices
|
|
418
|
+
const indices = shape.getLayerIndices(); // e.g. [0, 2]
|
|
419
|
+
```
|
|
420
|
+
|
|
348
421
|
## Examples
|
|
349
422
|
|
|
350
423
|
Check out the [examples](./examples) directory for complete scripts.
|
package/dist/Page.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export declare class Page {
|
|
|
18
18
|
get id(): string;
|
|
19
19
|
get name(): string;
|
|
20
20
|
getShapes(): Shape[];
|
|
21
|
+
/**
|
|
22
|
+
* Find a shape by its ID anywhere on the page, including shapes nested inside groups.
|
|
23
|
+
* Returns undefined if no shape with that ID exists.
|
|
24
|
+
*/
|
|
25
|
+
getShapeById(id: string): Shape | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Return all shapes on the page (including nested group children) that satisfy
|
|
28
|
+
* the predicate. Equivalent to getAllShapes().filter(predicate).
|
|
29
|
+
*/
|
|
30
|
+
findShapes(predicate: (shape: Shape) => boolean): Shape[];
|
|
21
31
|
addShape(props: NewShapeProps, parentId?: string): Promise<Shape>;
|
|
22
32
|
connectShapes(fromShape: Shape, toShape: Shape, beginArrow?: string, endArrow?: string): Promise<void>;
|
|
23
33
|
addImage(data: Buffer, name: string, x: number, y: number, width: number, height: number): Promise<Shape>;
|
package/dist/Page.js
CHANGED
|
@@ -33,11 +33,32 @@ class Page {
|
|
|
33
33
|
return internalShapes.map(s => new Shape_1.Shape(s, this.id, this.pkg, this.modifier));
|
|
34
34
|
}
|
|
35
35
|
catch (e) {
|
|
36
|
-
// If page file doesn't exist or is empty, return empty array
|
|
37
36
|
console.warn(`Could not read shapes for page ${this.id}:`, e);
|
|
38
37
|
return [];
|
|
39
38
|
}
|
|
40
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Find a shape by its ID anywhere on the page, including shapes nested inside groups.
|
|
42
|
+
* Returns undefined if no shape with that ID exists.
|
|
43
|
+
*/
|
|
44
|
+
getShapeById(id) {
|
|
45
|
+
const reader = new ShapeReader_1.ShapeReader(this.pkg);
|
|
46
|
+
const internal = reader.readShapeById(this.pagePath, id);
|
|
47
|
+
if (!internal)
|
|
48
|
+
return undefined;
|
|
49
|
+
return new Shape_1.Shape(internal, this.id, this.pkg, this.modifier);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Return all shapes on the page (including nested group children) that satisfy
|
|
53
|
+
* the predicate. Equivalent to getAllShapes().filter(predicate).
|
|
54
|
+
*/
|
|
55
|
+
findShapes(predicate) {
|
|
56
|
+
const reader = new ShapeReader_1.ShapeReader(this.pkg);
|
|
57
|
+
const all = reader.readAllShapes(this.pagePath);
|
|
58
|
+
return all
|
|
59
|
+
.map(s => new Shape_1.Shape(s, this.id, this.pkg, this.modifier))
|
|
60
|
+
.filter(predicate);
|
|
61
|
+
}
|
|
41
62
|
async addShape(props, parentId) {
|
|
42
63
|
const newId = await this.modifier.addShape(this.id, props, parentId);
|
|
43
64
|
// Return a fresh Shape object representing the new shape
|
package/dist/Shape.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface ShapeData {
|
|
|
9
9
|
hidden?: boolean;
|
|
10
10
|
type?: VisioPropType;
|
|
11
11
|
}
|
|
12
|
+
export interface ShapeHyperlink {
|
|
13
|
+
address?: string;
|
|
14
|
+
subAddress?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
newWindow: boolean;
|
|
17
|
+
}
|
|
12
18
|
export declare class Shape {
|
|
13
19
|
private internalShape;
|
|
14
20
|
private pageId;
|
|
@@ -23,6 +29,7 @@ export declare class Shape {
|
|
|
23
29
|
get height(): number;
|
|
24
30
|
get x(): number;
|
|
25
31
|
get y(): number;
|
|
32
|
+
delete(): Promise<void>;
|
|
26
33
|
connectTo(targetShape: Shape, beginArrow?: string, endArrow?: string): Promise<this>;
|
|
27
34
|
setStyle(style: ShapeStyle): Promise<this>;
|
|
28
35
|
placeRightOf(targetShape: Shape, options?: {
|
|
@@ -37,6 +44,41 @@ export declare class Shape {
|
|
|
37
44
|
}): this;
|
|
38
45
|
setPropertyValue(name: string, value: string | number | boolean | Date): this;
|
|
39
46
|
addData(key: string, data: ShapeData): this;
|
|
47
|
+
/**
|
|
48
|
+
* Read back all custom property (shape data) entries written to this shape.
|
|
49
|
+
* Returns a map of property key → ShapeData. Values are coerced to the
|
|
50
|
+
* declared Visio type (Number, Boolean, Date, or String).
|
|
51
|
+
*/
|
|
52
|
+
getProperties(): Record<string, ShapeData>;
|
|
53
|
+
/**
|
|
54
|
+
* Read back all hyperlinks attached to this shape.
|
|
55
|
+
*/
|
|
56
|
+
getHyperlinks(): ShapeHyperlink[];
|
|
57
|
+
/**
|
|
58
|
+
* Read back the layer indices this shape is assigned to.
|
|
59
|
+
* Returns an empty array if the shape has no layer assignment.
|
|
60
|
+
*/
|
|
61
|
+
getLayerIndices(): number[];
|
|
62
|
+
/** Current rotation angle in degrees (0 if no Angle cell is set). */
|
|
63
|
+
get angle(): number;
|
|
64
|
+
/**
|
|
65
|
+
* Rotate the shape to an absolute angle (degrees, clockwise).
|
|
66
|
+
* Replaces any existing rotation.
|
|
67
|
+
*/
|
|
68
|
+
rotate(degrees: number): Promise<this>;
|
|
69
|
+
/**
|
|
70
|
+
* Resize the shape to the given width and height (in inches).
|
|
71
|
+
* Updates LocPinX/LocPinY to keep the centre-pin at width/2, height/2.
|
|
72
|
+
*/
|
|
73
|
+
resize(width: number, height: number): Promise<this>;
|
|
74
|
+
/**
|
|
75
|
+
* Flip the shape horizontally. Pass `false` to un-flip.
|
|
76
|
+
*/
|
|
77
|
+
flipX(enabled?: boolean): Promise<this>;
|
|
78
|
+
/**
|
|
79
|
+
* Flip the shape vertically. Pass `false` to un-flip.
|
|
80
|
+
*/
|
|
81
|
+
flipY(enabled?: boolean): Promise<this>;
|
|
40
82
|
addMember(memberShape: Shape): Promise<this>;
|
|
41
83
|
addListItem(item: Shape): Promise<this>;
|
|
42
84
|
resizeToFit(padding?: number): Promise<this>;
|
|
@@ -67,4 +109,5 @@ export declare class Shape {
|
|
|
67
109
|
*/
|
|
68
110
|
addToLayer(layer: Layer | number): Promise<this>;
|
|
69
111
|
private setLocalCoord;
|
|
112
|
+
private setLocalRawCell;
|
|
70
113
|
}
|
package/dist/Shape.js
CHANGED
|
@@ -39,6 +39,9 @@ class Shape {
|
|
|
39
39
|
get y() {
|
|
40
40
|
return this.internalShape.Cells['PinY'] ? Number(this.internalShape.Cells['PinY'].V) : 0;
|
|
41
41
|
}
|
|
42
|
+
async delete() {
|
|
43
|
+
await this.modifier.deleteShape(this.pageId, this.id);
|
|
44
|
+
}
|
|
42
45
|
async connectTo(targetShape, beginArrow, endArrow) {
|
|
43
46
|
await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow);
|
|
44
47
|
return this;
|
|
@@ -96,6 +99,72 @@ class Shape {
|
|
|
96
99
|
this.setPropertyValue(key, data.value);
|
|
97
100
|
return this;
|
|
98
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Read back all custom property (shape data) entries written to this shape.
|
|
104
|
+
* Returns a map of property key → ShapeData. Values are coerced to the
|
|
105
|
+
* declared Visio type (Number, Boolean, Date, or String).
|
|
106
|
+
*/
|
|
107
|
+
getProperties() {
|
|
108
|
+
return this.modifier.getShapeProperties(this.pageId, this.id);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read back all hyperlinks attached to this shape.
|
|
112
|
+
*/
|
|
113
|
+
getHyperlinks() {
|
|
114
|
+
return this.modifier.getShapeHyperlinks(this.pageId, this.id);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read back the layer indices this shape is assigned to.
|
|
118
|
+
* Returns an empty array if the shape has no layer assignment.
|
|
119
|
+
*/
|
|
120
|
+
getLayerIndices() {
|
|
121
|
+
return this.modifier.getShapeLayerIndices(this.pageId, this.id);
|
|
122
|
+
}
|
|
123
|
+
/** Current rotation angle in degrees (0 if no Angle cell is set). */
|
|
124
|
+
get angle() {
|
|
125
|
+
const cell = this.internalShape.Cells['Angle'];
|
|
126
|
+
return cell ? (parseFloat(cell.V) * 180) / Math.PI : 0;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Rotate the shape to an absolute angle (degrees, clockwise).
|
|
130
|
+
* Replaces any existing rotation.
|
|
131
|
+
*/
|
|
132
|
+
async rotate(degrees) {
|
|
133
|
+
await this.modifier.rotateShape(this.pageId, this.id, degrees);
|
|
134
|
+
const radians = (degrees * Math.PI) / 180;
|
|
135
|
+
this.setLocalCoord('Angle', radians);
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resize the shape to the given width and height (in inches).
|
|
140
|
+
* Updates LocPinX/LocPinY to keep the centre-pin at width/2, height/2.
|
|
141
|
+
*/
|
|
142
|
+
async resize(width, height) {
|
|
143
|
+
if (width <= 0 || height <= 0)
|
|
144
|
+
throw new Error('Shape dimensions must be positive');
|
|
145
|
+
await this.modifier.resizeShape(this.pageId, this.id, width, height);
|
|
146
|
+
this.setLocalCoord('Width', width);
|
|
147
|
+
this.setLocalCoord('Height', height);
|
|
148
|
+
this.setLocalCoord('LocPinX', width / 2);
|
|
149
|
+
this.setLocalCoord('LocPinY', height / 2);
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Flip the shape horizontally. Pass `false` to un-flip.
|
|
154
|
+
*/
|
|
155
|
+
async flipX(enabled = true) {
|
|
156
|
+
this.modifier.setShapeFlip(this.pageId, this.id, 'x', enabled);
|
|
157
|
+
this.setLocalRawCell('FlipX', enabled ? '1' : '0');
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Flip the shape vertically. Pass `false` to un-flip.
|
|
162
|
+
*/
|
|
163
|
+
async flipY(enabled = true) {
|
|
164
|
+
this.modifier.setShapeFlip(this.pageId, this.id, 'y', enabled);
|
|
165
|
+
this.setLocalRawCell('FlipY', enabled ? '1' : '0');
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
99
168
|
async addMember(memberShape) {
|
|
100
169
|
await this.modifier.addRelationship(this.pageId, this.id, memberShape.id, 'Container');
|
|
101
170
|
return this;
|
|
@@ -165,5 +234,11 @@ class Shape {
|
|
|
165
234
|
else
|
|
166
235
|
this.internalShape.Cells[name] = { V: v, N: name };
|
|
167
236
|
}
|
|
237
|
+
setLocalRawCell(name, value) {
|
|
238
|
+
if (this.internalShape.Cells[name])
|
|
239
|
+
this.internalShape.Cells[name].V = value;
|
|
240
|
+
else
|
|
241
|
+
this.internalShape.Cells[name] = { V: value, N: name };
|
|
242
|
+
}
|
|
168
243
|
}
|
|
169
244
|
exports.Shape = Shape;
|
package/dist/ShapeModifier.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { VisioPackage } from './VisioPackage';
|
|
2
|
+
import { HorzAlign, VertAlign } from './utils/StyleHelpers';
|
|
2
3
|
import { NewShapeProps } from './types/VisioTypes';
|
|
4
|
+
import type { ShapeData, ShapeHyperlink } from './Shape';
|
|
3
5
|
export declare class ShapeModifier {
|
|
4
6
|
private pkg;
|
|
5
7
|
addContainer(pageId: string, props: NewShapeProps): Promise<string>;
|
|
@@ -31,9 +33,28 @@ export declare class ShapeModifier {
|
|
|
31
33
|
flush(): void;
|
|
32
34
|
addConnector(pageId: string, fromShapeId: string, toShapeId: string, beginArrow?: string, endArrow?: string): Promise<string>;
|
|
33
35
|
addShape(pageId: string, props: NewShapeProps, parentId?: string): Promise<string>;
|
|
36
|
+
deleteShape(pageId: string, shapeId: string): Promise<void>;
|
|
37
|
+
private removeShapeFromTree;
|
|
34
38
|
updateShapeText(pageId: string, shapeId: string, newText: string): Promise<void>;
|
|
35
39
|
updateShapeStyle(pageId: string, shapeId: string, style: ShapeStyle): Promise<void>;
|
|
36
40
|
updateShapeDimensions(pageId: string, shapeId: string, w: number, h: number): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Set the rotation angle of a shape. Degrees are converted to radians
|
|
43
|
+
* for storage in the Angle cell (Visio's native unit).
|
|
44
|
+
*/
|
|
45
|
+
rotateShape(pageId: string, shapeId: string, degrees: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Set the flip state for a shape along the X or Y axis.
|
|
48
|
+
* FlipX mirrors left-to-right; FlipY mirrors top-to-bottom.
|
|
49
|
+
*/
|
|
50
|
+
setShapeFlip(pageId: string, shapeId: string, axis: 'x' | 'y', enabled: boolean): void;
|
|
51
|
+
/**
|
|
52
|
+
* Resize a shape, keeping it centred on its current PinX/PinY.
|
|
53
|
+
* Updates Width, Height, LocPinX, LocPinY, and the cached @_V on any
|
|
54
|
+
* Geometry cells whose @_F formula references Width or Height, so that
|
|
55
|
+
* non-Visio renderers see consistent values.
|
|
56
|
+
*/
|
|
57
|
+
resizeShape(pageId: string, shapeId: string, width: number, height: number): Promise<void>;
|
|
37
58
|
updateShapePosition(pageId: string, shapeId: string, x: number, y: number): Promise<void>;
|
|
38
59
|
addPropertyDefinition(pageId: string, shapeId: string, name: string, type: number, options?: {
|
|
39
60
|
label?: string;
|
|
@@ -67,9 +88,32 @@ export declare class ShapeModifier {
|
|
|
67
88
|
}>;
|
|
68
89
|
assignLayer(pageId: string, shapeId: string, layerIndex: number): Promise<void>;
|
|
69
90
|
updateLayerProperty(pageId: string, layerIndex: number, propName: string, value: string): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Read back all custom property (shape data) entries for a shape.
|
|
93
|
+
* Returns a map of property key → ShapeData, with values coerced to
|
|
94
|
+
* the declared type (Number, Boolean, Date, or String).
|
|
95
|
+
*/
|
|
96
|
+
getShapeProperties(pageId: string, shapeId: string): Record<string, ShapeData>;
|
|
97
|
+
/**
|
|
98
|
+
* Read back all hyperlinks attached to a shape.
|
|
99
|
+
*/
|
|
100
|
+
getShapeHyperlinks(pageId: string, shapeId: string): ShapeHyperlink[];
|
|
101
|
+
/**
|
|
102
|
+
* Read back the layer indices a shape is assigned to.
|
|
103
|
+
* Returns an empty array if the shape has no layer assignment.
|
|
104
|
+
*/
|
|
105
|
+
getShapeLayerIndices(pageId: string, shapeId: string): number[];
|
|
70
106
|
}
|
|
71
107
|
export interface ShapeStyle {
|
|
72
108
|
fillColor?: string;
|
|
73
109
|
fontColor?: string;
|
|
74
110
|
bold?: boolean;
|
|
111
|
+
/** Font size in points (e.g. 14 for 14pt). */
|
|
112
|
+
fontSize?: number;
|
|
113
|
+
/** Font family name (e.g. "Arial"). */
|
|
114
|
+
fontFamily?: string;
|
|
115
|
+
/** Horizontal text alignment. */
|
|
116
|
+
horzAlign?: HorzAlign;
|
|
117
|
+
/** Vertical text alignment. */
|
|
118
|
+
verticalAlign?: VertAlign;
|
|
75
119
|
}
|
package/dist/ShapeModifier.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.ShapeModifier = void 0;
|
|
|
4
4
|
const RelsManager_1 = require("./core/RelsManager");
|
|
5
5
|
const StyleHelpers_1 = require("./utils/StyleHelpers");
|
|
6
6
|
const VisioConstants_1 = require("./core/VisioConstants");
|
|
7
|
+
const VisioTypes_1 = require("./types/VisioTypes");
|
|
7
8
|
const ForeignShapeBuilder_1 = require("./shapes/ForeignShapeBuilder");
|
|
8
9
|
const ShapeBuilder_1 = require("./shapes/ShapeBuilder");
|
|
9
10
|
const ConnectorBuilder_1 = require("./shapes/ConnectorBuilder");
|
|
@@ -271,6 +272,52 @@ class ShapeModifier {
|
|
|
271
272
|
this.saveParsed(pageId, parsed);
|
|
272
273
|
return newId;
|
|
273
274
|
}
|
|
275
|
+
async deleteShape(pageId, shapeId) {
|
|
276
|
+
const parsed = this.getParsed(pageId);
|
|
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;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
274
321
|
async updateShapeText(pageId, shapeId, newText) {
|
|
275
322
|
const parsed = this.getParsed(pageId);
|
|
276
323
|
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
@@ -302,14 +349,34 @@ class ShapeModifier {
|
|
|
302
349
|
shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
|
|
303
350
|
}
|
|
304
351
|
// Update/Add Character (Font/Text Style)
|
|
305
|
-
if (style.fontColor || style.bold !== undefined) {
|
|
306
|
-
// Remove existing Character section if any
|
|
352
|
+
if (style.fontColor || style.bold !== undefined || style.fontSize !== undefined || style.fontFamily !== undefined) {
|
|
307
353
|
shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Character');
|
|
308
354
|
shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
|
|
309
355
|
bold: style.bold,
|
|
310
|
-
color: style.fontColor
|
|
356
|
+
color: style.fontColor,
|
|
357
|
+
fontSize: style.fontSize,
|
|
358
|
+
fontFamily: style.fontFamily,
|
|
311
359
|
}));
|
|
312
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
|
+
}
|
|
313
380
|
this.saveParsed(pageId, parsed);
|
|
314
381
|
}
|
|
315
382
|
async updateShapeDimensions(pageId, shapeId, w, h) {
|
|
@@ -333,6 +400,97 @@ class ShapeModifier {
|
|
|
333
400
|
updateCell('Height', h.toString());
|
|
334
401
|
this.saveParsed(pageId, parsed);
|
|
335
402
|
}
|
|
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) {
|
|
408
|
+
const parsed = this.getParsed(pageId);
|
|
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();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
this.saveParsed(pageId, parsed);
|
|
493
|
+
}
|
|
336
494
|
async updateShapePosition(pageId, shapeId, x, y) {
|
|
337
495
|
const parsed = this.getParsed(pageId);
|
|
338
496
|
const shape = this.getShapeMap(parsed).get(shapeId);
|
|
@@ -804,5 +962,112 @@ class ShapeModifier {
|
|
|
804
962
|
}
|
|
805
963
|
this.saveParsed(pageId, parsed);
|
|
806
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
|
+
}
|
|
807
1072
|
}
|
|
808
1073
|
exports.ShapeModifier = ShapeModifier;
|
package/dist/ShapeReader.d.ts
CHANGED
|
@@ -5,5 +5,17 @@ export declare class ShapeReader {
|
|
|
5
5
|
private parser;
|
|
6
6
|
constructor(pkg: VisioPackage);
|
|
7
7
|
readShapes(path: string): VisioShape[];
|
|
8
|
+
/**
|
|
9
|
+
* Returns every shape on the page flattened into a single array,
|
|
10
|
+
* including shapes nested inside groups at any depth.
|
|
11
|
+
*/
|
|
12
|
+
readAllShapes(path: string): VisioShape[];
|
|
13
|
+
/**
|
|
14
|
+
* Find a single shape by ID anywhere in the page tree (including nested groups).
|
|
15
|
+
* Returns undefined if not found.
|
|
16
|
+
*/
|
|
17
|
+
readShapeById(path: string, shapeId: string): VisioShape | undefined;
|
|
18
|
+
private gatherShapes;
|
|
19
|
+
private findShapeById;
|
|
8
20
|
private parseShape;
|
|
9
21
|
}
|
package/dist/ShapeReader.js
CHANGED
|
@@ -20,12 +20,68 @@ class ShapeReader {
|
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
22
|
const parsed = this.parser.parse(content);
|
|
23
|
-
// Supports PageContents -> Shapes -> Shape or just Shapes -> Shape depending on structure
|
|
24
23
|
const shapesData = parsed.PageContents?.Shapes?.Shape;
|
|
25
24
|
if (!shapesData)
|
|
26
25
|
return [];
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
return (0, VisioParsers_1.asArray)(shapesData).map(s => this.parseShape(s));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns every shape on the page flattened into a single array,
|
|
30
|
+
* including shapes nested inside groups at any depth.
|
|
31
|
+
*/
|
|
32
|
+
readAllShapes(path) {
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = this.pkg.getFileText(path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const parsed = this.parser.parse(content);
|
|
41
|
+
const shapesData = parsed.PageContents?.Shapes?.Shape;
|
|
42
|
+
if (!shapesData)
|
|
43
|
+
return [];
|
|
44
|
+
const result = [];
|
|
45
|
+
this.gatherShapes((0, VisioParsers_1.asArray)(shapesData), result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Find a single shape by ID anywhere in the page tree (including nested groups).
|
|
50
|
+
* Returns undefined if not found.
|
|
51
|
+
*/
|
|
52
|
+
readShapeById(path, shapeId) {
|
|
53
|
+
let content;
|
|
54
|
+
try {
|
|
55
|
+
content = this.pkg.getFileText(path);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const parsed = this.parser.parse(content);
|
|
61
|
+
const shapesData = parsed.PageContents?.Shapes?.Shape;
|
|
62
|
+
if (!shapesData)
|
|
63
|
+
return undefined;
|
|
64
|
+
return this.findShapeById((0, VisioParsers_1.asArray)(shapesData), shapeId);
|
|
65
|
+
}
|
|
66
|
+
gatherShapes(rawShapes, result) {
|
|
67
|
+
for (const s of rawShapes) {
|
|
68
|
+
result.push(this.parseShape(s));
|
|
69
|
+
if (s.Shapes?.Shape) {
|
|
70
|
+
this.gatherShapes((0, VisioParsers_1.asArray)(s.Shapes.Shape), result);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
findShapeById(rawShapes, shapeId) {
|
|
75
|
+
for (const s of rawShapes) {
|
|
76
|
+
if (s['@_ID'] === shapeId)
|
|
77
|
+
return this.parseShape(s);
|
|
78
|
+
if (s.Shapes?.Shape) {
|
|
79
|
+
const found = this.findShapeById((0, VisioParsers_1.asArray)(s.Shapes.Shape), shapeId);
|
|
80
|
+
if (found)
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
29
85
|
}
|
|
30
86
|
parseShape(s) {
|
|
31
87
|
const shape = {
|
package/dist/VisioDocument.d.ts
CHANGED
|
@@ -17,5 +17,15 @@ export declare class VisioDocument {
|
|
|
17
17
|
* Set a background page for a foreground page
|
|
18
18
|
*/
|
|
19
19
|
setBackgroundPage(foregroundPage: Page, backgroundPage: Page): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Find a page by name. Returns undefined if no page with that name exists.
|
|
22
|
+
*/
|
|
23
|
+
getPage(name: string): Page | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Delete a page from the document.
|
|
26
|
+
* Removes the page XML, its relationships, the Content Types entry,
|
|
27
|
+
* and any BackPage references from other pages.
|
|
28
|
+
*/
|
|
29
|
+
deletePage(page: Page): Promise<void>;
|
|
20
30
|
save(filename?: string): Promise<Buffer>;
|
|
21
31
|
}
|
package/dist/VisioDocument.js
CHANGED
|
@@ -116,6 +116,21 @@ class VisioDocument {
|
|
|
116
116
|
await this.pageManager.setBackgroundPage(foregroundPage.id, backgroundPage.id);
|
|
117
117
|
this._pageCache = null;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Find a page by name. Returns undefined if no page with that name exists.
|
|
121
|
+
*/
|
|
122
|
+
getPage(name) {
|
|
123
|
+
return this.pages.find(p => p.name === name);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Delete a page from the document.
|
|
127
|
+
* Removes the page XML, its relationships, the Content Types entry,
|
|
128
|
+
* and any BackPage references from other pages.
|
|
129
|
+
*/
|
|
130
|
+
async deletePage(page) {
|
|
131
|
+
await this.pageManager.deletePage(page.id);
|
|
132
|
+
this._pageCache = null;
|
|
133
|
+
}
|
|
119
134
|
async save(filename) {
|
|
120
135
|
return this.pkg.save(filename);
|
|
121
136
|
}
|
package/dist/VisioPackage.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export declare class VisioPackage {
|
|
|
6
6
|
load(buffer: Buffer | ArrayBuffer | Uint8Array): Promise<void>;
|
|
7
7
|
updateFile(path: string, content: string | Buffer): void;
|
|
8
8
|
save(filename?: string): Promise<Buffer>;
|
|
9
|
+
removeFile(path: string): void;
|
|
9
10
|
getFileText(path: string): string;
|
|
10
11
|
}
|
package/dist/VisioPackage.js
CHANGED
|
@@ -98,6 +98,13 @@ class VisioPackage {
|
|
|
98
98
|
}
|
|
99
99
|
return buffer;
|
|
100
100
|
}
|
|
101
|
+
removeFile(path) {
|
|
102
|
+
if (!this.zip) {
|
|
103
|
+
throw new Error("Package not loaded");
|
|
104
|
+
}
|
|
105
|
+
this._files.delete(path);
|
|
106
|
+
this.zip.remove(path);
|
|
107
|
+
}
|
|
101
108
|
getFileText(path) {
|
|
102
109
|
const content = this._files.get(path);
|
|
103
110
|
if (content === undefined) {
|
|
@@ -21,6 +21,13 @@ export declare class PageManager {
|
|
|
21
21
|
* Create a background page
|
|
22
22
|
*/
|
|
23
23
|
createBackgroundPage(name: string): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Delete a page and clean up all associated XML entries.
|
|
26
|
+
* Removes the page file, its .rels file, the entry in pages.xml,
|
|
27
|
+
* the relationship in pages.xml.rels, the Content Types override,
|
|
28
|
+
* and any BackPage references from other pages that pointed to it.
|
|
29
|
+
*/
|
|
30
|
+
deletePage(pageId: string): Promise<void>;
|
|
24
31
|
/**
|
|
25
32
|
* Set a background page for a foreground page
|
|
26
33
|
*/
|
package/dist/core/PageManager.js
CHANGED
|
@@ -208,6 +208,69 @@ class PageManager {
|
|
|
208
208
|
this.load(true);
|
|
209
209
|
return newId.toString();
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Delete a page and clean up all associated XML entries.
|
|
213
|
+
* Removes the page file, its .rels file, the entry in pages.xml,
|
|
214
|
+
* the relationship in pages.xml.rels, the Content Types override,
|
|
215
|
+
* and any BackPage references from other pages that pointed to it.
|
|
216
|
+
*/
|
|
217
|
+
async deletePage(pageId) {
|
|
218
|
+
this.load();
|
|
219
|
+
const page = this.pages.find(p => p.id.toString() === pageId);
|
|
220
|
+
if (!page)
|
|
221
|
+
throw new Error(`Page ${pageId} not found`);
|
|
222
|
+
const pageFileName = page.xmlPath.split('/').pop(); // e.g. "page2.xml"
|
|
223
|
+
// 1. Remove the page XML file
|
|
224
|
+
try {
|
|
225
|
+
this.pkg.removeFile(page.xmlPath);
|
|
226
|
+
}
|
|
227
|
+
catch { /* already gone */ }
|
|
228
|
+
// 2. Remove the page's .rels file if it exists
|
|
229
|
+
const pageRelsPath = `visio/pages/_rels/${pageFileName}.rels`;
|
|
230
|
+
try {
|
|
231
|
+
this.pkg.removeFile(pageRelsPath);
|
|
232
|
+
}
|
|
233
|
+
catch { /* no rels file is fine */ }
|
|
234
|
+
// 3. Remove entry from pages.xml (and strip BackPage refs pointing to this page)
|
|
235
|
+
const pagesPath = 'visio/pages/pages.xml';
|
|
236
|
+
const pagesContent = this.pkg.getFileText(pagesPath);
|
|
237
|
+
const parsedPages = this.parser.parse(pagesContent);
|
|
238
|
+
let pageNodes = parsedPages.Pages.Page;
|
|
239
|
+
if (!Array.isArray(pageNodes))
|
|
240
|
+
pageNodes = pageNodes ? [pageNodes] : [];
|
|
241
|
+
// Remove deleted page and clean up BackPage refs on remaining pages
|
|
242
|
+
parsedPages.Pages.Page = pageNodes
|
|
243
|
+
.filter((n) => n['@_ID'] !== pageId)
|
|
244
|
+
.map((n) => {
|
|
245
|
+
if (n['@_BackPage'] === pageId) {
|
|
246
|
+
const copy = { ...n };
|
|
247
|
+
delete copy['@_BackPage'];
|
|
248
|
+
return copy;
|
|
249
|
+
}
|
|
250
|
+
return n;
|
|
251
|
+
});
|
|
252
|
+
this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsedPages));
|
|
253
|
+
// 4. Remove relationship from pages.xml.rels
|
|
254
|
+
const pagesRelsPath = 'visio/pages/_rels/pages.xml.rels';
|
|
255
|
+
const pagesRelsContent = this.pkg.getFileText(pagesRelsPath);
|
|
256
|
+
const parsedPagesRels = this.parser.parse(pagesRelsContent);
|
|
257
|
+
let rels = parsedPagesRels.Relationships?.Relationship;
|
|
258
|
+
if (!Array.isArray(rels))
|
|
259
|
+
rels = rels ? [rels] : [];
|
|
260
|
+
parsedPagesRels.Relationships.Relationship = rels.filter((r) => r['@_Id'] !== page.relId);
|
|
261
|
+
this.pkg.updateFile(pagesRelsPath, (0, XmlHelper_1.buildXml)(this.builder, parsedPagesRels));
|
|
262
|
+
// 5. Remove Content Types override for the page file
|
|
263
|
+
const ctPath = '[Content_Types].xml';
|
|
264
|
+
const ctContent = this.pkg.getFileText(ctPath);
|
|
265
|
+
const parsedCt = this.parser.parse(ctContent);
|
|
266
|
+
let overrides = parsedCt.Types.Override;
|
|
267
|
+
if (!Array.isArray(overrides))
|
|
268
|
+
overrides = overrides ? [overrides] : [];
|
|
269
|
+
parsedCt.Types.Override = overrides.filter((o) => !o['@_PartName']?.endsWith(pageFileName));
|
|
270
|
+
this.pkg.updateFile(ctPath, (0, XmlHelper_1.buildXml)(this.builder, parsedCt));
|
|
271
|
+
// 6. Reload the page list to reflect the deletion
|
|
272
|
+
this.load(true);
|
|
273
|
+
}
|
|
211
274
|
/**
|
|
212
275
|
* Set a background page for a foreground page
|
|
213
276
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ class ForeignShapeBuilder {
|
|
|
17
17
|
{ '@_N': 'PinY', '@_V': props.y.toString() },
|
|
18
18
|
{ '@_N': 'Width', '@_V': props.width.toString() },
|
|
19
19
|
{ '@_N': 'Height', '@_V': props.height.toString() },
|
|
20
|
-
{ '@_N': 'LocPinX', '@_V': (props.width / 2).toString() },
|
|
21
|
-
{ '@_N': 'LocPinY', '@_V': (props.height / 2).toString() }
|
|
20
|
+
{ '@_N': 'LocPinX', '@_V': (props.width / 2).toString(), '@_F': 'Width*0.5' },
|
|
21
|
+
{ '@_N': 'LocPinY', '@_V': (props.height / 2).toString(), '@_F': 'Height*0.5' }
|
|
22
22
|
],
|
|
23
23
|
Section: [
|
|
24
24
|
// Foreign shapes typically have no border (LinePattern=0)
|
|
@@ -18,8 +18,8 @@ class ShapeBuilder {
|
|
|
18
18
|
{ '@_N': 'PinY', '@_V': props.y.toString() },
|
|
19
19
|
{ '@_N': 'Width', '@_V': props.width.toString() },
|
|
20
20
|
{ '@_N': 'Height', '@_V': props.height.toString() },
|
|
21
|
-
{ '@_N': 'LocPinX', '@_V': (props.width / 2).toString() },
|
|
22
|
-
{ '@_N': 'LocPinY', '@_V': (props.height / 2).toString() }
|
|
21
|
+
{ '@_N': 'LocPinX', '@_V': (props.width / 2).toString(), '@_F': 'Width*0.5' },
|
|
22
|
+
{ '@_N': 'LocPinY', '@_V': (props.height / 2).toString(), '@_F': 'Height*0.5' }
|
|
23
23
|
],
|
|
24
24
|
Section: []
|
|
25
25
|
// Text added at end by caller or we can do it here if props.text is final
|
|
@@ -36,12 +36,20 @@ class ShapeBuilder {
|
|
|
36
36
|
weight: '0.01'
|
|
37
37
|
}));
|
|
38
38
|
}
|
|
39
|
-
if (props.fontColor || props.bold) {
|
|
39
|
+
if (props.fontColor || props.bold || props.fontSize !== undefined || props.fontFamily !== undefined) {
|
|
40
40
|
shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
|
|
41
41
|
bold: props.bold,
|
|
42
|
-
color: props.fontColor
|
|
42
|
+
color: props.fontColor,
|
|
43
|
+
fontSize: props.fontSize,
|
|
44
|
+
fontFamily: props.fontFamily,
|
|
43
45
|
}));
|
|
44
46
|
}
|
|
47
|
+
if (props.horzAlign !== undefined) {
|
|
48
|
+
shape.Section.push((0, StyleHelpers_1.createParagraphSection)(props.horzAlign));
|
|
49
|
+
}
|
|
50
|
+
if (props.verticalAlign !== undefined) {
|
|
51
|
+
shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(props.verticalAlign) });
|
|
52
|
+
}
|
|
45
53
|
// Add Geometry
|
|
46
54
|
// Only if NOT a Group AND NOT a Master Instance
|
|
47
55
|
if (props.type !== 'Group' && !props.masterId) {
|
|
@@ -82,6 +82,14 @@ export interface NewShapeProps {
|
|
|
82
82
|
fillColor?: string;
|
|
83
83
|
fontColor?: string;
|
|
84
84
|
bold?: boolean;
|
|
85
|
+
/** Font size in points (e.g. 14 for 14pt). */
|
|
86
|
+
fontSize?: number;
|
|
87
|
+
/** Font family name (e.g. "Arial", "Times New Roman"). */
|
|
88
|
+
fontFamily?: string;
|
|
89
|
+
/** Horizontal text alignment within the shape. */
|
|
90
|
+
horzAlign?: 'left' | 'center' | 'right' | 'justify';
|
|
91
|
+
/** Vertical text alignment within the shape. */
|
|
92
|
+
verticalAlign?: 'top' | 'middle' | 'bottom';
|
|
85
93
|
type?: string;
|
|
86
94
|
masterId?: string;
|
|
87
95
|
imgRelId?: string;
|
|
@@ -15,12 +15,33 @@ export declare const ArrowHeads: {
|
|
|
15
15
|
CrowsFoot: string;
|
|
16
16
|
One: string;
|
|
17
17
|
};
|
|
18
|
+
declare const HORZ_ALIGN_VALUES: {
|
|
19
|
+
readonly left: "0";
|
|
20
|
+
readonly center: "1";
|
|
21
|
+
readonly right: "2";
|
|
22
|
+
readonly justify: "3";
|
|
23
|
+
};
|
|
24
|
+
declare const VERT_ALIGN_VALUES: {
|
|
25
|
+
readonly top: "0";
|
|
26
|
+
readonly middle: "1";
|
|
27
|
+
readonly bottom: "2";
|
|
28
|
+
};
|
|
29
|
+
export type HorzAlign = keyof typeof HORZ_ALIGN_VALUES;
|
|
30
|
+
export type VertAlign = keyof typeof VERT_ALIGN_VALUES;
|
|
31
|
+
export declare function horzAlignValue(align: HorzAlign): string;
|
|
32
|
+
export declare function vertAlignValue(align: VertAlign): string;
|
|
18
33
|
export declare function createCharacterSection(props: {
|
|
19
34
|
bold?: boolean;
|
|
20
35
|
color?: string;
|
|
36
|
+
/** Font size in points (e.g. 12 for 12pt). Stored internally as inches (pt / 72). */
|
|
37
|
+
fontSize?: number;
|
|
38
|
+
/** Font family name (e.g. "Arial"). Uses FONT() formula for portability. */
|
|
39
|
+
fontFamily?: string;
|
|
21
40
|
}): VisioSection;
|
|
41
|
+
export declare function createParagraphSection(horzAlign: HorzAlign): VisioSection;
|
|
22
42
|
export declare function createLineSection(props: {
|
|
23
43
|
color?: string;
|
|
24
44
|
pattern?: string;
|
|
25
45
|
weight?: string;
|
|
26
46
|
}): VisioSection;
|
|
47
|
+
export {};
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ArrowHeads = void 0;
|
|
4
4
|
exports.createFillSection = createFillSection;
|
|
5
|
+
exports.horzAlignValue = horzAlignValue;
|
|
6
|
+
exports.vertAlignValue = vertAlignValue;
|
|
5
7
|
exports.createCharacterSection = createCharacterSection;
|
|
8
|
+
exports.createParagraphSection = createParagraphSection;
|
|
6
9
|
exports.createLineSection = createLineSection;
|
|
7
10
|
const hexToRgb = (hex) => {
|
|
8
11
|
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
|
@@ -35,32 +38,66 @@ exports.ArrowHeads = {
|
|
|
35
38
|
One: '24', // Visio "One" (Dash) - Approximate, or '26'
|
|
36
39
|
// There are many variants, but 29 is the standard "Fork"
|
|
37
40
|
};
|
|
41
|
+
const HORZ_ALIGN_VALUES = {
|
|
42
|
+
left: '0',
|
|
43
|
+
center: '1',
|
|
44
|
+
right: '2',
|
|
45
|
+
justify: '3',
|
|
46
|
+
};
|
|
47
|
+
const VERT_ALIGN_VALUES = {
|
|
48
|
+
top: '0',
|
|
49
|
+
middle: '1',
|
|
50
|
+
bottom: '2',
|
|
51
|
+
};
|
|
52
|
+
function horzAlignValue(align) {
|
|
53
|
+
return HORZ_ALIGN_VALUES[align];
|
|
54
|
+
}
|
|
55
|
+
function vertAlignValue(align) {
|
|
56
|
+
return VERT_ALIGN_VALUES[align];
|
|
57
|
+
}
|
|
38
58
|
function createCharacterSection(props) {
|
|
39
|
-
// Visio Character Section
|
|
40
|
-
// N="Character"
|
|
41
|
-
// Row T="Character"
|
|
42
|
-
// Cell N="Color" V="#FF0000"
|
|
43
|
-
// Cell N="Style" V="1" (1=Bold, 2=Italic, 4=Underline) - Bitwise
|
|
44
|
-
// Visio booleans are often 0 or 1.
|
|
45
|
-
// Style=1 (Bold)
|
|
46
|
-
// Default Style is 0 (Normal)
|
|
47
59
|
let styleVal = 0;
|
|
48
60
|
if (props.bold) {
|
|
49
|
-
styleVal += 1; //
|
|
61
|
+
styleVal += 1; // Bold bit
|
|
50
62
|
}
|
|
51
|
-
// Default Color is usually 0 (Black) or specific hex
|
|
52
63
|
const colorVal = props.color || '#000000';
|
|
64
|
+
const cells = [
|
|
65
|
+
{ '@_N': 'Color', '@_V': colorVal, '@_F': hexToRgb(colorVal) },
|
|
66
|
+
{ '@_N': 'Style', '@_V': styleVal.toString() },
|
|
67
|
+
];
|
|
68
|
+
if (props.fontSize !== undefined) {
|
|
69
|
+
// Visio stores size in inches internally; @_U="PT" is a display hint for the ShapeSheet UI
|
|
70
|
+
const sizeInInches = props.fontSize / 72;
|
|
71
|
+
cells.push({ '@_N': 'Size', '@_V': sizeInInches.toString(), '@_U': 'PT' });
|
|
72
|
+
}
|
|
73
|
+
if (props.fontFamily !== undefined) {
|
|
74
|
+
// FONT("name") formula lets Visio resolve the font by name at load time.
|
|
75
|
+
// @_V="0" is a safe placeholder (document default font) used before Visio evaluates the formula.
|
|
76
|
+
cells.push({ '@_N': 'Font', '@_V': '0', '@_F': `FONT("${props.fontFamily}")` });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
cells.push({ '@_N': 'Font', '@_V': '1' }); // Default (Calibri)
|
|
80
|
+
}
|
|
53
81
|
return {
|
|
54
82
|
'@_N': 'Character',
|
|
55
83
|
Row: [
|
|
56
84
|
{
|
|
57
85
|
'@_T': 'Character',
|
|
58
86
|
'@_IX': '0',
|
|
87
|
+
Cell: cells,
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function createParagraphSection(horzAlign) {
|
|
93
|
+
return {
|
|
94
|
+
'@_N': 'Paragraph',
|
|
95
|
+
Row: [
|
|
96
|
+
{
|
|
97
|
+
'@_T': 'Paragraph',
|
|
98
|
+
'@_IX': '0',
|
|
59
99
|
Cell: [
|
|
60
|
-
{ '@_N': '
|
|
61
|
-
{ '@_N': 'Style', '@_V': styleVal.toString() },
|
|
62
|
-
// Size, Font, etc could go here
|
|
63
|
-
{ '@_N': 'Font', '@_V': '1' } // Default font (Calibri usually)
|
|
100
|
+
{ '@_N': 'HorzAlign', '@_V': HORZ_ALIGN_VALUES[horzAlign] },
|
|
64
101
|
]
|
|
65
102
|
}
|
|
66
103
|
]
|