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 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 (zippped XML).
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", // Option hexadecimal fill color
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;
@@ -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
  }
@@ -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;
@@ -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
  }
@@ -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
- const shapesArray = (0, VisioParsers_1.asArray)(shapesData);
28
- return shapesArray.map(s => this.parseShape(s));
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 = {
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  */
@@ -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
@@ -6,4 +6,5 @@ export { ShapeModifier } from './ShapeModifier';
6
6
  export { VisioDocument } from './VisioDocument';
7
7
  export { Page } from './Page';
8
8
  export { Shape } from './Shape';
9
+ export type { ShapeData, ShapeHyperlink } from './Shape';
9
10
  export * from './types/VisioTypes';
@@ -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; // Add Bold bit
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': 'Color', '@_V': colorVal, '@_F': hexToRgb(colorVal) },
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
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-visio",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {