ts-visio 1.10.0 → 1.13.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
@@ -28,6 +28,8 @@ Built using specific schema-level abstractions to handle the complex internal st
28
28
  - **Named Connection Points**: Define specific ports on shapes (`Top`, `Right`, etc.) and connect to them precisely using `fromPort`/`toPort` on any connector API.
29
29
  - **StyleSheets**: Create document-level named styles with fill, line, and text properties via `doc.createStyle()` and apply them to shapes at creation time (`styleId`) or post-creation (`shape.applyStyle()`).
30
30
  - **Color Palette**: Register named colors in the document's color table via `doc.addColor()` and look them up by index or hex value with `doc.getColors()` / `doc.getColorIndex()`.
31
+ - **Read Layers Back**: Enumerate existing layers from loaded files via `page.getLayers()`; delete a layer with `layer.delete()`, rename with `layer.rename()`, and read `layer.visible` / `layer.locked` state.
32
+ - **Group Traversal**: Access nested child shapes via `shape.getChildren()`, check `shape.isGroup`, and read `shape.type`.
31
33
 
32
34
  Feature gaps are being tracked in [FEATURES.md](./FEATURES.md).
33
35
 
@@ -316,12 +318,12 @@ await shape.toUrl('https://google.com')
316
318
  ```
317
319
 
318
320
  #### 17. Layers
319
- Organize complex diagrams with layers. Control visibility and locking programmatically.
321
+ Organize complex diagrams with layers. Control visibility and locking programmatically, and read layers back from loaded files.
320
322
 
321
323
  ```typescript
322
324
  // 1. Define Layers
323
- const wireframe = await page.addLayer('Wireframe');
324
- const annotations = await page.addLayer('Annotations');
325
+ const wireframe = await page.addLayer('Wireframe');
326
+ const annotations = await page.addLayer('Annotations', { visible: false });
325
327
 
326
328
  // 2. Assign Shapes to Layers
327
329
  await shape.addToLayer(wireframe);
@@ -333,6 +335,21 @@ await annotations.show(); // Show again
333
335
 
334
336
  // 4. Lock Layer
335
337
  await wireframe.setLocked(true);
338
+
339
+ // 5. Read all layers back (works on loaded files too)
340
+ const layers = page.getLayers();
341
+ // [ { name: 'Wireframe', index: 0, visible: true, locked: true },
342
+ // { name: 'Annotations', index: 1, visible: true, locked: false } ]
343
+
344
+ for (const layer of layers) {
345
+ console.log(layer.name, layer.visible, layer.locked);
346
+ }
347
+
348
+ // 6. Rename a layer
349
+ await wireframe.rename('Structural');
350
+
351
+ // 7. Delete a layer (cleans up shape assignments automatically)
352
+ await annotations.delete();
336
353
  ```
337
354
 
338
355
  #### 18. Cross-Functional Flowcharts (Swimlanes)
@@ -728,6 +745,52 @@ Built-in colors: IX 0 = `#000000` (black), IX 1 = `#FFFFFF` (white). User colors
728
745
 
729
746
  ---
730
747
 
748
+ #### 32. Group Traversal (`shape.getChildren()`)
749
+ Access nested child shapes of a group without touching XML. Only **direct** children are returned — call `getChildren()` recursively to walk a deeper tree.
750
+
751
+ ```typescript
752
+ // 1. Create a group with children
753
+ const group = await page.addShape({
754
+ text: 'Container', x: 5, y: 5, width: 6, height: 6, type: 'Group'
755
+ });
756
+ const childA = await page.addShape({ text: 'A', x: 1, y: 1, width: 2, height: 1 }, group.id);
757
+ const childB = await page.addShape({ text: 'B', x: 1, y: 3, width: 2, height: 1 }, group.id);
758
+
759
+ // 2. Check if a shape is a group
760
+ console.log(group.isGroup); // true
761
+ console.log(childA.isGroup); // false
762
+ console.log(group.type); // 'Group'
763
+
764
+ // 3. Get direct children
765
+ const children = group.getChildren();
766
+ // → [Shape('A'), Shape('B')]
767
+
768
+ for (const child of children) {
769
+ console.log(child.text, child.id);
770
+ }
771
+
772
+ // 4. Recursively walk a nested tree
773
+ function walk(shape, depth = 0) {
774
+ console.log(' '.repeat(depth * 2) + shape.text);
775
+ for (const child of shape.getChildren()) {
776
+ walk(child, depth + 1);
777
+ }
778
+ }
779
+ walk(group);
780
+
781
+ // 5. Works on shapes loaded from an existing .vsdx file
782
+ const doc2 = await VisioDocument.load('existing.vsdx');
783
+ const shapes = doc2.pages[0].getShapes();
784
+ const groups = shapes.filter(s => s.isGroup);
785
+ for (const g of groups) {
786
+ console.log(`Group "${g.text}" has ${g.getChildren().length} children`);
787
+ }
788
+ ```
789
+
790
+ `getChildren()` returns `[]` for non-group shapes. Children are full `Shape` instances — all existing methods (`setStyle()`, `getProperties()`, `delete()`, etc.) work on them.
791
+
792
+ ---
793
+
731
794
  ## Examples
732
795
 
733
796
  Check out the [examples](./examples) directory for complete scripts.
package/dist/Layer.d.ts CHANGED
@@ -5,10 +5,25 @@ export declare class Layer {
5
5
  index: number;
6
6
  private pageId?;
7
7
  private pkg?;
8
+ private _visible;
9
+ private _locked;
8
10
  private modifier;
9
- constructor(name: string, index: number, pageId?: string | undefined, pkg?: VisioPackage | undefined, modifier?: ShapeModifier);
11
+ constructor(name: string, index: number, pageId?: string | undefined, pkg?: VisioPackage | undefined, modifier?: ShapeModifier, _visible?: boolean, _locked?: boolean);
12
+ /** Whether the layer is currently visible. */
13
+ get visible(): boolean;
14
+ /** Whether the layer is currently locked. */
15
+ get locked(): boolean;
10
16
  setVisible(visible: boolean): Promise<this>;
11
17
  setLocked(locked: boolean): Promise<this>;
12
18
  hide(): Promise<this>;
13
19
  show(): Promise<this>;
20
+ /**
21
+ * Rename this layer.
22
+ */
23
+ rename(newName: string): Promise<this>;
24
+ /**
25
+ * Delete this layer from the page.
26
+ * Removes the layer definition and strips it from all shape LayerMember cells.
27
+ */
28
+ delete(): Promise<void>;
14
29
  }
package/dist/Layer.js CHANGED
@@ -3,18 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Layer = void 0;
4
4
  const ShapeModifier_1 = require("./ShapeModifier");
5
5
  class Layer {
6
- constructor(name, index, pageId, pkg, modifier) {
6
+ constructor(name, index, pageId, pkg, modifier, _visible = true, _locked = false) {
7
7
  this.name = name;
8
8
  this.index = index;
9
9
  this.pageId = pageId;
10
10
  this.pkg = pkg;
11
+ this._visible = _visible;
12
+ this._locked = _locked;
11
13
  this.modifier = modifier ?? (pkg ? new ShapeModifier_1.ShapeModifier(pkg) : null);
12
14
  }
15
+ /** Whether the layer is currently visible. */
16
+ get visible() {
17
+ return this._visible;
18
+ }
19
+ /** Whether the layer is currently locked. */
20
+ get locked() {
21
+ return this._locked;
22
+ }
13
23
  async setVisible(visible) {
14
24
  if (!this.pageId || !this.modifier) {
15
25
  throw new Error('Layer was not created with page context. Cannot update properties.');
16
26
  }
17
27
  await this.modifier.updateLayerProperty(this.pageId, this.index, 'Visible', visible ? '1' : '0');
28
+ this._visible = visible;
18
29
  return this;
19
30
  }
20
31
  async setLocked(locked) {
@@ -22,6 +33,7 @@ class Layer {
22
33
  throw new Error('Layer was not created with page context. Cannot update properties.');
23
34
  }
24
35
  await this.modifier.updateLayerProperty(this.pageId, this.index, 'Lock', locked ? '1' : '0');
36
+ this._locked = locked;
25
37
  return this;
26
38
  }
27
39
  async hide() {
@@ -30,5 +42,26 @@ class Layer {
30
42
  async show() {
31
43
  return this.setVisible(true);
32
44
  }
45
+ /**
46
+ * Rename this layer.
47
+ */
48
+ async rename(newName) {
49
+ if (!this.pageId || !this.modifier) {
50
+ throw new Error('Layer was not created with page context. Cannot update properties.');
51
+ }
52
+ await this.modifier.updateLayerProperty(this.pageId, this.index, 'Name', newName);
53
+ this.name = newName;
54
+ return this;
55
+ }
56
+ /**
57
+ * Delete this layer from the page.
58
+ * Removes the layer definition and strips it from all shape LayerMember cells.
59
+ */
60
+ async delete() {
61
+ if (!this.pageId || !this.modifier) {
62
+ throw new Error('Layer was not created with page context. Cannot delete.');
63
+ }
64
+ this.modifier.deleteLayer(this.pageId, this.index);
65
+ }
33
66
  }
34
67
  exports.Layer = Layer;
package/dist/Page.d.ts CHANGED
@@ -77,4 +77,15 @@ export declare class Page {
77
77
  lock?: boolean;
78
78
  print?: boolean;
79
79
  }): Promise<Layer>;
80
+ /**
81
+ * Return all layers defined on this page, ordered by index.
82
+ * Works for both newly created documents and loaded `.vsdx` files.
83
+ *
84
+ * @example
85
+ * const layers = page.getLayers();
86
+ * // [{ name: 'Background', index: 0, visible: true, locked: false }, ...]
87
+ */
88
+ getLayers(): Layer[];
89
+ /** @internal Used by VisioDocument.renamePage() to keep in-memory state in sync. */
90
+ _updateName(newName: string): void;
80
91
  }
package/dist/Page.js CHANGED
@@ -113,6 +113,7 @@ class Page {
113
113
  // In a real scenario, we might want to re-read the shape from disk to get full defaults
114
114
  const internalStub = (0, StubHelpers_1.createVisioShapeStub)({
115
115
  ID: newId,
116
+ Type: props.type,
116
117
  Text: props.text,
117
118
  Cells: {
118
119
  'Width': props.width,
@@ -267,5 +268,21 @@ class Page {
267
268
  const info = await this.modifier.addLayer(this.id, name, options);
268
269
  return new Layer_1.Layer(info.name, info.index, this.id, this.pkg, this.modifier);
269
270
  }
271
+ /**
272
+ * Return all layers defined on this page, ordered by index.
273
+ * Works for both newly created documents and loaded `.vsdx` files.
274
+ *
275
+ * @example
276
+ * const layers = page.getLayers();
277
+ * // [{ name: 'Background', index: 0, visible: true, locked: false }, ...]
278
+ */
279
+ getLayers() {
280
+ const infos = this.modifier.getPageLayers(this.id);
281
+ return infos.map(l => new Layer_1.Layer(l.name, l.index, this.id, this.pkg, this.modifier, l.visible, l.locked));
282
+ }
283
+ /** @internal Used by VisioDocument.renamePage() to keep in-memory state in sync. */
284
+ _updateName(newName) {
285
+ this.internalPage.Name = newName;
286
+ }
270
287
  }
271
288
  exports.Page = Page;
package/dist/Shape.d.ts CHANGED
@@ -23,6 +23,16 @@ export declare class Shape {
23
23
  constructor(internalShape: VisioShape, pageId: string, pkg: VisioPackage, modifier?: ShapeModifier);
24
24
  get id(): string;
25
25
  get name(): string;
26
+ /**
27
+ * The shape's Type attribute — `'Group'` for group shapes, `'Shape'` (or `undefined`
28
+ * normalised to `'Shape'`) for regular shapes.
29
+ */
30
+ get type(): string;
31
+ /**
32
+ * `true` if this shape is a Group (i.e. it can contain nested child shapes).
33
+ * Use `shape.getChildren()` to retrieve those children.
34
+ */
35
+ get isGroup(): boolean;
26
36
  get text(): string;
27
37
  setText(newText: string): Promise<void>;
28
38
  get width(): number;
@@ -73,6 +83,20 @@ export declare class Shape {
73
83
  * Returns an empty array if the shape has no layer assignment.
74
84
  */
75
85
  getLayerIndices(): number[];
86
+ /**
87
+ * Return the direct child shapes of this group.
88
+ * Returns an empty array for non-group shapes or groups with no children.
89
+ *
90
+ * Only direct children are returned — grandchildren are accessible by calling
91
+ * `getChildren()` on the child shape.
92
+ *
93
+ * @example
94
+ * const group = await page.addShape({ text: 'G', x: 5, y: 5, width: 4, height: 4, type: 'Group' });
95
+ * await page.addShape({ text: 'Child A', x: 1, y: 1, width: 1, height: 1 }, group.id);
96
+ * await page.addShape({ text: 'Child B', x: 2, y: 1, width: 1, height: 1 }, group.id);
97
+ * group.getChildren(); // → [Shape('Child A'), Shape('Child B')]
98
+ */
99
+ getChildren(): Shape[];
76
100
  /** Current rotation angle in degrees (0 if no Angle cell is set). */
77
101
  get angle(): number;
78
102
  /**
package/dist/Shape.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Shape = void 0;
4
4
  const ShapeModifier_1 = require("./ShapeModifier");
5
5
  const VisioTypes_1 = require("./types/VisioTypes");
6
+ const VisioConstants_1 = require("./core/VisioConstants");
6
7
  /** Round a coordinate to 10 decimal places to prevent float-to-string-to-float precision drift. */
7
8
  function fmtCoord(n) {
8
9
  return parseFloat(n.toFixed(10)).toString();
@@ -20,6 +21,20 @@ class Shape {
20
21
  get name() {
21
22
  return this.internalShape.Name;
22
23
  }
24
+ /**
25
+ * The shape's Type attribute — `'Group'` for group shapes, `'Shape'` (or `undefined`
26
+ * normalised to `'Shape'`) for regular shapes.
27
+ */
28
+ get type() {
29
+ return this.internalShape.Type ?? 'Shape';
30
+ }
31
+ /**
32
+ * `true` if this shape is a Group (i.e. it can contain nested child shapes).
33
+ * Use `shape.getChildren()` to retrieve those children.
34
+ */
35
+ get isGroup() {
36
+ return this.internalShape.Type === VisioConstants_1.SHAPE_TYPES.Group;
37
+ }
23
38
  get text() {
24
39
  return this.internalShape.Text || '';
25
40
  }
@@ -139,6 +154,23 @@ class Shape {
139
154
  getLayerIndices() {
140
155
  return this.modifier.getShapeLayerIndices(this.pageId, this.id);
141
156
  }
157
+ /**
158
+ * Return the direct child shapes of this group.
159
+ * Returns an empty array for non-group shapes or groups with no children.
160
+ *
161
+ * Only direct children are returned — grandchildren are accessible by calling
162
+ * `getChildren()` on the child shape.
163
+ *
164
+ * @example
165
+ * const group = await page.addShape({ text: 'G', x: 5, y: 5, width: 4, height: 4, type: 'Group' });
166
+ * await page.addShape({ text: 'Child A', x: 1, y: 1, width: 1, height: 1 }, group.id);
167
+ * await page.addShape({ text: 'Child B', x: 2, y: 1, width: 1, height: 1 }, group.id);
168
+ * group.getChildren(); // → [Shape('Child A'), Shape('Child B')]
169
+ */
170
+ getChildren() {
171
+ const children = this.modifier.getShapeChildren(this.pageId, this.id);
172
+ return children.map(c => new Shape(c, this.pageId, this.pkg, this.modifier));
173
+ }
142
174
  /** Current rotation angle in degrees (0 if no Angle cell is set). */
143
175
  get angle() {
144
176
  const cell = this.internalShape.Cells['Angle'];
@@ -185,7 +217,7 @@ class Shape {
185
217
  return this;
186
218
  }
187
219
  async addMember(memberShape) {
188
- await this.modifier.addRelationship(this.pageId, this.id, memberShape.id, 'Container');
220
+ await this.modifier.addRelationship(this.pageId, this.id, memberShape.id, VisioConstants_1.STRUCT_RELATIONSHIP_TYPES.Container);
189
221
  return this;
190
222
  }
191
223
  async addListItem(item) {
@@ -2,6 +2,7 @@ import { VisioPackage } from './VisioPackage';
2
2
  import { HorzAlign, VertAlign } from './utils/StyleHelpers';
3
3
  import { NewShapeProps, ConnectorStyle, ConnectionTarget, ConnectionPointDef } from './types/VisioTypes';
4
4
  import type { ShapeData, ShapeHyperlink } from './Shape';
5
+ import type { VisioShape } from './types/VisioTypes';
5
6
  export declare class ShapeModifier {
6
7
  private pkg;
7
8
  addContainer(pageId: string, props: NewShapeProps): Promise<string>;
@@ -100,6 +101,19 @@ export declare class ShapeModifier {
100
101
  }>;
101
102
  assignLayer(pageId: string, shapeId: string, layerIndex: number): Promise<void>;
102
103
  updateLayerProperty(pageId: string, layerIndex: number, propName: string, value: string): Promise<void>;
104
+ /**
105
+ * Return all layers defined in the page's PageSheet as plain objects.
106
+ */
107
+ getPageLayers(pageId: string): Array<{
108
+ name: string;
109
+ index: number;
110
+ visible: boolean;
111
+ locked: boolean;
112
+ }>;
113
+ /**
114
+ * Delete a layer by index and remove it from all shape LayerMember cells.
115
+ */
116
+ deleteLayer(pageId: string, layerIndex: number): void;
103
117
  /**
104
118
  * Read back all custom property (shape data) entries for a shape.
105
119
  * Returns a map of property key → ShapeData, with values coerced to
@@ -128,6 +142,11 @@ export declare class ShapeModifier {
128
142
  * Returns an empty array if the shape has no layer assignment.
129
143
  */
130
144
  getShapeLayerIndices(pageId: string, shapeId: string): number[];
145
+ /**
146
+ * Return the direct child shapes of a group or container shape.
147
+ * Returns an empty array for non-group shapes or shapes with no children.
148
+ */
149
+ getShapeChildren(pageId: string, shapeId: string): VisioShape[];
131
150
  }
132
151
  export interface ShapeStyle {
133
152
  fillColor?: string;
@@ -6,6 +6,7 @@ const StyleHelpers_1 = require("./utils/StyleHelpers");
6
6
  const VisioConstants_1 = require("./core/VisioConstants");
7
7
  const VisioTypes_1 = require("./types/VisioTypes");
8
8
  const ConnectionPointBuilder_1 = require("./shapes/ConnectionPointBuilder");
9
+ const ShapeReader_1 = require("./ShapeReader");
9
10
  const ForeignShapeBuilder_1 = require("./shapes/ForeignShapeBuilder");
10
11
  const ShapeBuilder_1 = require("./shapes/ShapeBuilder");
11
12
  const ConnectorBuilder_1 = require("./shapes/ConnectorBuilder");
@@ -230,9 +231,9 @@ class ShapeModifier {
230
231
  if (!Array.isArray(shape.Section))
231
232
  shape.Section = [shape.Section];
232
233
  // Find or create Connection section
233
- let connSection = shape.Section.find((s) => s['@_N'] === 'Connection');
234
+ let connSection = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Connection);
234
235
  if (!connSection) {
235
- connSection = { '@_N': 'Connection', Row: [] };
236
+ connSection = { '@_N': VisioConstants_1.SECTION_NAMES.Connection, Row: [] };
236
237
  shape.Section.push(connSection);
237
238
  }
238
239
  if (!connSection.Row)
@@ -282,7 +283,7 @@ class ShapeModifier {
282
283
  newId = this.getNextId(parsed);
283
284
  }
284
285
  let newShape;
285
- if (props.type === 'Foreign' && props.imgRelId) {
286
+ if (props.type === VisioConstants_1.SHAPE_TYPES.Foreign && props.imgRelId) {
286
287
  newShape = ForeignShapeBuilder_1.ForeignShapeBuilder.createImageShapeObject(newId, props.imgRelId, props);
287
288
  // Text for foreign shapes? Usually none, but we can support it.
288
289
  if (props.text !== undefined && props.text !== null) {
@@ -311,8 +312,8 @@ class ShapeModifier {
311
312
  parent.Shapes.Shape = parent.Shapes.Shape ? [parent.Shapes.Shape] : [];
312
313
  }
313
314
  // Mark parent as Group if not already
314
- if (parent['@_Type'] !== 'Group') {
315
- parent['@_Type'] = 'Group';
315
+ if (parent['@_Type'] !== VisioConstants_1.SHAPE_TYPES.Group) {
316
+ parent['@_Type'] = VisioConstants_1.SHAPE_TYPES.Group;
316
317
  }
317
318
  parent.Shapes.Shape.push(newShape);
318
319
  }
@@ -397,7 +398,7 @@ class ShapeModifier {
397
398
  // Update/Add Fill
398
399
  if (style.fillColor) {
399
400
  // Remove existing Fill section if any (simplified: assuming IX=0)
400
- shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Fill');
401
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Fill);
401
402
  shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
402
403
  }
403
404
  // Update/Add Line
@@ -405,7 +406,7 @@ class ShapeModifier {
405
406
  || style.lineWeight !== undefined
406
407
  || style.linePattern !== undefined;
407
408
  if (hasLineProps) {
408
- shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Line');
409
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Line);
409
410
  shape.Section.push((0, StyleHelpers_1.createLineSection)({
410
411
  color: style.lineColor,
411
412
  weight: style.lineWeight !== undefined ? (style.lineWeight / 72).toString() : undefined,
@@ -421,7 +422,7 @@ class ShapeModifier {
421
422
  || style.fontSize !== undefined
422
423
  || style.fontFamily !== undefined;
423
424
  if (hasCharProps) {
424
- shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Character');
425
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Character);
425
426
  shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
426
427
  bold: style.bold,
427
428
  italic: style.italic,
@@ -438,7 +439,7 @@ class ShapeModifier {
438
439
  || style.spaceAfter !== undefined
439
440
  || style.lineSpacing !== undefined;
440
441
  if (hasParagraphProps) {
441
- shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Paragraph');
442
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.Paragraph);
442
443
  shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
443
444
  horzAlign: style.horzAlign,
444
445
  spaceBefore: style.spaceBefore,
@@ -452,7 +453,7 @@ class ShapeModifier {
452
453
  || style.textMarginLeft !== undefined
453
454
  || style.textMarginRight !== undefined;
454
455
  if (hasTextBlockProps) {
455
- shape.Section = shape.Section.filter((s) => s['@_N'] !== 'TextBlock');
456
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== VisioConstants_1.SECTION_NAMES.TextBlock);
456
457
  shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
457
458
  topMargin: style.textMarginTop,
458
459
  bottomMargin: style.textMarginBottom,
@@ -570,7 +571,7 @@ class ShapeModifier {
570
571
  if (shape.Section) {
571
572
  const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
572
573
  for (const section of sections) {
573
- if (section['@_N'] !== 'Geometry' || !section.Row)
574
+ if (section['@_N'] !== VisioConstants_1.SECTION_NAMES.Geometry || !section.Row)
574
575
  continue;
575
576
  const rows = Array.isArray(section.Row) ? section.Row : [section.Row];
576
577
  for (const row of rows) {
@@ -627,9 +628,9 @@ class ShapeModifier {
627
628
  if (!Array.isArray(shape.Section))
628
629
  shape.Section = [shape.Section];
629
630
  // Find or Create Property Section
630
- let propSection = shape.Section.find((s) => s['@_N'] === 'Property');
631
+ let propSection = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Property);
631
632
  if (!propSection) {
632
- propSection = { '@_N': 'Property', Row: [] };
633
+ propSection = { '@_N': VisioConstants_1.SECTION_NAMES.Property, Row: [] };
633
634
  shape.Section.push(propSection);
634
635
  }
635
636
  // Ensure Row array exists
@@ -681,7 +682,7 @@ class ShapeModifier {
681
682
  }
682
683
  // Ensure Section array exists
683
684
  const sections = shape.Section ? (Array.isArray(shape.Section) ? shape.Section : [shape.Section]) : [];
684
- const propSection = sections.find((s) => s['@_N'] === 'Property');
685
+ const propSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Property);
685
686
  if (!propSection) {
686
687
  throw new Error(`Property definition 'Prop.${name}' does not exist on shape ${shapeId}. Call addPropertyDefinition first.`);
687
688
  }
@@ -770,7 +771,7 @@ class ShapeModifier {
770
771
  return [];
771
772
  const relsArray = Array.isArray(rels) ? rels : [rels];
772
773
  return relsArray
773
- .filter((r) => r['@_Type'] === 'Container' && r['@_ShapeID'] === containerId)
774
+ .filter((r) => r['@_Type'] === VisioConstants_1.STRUCT_RELATIONSHIP_TYPES.Container && r['@_ShapeID'] === containerId)
774
775
  .map((r) => r['@_RelatedShapeID']);
775
776
  }
776
777
  async reorderShape(pageId, shapeId, position) {
@@ -805,7 +806,7 @@ class ShapeModifier {
805
806
  const getUserVal = (name, def) => {
806
807
  if (!listShape.Section)
807
808
  return def;
808
- const userSec = listShape.Section.find((s) => s['@_N'] === 'User');
809
+ const userSec = listShape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.User);
809
810
  if (!userSec || !userSec.Row)
810
811
  return def;
811
812
  const rows = Array.isArray(userSec.Row) ? userSec.Row : [userSec.Row];
@@ -850,7 +851,7 @@ class ShapeModifier {
850
851
  // 3. Update Item Position
851
852
  await this.updateShapePosition(pageId, itemId, newX, newY);
852
853
  // 4. Add Relationship
853
- await this.addRelationship(pageId, listId, itemId, 'Container');
854
+ await this.addRelationship(pageId, listId, itemId, VisioConstants_1.STRUCT_RELATIONSHIP_TYPES.Container);
854
855
  // 5. Resize List Container
855
856
  await this.resizeContainerToFit(pageId, listId, 0.25);
856
857
  }
@@ -946,9 +947,9 @@ class ShapeModifier {
946
947
  if (!Array.isArray(pageSheet.Section))
947
948
  pageSheet.Section = [pageSheet.Section];
948
949
  // Find or Create Layer Section
949
- let layerSection = pageSheet.Section.find((s) => s['@_N'] === 'Layer');
950
+ let layerSection = pageSheet.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Layer);
950
951
  if (!layerSection) {
951
- layerSection = { '@_N': 'Layer', Row: [] };
952
+ layerSection = { '@_N': VisioConstants_1.SECTION_NAMES.Layer, Row: [] };
952
953
  pageSheet.Section.push(layerSection);
953
954
  }
954
955
  // Ensure Row array
@@ -990,9 +991,9 @@ class ShapeModifier {
990
991
  if (!Array.isArray(shape.Section))
991
992
  shape.Section = [shape.Section];
992
993
  // Find or Create LayerMem Section
993
- let memSection = shape.Section.find((s) => s['@_N'] === 'LayerMem');
994
+ let memSection = shape.Section.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.LayerMem);
994
995
  if (!memSection) {
995
- memSection = { '@_N': 'LayerMem', Row: [] };
996
+ memSection = { '@_N': VisioConstants_1.SECTION_NAMES.LayerMem, Row: [] };
996
997
  shape.Section.push(memSection);
997
998
  }
998
999
  // Ensure Row array
@@ -1036,7 +1037,7 @@ class ShapeModifier {
1036
1037
  if (!pageSheet.Section)
1037
1038
  return;
1038
1039
  const sections = Array.isArray(pageSheet.Section) ? pageSheet.Section : [pageSheet.Section];
1039
- const layerSection = sections.find((s) => s['@_N'] === 'Layer');
1040
+ const layerSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Layer);
1040
1041
  if (!layerSection || !layerSection.Row)
1041
1042
  return;
1042
1043
  const rows = Array.isArray(layerSection.Row) ? layerSection.Row : [layerSection.Row];
@@ -1059,6 +1060,69 @@ class ShapeModifier {
1059
1060
  }
1060
1061
  this.saveParsed(pageId, parsed);
1061
1062
  }
1063
+ /**
1064
+ * Return all layers defined in the page's PageSheet as plain objects.
1065
+ */
1066
+ getPageLayers(pageId) {
1067
+ const parsed = this.getParsed(pageId);
1068
+ const pageSheet = parsed.PageContents?.PageSheet;
1069
+ if (!pageSheet?.Section)
1070
+ return [];
1071
+ const sections = Array.isArray(pageSheet.Section) ? pageSheet.Section : [pageSheet.Section];
1072
+ const layerSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Layer);
1073
+ if (!layerSection?.Row)
1074
+ return [];
1075
+ const rows = Array.isArray(layerSection.Row) ? layerSection.Row : [layerSection.Row];
1076
+ return rows.map((row) => {
1077
+ const cells = Array.isArray(row.Cell) ? row.Cell : (row.Cell ? [row.Cell] : []);
1078
+ const getVal = (name) => cells.find((c) => c['@_N'] === name)?.['@_V'];
1079
+ return {
1080
+ name: getVal('Name') ?? '',
1081
+ index: parseInt(row['@_IX'], 10),
1082
+ visible: getVal('Visible') !== '0',
1083
+ locked: getVal('Lock') === '1',
1084
+ };
1085
+ });
1086
+ }
1087
+ /**
1088
+ * Delete a layer by index and remove it from all shape LayerMember cells.
1089
+ */
1090
+ deleteLayer(pageId, layerIndex) {
1091
+ const parsed = this.getParsed(pageId);
1092
+ // Remove the row from the Layer section in PageSheet
1093
+ const pageSheet = parsed.PageContents?.PageSheet;
1094
+ if (pageSheet?.Section) {
1095
+ const sections = Array.isArray(pageSheet.Section) ? pageSheet.Section : [pageSheet.Section];
1096
+ const layerSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Layer);
1097
+ if (layerSection?.Row) {
1098
+ const rows = Array.isArray(layerSection.Row) ? layerSection.Row : [layerSection.Row];
1099
+ layerSection.Row = rows.filter((r) => r['@_IX'] !== layerIndex.toString());
1100
+ }
1101
+ }
1102
+ // Remove this layer index from every shape's LayerMember cell
1103
+ const idxStr = layerIndex.toString();
1104
+ for (const [, shape] of this.getShapeMap(parsed)) {
1105
+ if (!shape.Section)
1106
+ continue;
1107
+ const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
1108
+ const memSec = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.LayerMem);
1109
+ if (!memSec?.Row)
1110
+ continue;
1111
+ const rows = Array.isArray(memSec.Row) ? memSec.Row : [memSec.Row];
1112
+ const row = rows[0];
1113
+ if (!row?.Cell)
1114
+ continue;
1115
+ const cells = Array.isArray(row.Cell) ? row.Cell : [row.Cell];
1116
+ const memberCell = cells.find((c) => c['@_N'] === 'LayerMember');
1117
+ if (!memberCell?.['@_V'])
1118
+ continue;
1119
+ const remaining = memberCell['@_V']
1120
+ .split(';')
1121
+ .filter((s) => s.length > 0 && s !== idxStr);
1122
+ memberCell['@_V'] = remaining.join(';');
1123
+ }
1124
+ this.saveParsed(pageId, parsed);
1125
+ }
1062
1126
  /**
1063
1127
  * Read back all custom property (shape data) entries for a shape.
1064
1128
  * Returns a map of property key → ShapeData, with values coerced to
@@ -1073,7 +1137,7 @@ class ShapeModifier {
1073
1137
  if (!shape.Section)
1074
1138
  return result;
1075
1139
  const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
1076
- const propSection = sections.find((s) => s['@_N'] === 'Property');
1140
+ const propSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.Property);
1077
1141
  if (!propSection?.Row)
1078
1142
  return result;
1079
1143
  const rows = Array.isArray(propSection.Row) ? propSection.Row : [propSection.Row];
@@ -1188,7 +1252,7 @@ class ShapeModifier {
1188
1252
  if (!shape.Section)
1189
1253
  return [];
1190
1254
  const sections = Array.isArray(shape.Section) ? shape.Section : [shape.Section];
1191
- const memSection = sections.find((s) => s['@_N'] === 'LayerMem');
1255
+ const memSection = sections.find((s) => s['@_N'] === VisioConstants_1.SECTION_NAMES.LayerMem);
1192
1256
  if (!memSection?.Row)
1193
1257
  return [];
1194
1258
  const rows = Array.isArray(memSection.Row) ? memSection.Row : [memSection.Row];
@@ -1205,5 +1269,14 @@ class ShapeModifier {
1205
1269
  .map((s) => parseInt(s))
1206
1270
  .filter((n) => !isNaN(n));
1207
1271
  }
1272
+ /**
1273
+ * Return the direct child shapes of a group or container shape.
1274
+ * Returns an empty array for non-group shapes or shapes with no children.
1275
+ */
1276
+ getShapeChildren(pageId, shapeId) {
1277
+ const pagePath = this.getPagePath(pageId);
1278
+ const reader = new ShapeReader_1.ShapeReader(this.pkg);
1279
+ return reader.readChildShapes(pagePath, shapeId);
1280
+ }
1208
1281
  }
1209
1282
  exports.ShapeModifier = ShapeModifier;
@@ -25,6 +25,12 @@ export declare class ShapeReader {
25
25
  /** Decode a Visio ToPart integer string to a ConnectionTarget. */
26
26
  private decodeToPart;
27
27
  private gatherShapes;
28
+ /**
29
+ * Return the direct child shapes of a group or container shape.
30
+ * Returns an empty array if the shape has no children or does not exist.
31
+ */
32
+ readChildShapes(path: string, parentId: string): VisioShape[];
28
33
  private findShapeById;
34
+ private findRawShape;
29
35
  private parseShape;
30
36
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ShapeReader = void 0;
4
4
  const fast_xml_parser_1 = require("fast-xml-parser");
5
5
  const VisioParsers_1 = require("./utils/VisioParsers");
6
+ const VisioConstants_1 = require("./core/VisioConstants");
6
7
  class ShapeReader {
7
8
  constructor(pkg) {
8
9
  this.pkg = pkg;
@@ -138,7 +139,7 @@ class ShapeReader {
138
139
  let lineWeight;
139
140
  let linePattern;
140
141
  for (const sec of sections) {
141
- if (sec['@_N'] === 'Line') {
142
+ if (sec['@_N'] === VisioConstants_1.SECTION_NAMES.Line) {
142
143
  const lineCells = (0, VisioParsers_1.parseCells)(sec);
143
144
  if (lineCells['LineColor']?.V)
144
145
  lineColor = lineCells['LineColor'].V;
@@ -190,12 +191,37 @@ class ShapeReader {
190
191
  }
191
192
  }
192
193
  }
194
+ /**
195
+ * Return the direct child shapes of a group or container shape.
196
+ * Returns an empty array if the shape has no children or does not exist.
197
+ */
198
+ readChildShapes(path, parentId) {
199
+ let content;
200
+ try {
201
+ content = this.pkg.getFileText(path);
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ const parsed = this.parser.parse(content);
207
+ const shapesData = parsed.PageContents?.Shapes?.Shape;
208
+ if (!shapesData)
209
+ return [];
210
+ const rawParent = this.findRawShape((0, VisioParsers_1.asArray)(shapesData), parentId);
211
+ if (!rawParent?.Shapes?.Shape)
212
+ return [];
213
+ return (0, VisioParsers_1.asArray)(rawParent.Shapes.Shape).map((s) => this.parseShape(s));
214
+ }
193
215
  findShapeById(rawShapes, shapeId) {
216
+ const raw = this.findRawShape(rawShapes, shapeId);
217
+ return raw ? this.parseShape(raw) : undefined;
218
+ }
219
+ findRawShape(rawShapes, shapeId) {
194
220
  for (const s of rawShapes) {
195
221
  if (s['@_ID'] === shapeId)
196
- return this.parseShape(s);
222
+ return s;
197
223
  if (s.Shapes?.Shape) {
198
- const found = this.findShapeById((0, VisioParsers_1.asArray)(s.Shapes.Shape), shapeId);
224
+ const found = this.findRawShape((0, VisioParsers_1.asArray)(s.Shapes.Shape), shapeId);
199
225
  if (found)
200
226
  return found;
201
227
  }
@@ -31,6 +31,30 @@ export declare class VisioDocument {
31
31
  * and any BackPage references from other pages.
32
32
  */
33
33
  deletePage(page: Page): Promise<void>;
34
+ /**
35
+ * Rename a page. Updates `page.name` in-memory as well as the pages.xml record.
36
+ *
37
+ * @example
38
+ * doc.renamePage(page, 'Architecture Overview');
39
+ */
40
+ renamePage(page: Page, newName: string): void;
41
+ /**
42
+ * Move a page to a new 0-based position in the tab order.
43
+ * Clamps `toIndex` to the valid range automatically.
44
+ *
45
+ * @example
46
+ * doc.movePage(page, 0); // move to first position
47
+ */
48
+ movePage(page: Page, toIndex: number): void;
49
+ /**
50
+ * Duplicate a page and return the new Page object.
51
+ * The duplicate is inserted directly after the source page in the tab order.
52
+ * If `newName` is omitted, `"<original name> (Copy)"` is used.
53
+ *
54
+ * @example
55
+ * const copy = await doc.duplicatePage(page, 'Page 2');
56
+ */
57
+ duplicatePage(page: Page, newName?: string): Promise<Page>;
34
58
  /**
35
59
  * Read document metadata from `docProps/core.xml` and `docProps/app.xml`.
36
60
  * Fields not present in the file are returned as `undefined`.
@@ -137,6 +137,48 @@ class VisioDocument {
137
137
  await this.pageManager.deletePage(page.id);
138
138
  this._pageCache = null;
139
139
  }
140
+ /**
141
+ * Rename a page. Updates `page.name` in-memory as well as the pages.xml record.
142
+ *
143
+ * @example
144
+ * doc.renamePage(page, 'Architecture Overview');
145
+ */
146
+ renamePage(page, newName) {
147
+ this.pageManager.renamePage(page.id, newName);
148
+ page._updateName(newName);
149
+ }
150
+ /**
151
+ * Move a page to a new 0-based position in the tab order.
152
+ * Clamps `toIndex` to the valid range automatically.
153
+ *
154
+ * @example
155
+ * doc.movePage(page, 0); // move to first position
156
+ */
157
+ movePage(page, toIndex) {
158
+ this.pageManager.reorderPage(page.id, toIndex);
159
+ this._pageCache = null;
160
+ }
161
+ /**
162
+ * Duplicate a page and return the new Page object.
163
+ * The duplicate is inserted directly after the source page in the tab order.
164
+ * If `newName` is omitted, `"<original name> (Copy)"` is used.
165
+ *
166
+ * @example
167
+ * const copy = await doc.duplicatePage(page, 'Page 2');
168
+ */
169
+ async duplicatePage(page, newName) {
170
+ const resolvedName = newName ?? `${page.name} (Copy)`;
171
+ const newId = await this.pageManager.duplicatePage(page.id, resolvedName);
172
+ this._pageCache = null;
173
+ const pageStub = {
174
+ ID: newId,
175
+ Name: resolvedName,
176
+ xmlPath: `visio/pages/page${newId}.xml`,
177
+ Shapes: [],
178
+ Connects: []
179
+ };
180
+ return new Page_1.Page(pageStub, this.pkg, this.mediaManager);
181
+ }
140
182
  /**
141
183
  * Read document metadata from `docProps/core.xml` and `docProps/app.xml`.
142
184
  * Fields not present in the file are returned as `undefined`.
@@ -28,6 +28,21 @@ export declare class PageManager {
28
28
  * and any BackPage references from other pages that pointed to it.
29
29
  */
30
30
  deletePage(pageId: string): Promise<void>;
31
+ /**
32
+ * Rename a page — updates Name and NameU in pages.xml.
33
+ */
34
+ renamePage(pageId: string, newName: string): void;
35
+ /**
36
+ * Move a page to a new 0-based position in the pages.xml order (which
37
+ * controls the tab order in the Visio UI).
38
+ */
39
+ reorderPage(pageId: string, toIndex: number): void;
40
+ /**
41
+ * Duplicate a page: copies the page XML (and its rels file if present),
42
+ * registers the new page in pages.xml and pages.xml.rels, and returns
43
+ * the new page ID.
44
+ */
45
+ duplicatePage(pageId: string, newName: string): Promise<string>;
31
46
  /**
32
47
  * Set a background page for a foreground page
33
48
  */
@@ -271,6 +271,107 @@ class PageManager {
271
271
  // 6. Reload the page list to reflect the deletion
272
272
  this.load(true);
273
273
  }
274
+ /**
275
+ * Rename a page — updates Name and NameU in pages.xml.
276
+ */
277
+ renamePage(pageId, newName) {
278
+ this.load();
279
+ const pagesPath = 'visio/pages/pages.xml';
280
+ const parsed = this.parser.parse(this.pkg.getFileText(pagesPath));
281
+ let pageNodes = parsed.Pages.Page;
282
+ if (!Array.isArray(pageNodes))
283
+ pageNodes = pageNodes ? [pageNodes] : [];
284
+ const node = pageNodes.find((n) => n['@_ID'] === pageId);
285
+ if (!node)
286
+ throw new Error(`Page ${pageId} not found`);
287
+ node['@_Name'] = newName;
288
+ node['@_NameU'] = newName;
289
+ this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsed));
290
+ this.load(true);
291
+ }
292
+ /**
293
+ * Move a page to a new 0-based position in the pages.xml order (which
294
+ * controls the tab order in the Visio UI).
295
+ */
296
+ reorderPage(pageId, toIndex) {
297
+ this.load();
298
+ const pagesPath = 'visio/pages/pages.xml';
299
+ const parsed = this.parser.parse(this.pkg.getFileText(pagesPath));
300
+ let pageNodes = parsed.Pages.Page;
301
+ if (!Array.isArray(pageNodes))
302
+ pageNodes = pageNodes ? [pageNodes] : [];
303
+ const fromIndex = pageNodes.findIndex((n) => n['@_ID'] === pageId);
304
+ if (fromIndex === -1)
305
+ throw new Error(`Page ${pageId} not found`);
306
+ const clamped = Math.max(0, Math.min(toIndex, pageNodes.length - 1));
307
+ const [moved] = pageNodes.splice(fromIndex, 1);
308
+ pageNodes.splice(clamped, 0, moved);
309
+ parsed.Pages.Page = pageNodes;
310
+ this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsed));
311
+ this.load(true);
312
+ }
313
+ /**
314
+ * Duplicate a page: copies the page XML (and its rels file if present),
315
+ * registers the new page in pages.xml and pages.xml.rels, and returns
316
+ * the new page ID.
317
+ */
318
+ async duplicatePage(pageId, newName) {
319
+ this.load();
320
+ const source = this.pages.find(p => p.id.toString() === pageId);
321
+ if (!source)
322
+ throw new Error(`Page ${pageId} not found`);
323
+ // 1. Calculate new ID and paths
324
+ const maxId = Math.max(...this.pages.map(p => p.id));
325
+ const newId = maxId + 1;
326
+ const newFileName = `page${newId}.xml`;
327
+ const newPath = `visio/pages/${newFileName}`;
328
+ // 2. Copy page XML verbatim
329
+ const sourceXml = this.pkg.getFileText(source.xmlPath);
330
+ this.pkg.updateFile(newPath, sourceXml);
331
+ // 3. Copy the page's .rels file if one exists (image refs etc.)
332
+ const sourceFileName = source.xmlPath.split('/').pop();
333
+ const sourceRelsPath = `visio/pages/_rels/${sourceFileName}.rels`;
334
+ const newRelsPath = `visio/pages/_rels/${newFileName}.rels`;
335
+ try {
336
+ this.pkg.updateFile(newRelsPath, this.pkg.getFileText(sourceRelsPath));
337
+ }
338
+ catch { /* no rels file — fine */ }
339
+ // 4. Add Content Types override
340
+ const ctPath = '[Content_Types].xml';
341
+ const parsedCt = this.parser.parse(this.pkg.getFileText(ctPath));
342
+ if (!parsedCt.Types.Override)
343
+ parsedCt.Types.Override = [];
344
+ if (!Array.isArray(parsedCt.Types.Override))
345
+ parsedCt.Types.Override = [parsedCt.Types.Override];
346
+ parsedCt.Types.Override.push({
347
+ '@_PartName': `/${newPath}`,
348
+ '@_ContentType': VisioConstants_1.CONTENT_TYPES.VISIO_PAGE
349
+ });
350
+ this.pkg.updateFile(ctPath, (0, XmlHelper_1.buildXml)(this.builder, parsedCt));
351
+ // 5. Add relationship in pages.xml.rels
352
+ const rId = await this.relsManager.ensureRelationship('visio/pages/pages.xml', newFileName, VisioConstants_1.RELATIONSHIP_TYPES.PAGE);
353
+ // 6. Append entry to pages.xml (directly after the source page)
354
+ const pagesPath = 'visio/pages/pages.xml';
355
+ const parsedPages = this.parser.parse(this.pkg.getFileText(pagesPath));
356
+ if (!parsedPages.Pages.Page)
357
+ parsedPages.Pages.Page = [];
358
+ if (!Array.isArray(parsedPages.Pages.Page))
359
+ parsedPages.Pages.Page = [parsedPages.Pages.Page];
360
+ const newEntry = {
361
+ '@_ID': newId.toString(),
362
+ '@_Name': newName,
363
+ '@_NameU': newName,
364
+ 'Rel': { '@_r:id': rId }
365
+ };
366
+ if (source.isBackground)
367
+ newEntry['@_Background'] = '1';
368
+ // Insert right after the source page so the duplicate is adjacent in the tab bar
369
+ const srcIdx = parsedPages.Pages.Page.findIndex((n) => n['@_ID'] === pageId);
370
+ parsedPages.Pages.Page.splice(srcIdx + 1, 0, newEntry);
371
+ this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsedPages));
372
+ this.load(true);
373
+ return newId.toString();
374
+ }
274
375
  /**
275
376
  * Set a background page for a foreground page
276
377
  */
@@ -21,6 +21,36 @@ export declare const RELATIONSHIP_TYPES: {
21
21
  readonly CORE_PROPERTIES: "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
22
22
  readonly EXTENDED_PROPERTIES: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
23
23
  };
24
+ /** Visio shape `Type` attribute values. */
25
+ export declare const SHAPE_TYPES: {
26
+ readonly Shape: "Shape";
27
+ readonly Group: "Group";
28
+ readonly Foreign: "Foreign";
29
+ };
30
+ /**
31
+ * Visio ShapeSheet section names (the `N` attribute on `<Section>` elements).
32
+ * Used when finding or filtering sections by name in page/master XML.
33
+ */
34
+ export declare const SECTION_NAMES: {
35
+ readonly Line: "Line";
36
+ readonly Fill: "Fill";
37
+ readonly Character: "Character";
38
+ readonly Paragraph: "Paragraph";
39
+ readonly TextBlock: "TextBlock";
40
+ readonly Geometry: "Geometry";
41
+ readonly Connection: "Connection";
42
+ readonly Property: "Property";
43
+ readonly User: "User";
44
+ readonly LayerMem: "LayerMem";
45
+ readonly Layer: "Layer";
46
+ };
47
+ /**
48
+ * Structural relationship types stored in `<Relationship>` elements
49
+ * inside Visio page XML (distinct from OPC `.rels` relationship types).
50
+ */
51
+ export declare const STRUCT_RELATIONSHIP_TYPES: {
52
+ readonly Container: "Container";
53
+ };
24
54
  export declare const CONTENT_TYPES: {
25
55
  readonly PNG: "image/png";
26
56
  readonly JPEG: "image/jpeg";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONTENT_TYPES = exports.RELATIONSHIP_TYPES = exports.XML_NAMESPACES = void 0;
3
+ exports.CONTENT_TYPES = exports.STRUCT_RELATIONSHIP_TYPES = exports.SECTION_NAMES = exports.SHAPE_TYPES = exports.RELATIONSHIP_TYPES = exports.XML_NAMESPACES = void 0;
4
4
  exports.XML_NAMESPACES = {
5
5
  VISIO_MAIN: 'http://schemas.microsoft.com/office/visio/2012/main',
6
6
  RELATIONSHIPS: 'http://schemas.openxmlformats.org/package/2006/relationships',
@@ -24,6 +24,36 @@ exports.RELATIONSHIP_TYPES = {
24
24
  CORE_PROPERTIES: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
25
25
  EXTENDED_PROPERTIES: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties'
26
26
  };
27
+ /** Visio shape `Type` attribute values. */
28
+ exports.SHAPE_TYPES = {
29
+ Shape: 'Shape',
30
+ Group: 'Group',
31
+ Foreign: 'Foreign',
32
+ };
33
+ /**
34
+ * Visio ShapeSheet section names (the `N` attribute on `<Section>` elements).
35
+ * Used when finding or filtering sections by name in page/master XML.
36
+ */
37
+ exports.SECTION_NAMES = {
38
+ Line: 'Line',
39
+ Fill: 'Fill',
40
+ Character: 'Character',
41
+ Paragraph: 'Paragraph',
42
+ TextBlock: 'TextBlock',
43
+ Geometry: 'Geometry',
44
+ Connection: 'Connection',
45
+ Property: 'Property',
46
+ User: 'User',
47
+ LayerMem: 'LayerMem',
48
+ Layer: 'Layer',
49
+ };
50
+ /**
51
+ * Structural relationship types stored in `<Relationship>` elements
52
+ * inside Visio page XML (distinct from OPC `.rels` relationship types).
53
+ */
54
+ exports.STRUCT_RELATIONSHIP_TYPES = {
55
+ Container: 'Container',
56
+ };
27
57
  exports.CONTENT_TYPES = {
28
58
  PNG: 'image/png',
29
59
  JPEG: 'image/jpeg',
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VisioValidator = void 0;
4
4
  const fast_xml_parser_1 = require("fast-xml-parser");
5
+ const VisioConstants_1 = require("./VisioConstants");
5
6
  // Known Visio section names per Microsoft schema
6
7
  const VALID_SECTION_NAMES = new Set([
7
8
  'Geometry', 'Character', 'Paragraph', 'Tabs', 'Scratch', 'Connection',
@@ -241,7 +242,7 @@ class VisioValidator {
241
242
  validateImageShapes(pkg, pageId, parsed, pagePath, errors, warnings) {
242
243
  const shapes = this.getAllShapes(parsed);
243
244
  for (const shape of shapes) {
244
- if (shape['@_Type'] !== 'Foreign')
245
+ if (shape['@_Type'] !== VisioConstants_1.SHAPE_TYPES.Foreign)
245
246
  continue;
246
247
  if (!shape.ForeignData) {
247
248
  warnings.push(`${pagePath}: Foreign shape ${shape['@_ID']} missing ForeignData`);
@@ -4,6 +4,7 @@ exports.ShapeBuilder = void 0;
4
4
  const StyleHelpers_1 = require("../utils/StyleHelpers");
5
5
  const GeometryBuilder_1 = require("./GeometryBuilder");
6
6
  const ConnectionPointBuilder_1 = require("./ConnectionPointBuilder");
7
+ const VisioConstants_1 = require("../core/VisioConstants");
7
8
  class ShapeBuilder {
8
9
  static createStandardShape(id, props) {
9
10
  // Validate dimensions
@@ -14,7 +15,7 @@ class ShapeBuilder {
14
15
  '@_ID': id,
15
16
  '@_NameU': `Sheet.${id}`,
16
17
  '@_Name': `Sheet.${id}`,
17
- '@_Type': props.type || 'Shape',
18
+ '@_Type': props.type || VisioConstants_1.SHAPE_TYPES.Shape,
18
19
  Cell: [
19
20
  { '@_N': 'PinX', '@_V': props.x.toString() },
20
21
  { '@_N': 'PinY', '@_V': props.y.toString() },
@@ -103,7 +104,7 @@ class ShapeBuilder {
103
104
  }
104
105
  // Add Geometry
105
106
  // Only if NOT a Group AND NOT a Master Instance
106
- if (props.type !== 'Group' && !props.masterId) {
107
+ if (props.type !== VisioConstants_1.SHAPE_TYPES.Group && !props.masterId) {
107
108
  shape.Section.push(GeometryBuilder_1.GeometryBuilder.build(props));
108
109
  }
109
110
  // Handle Text if provided
@@ -2,6 +2,7 @@ import { VisioShape } from '../types/VisioTypes';
2
2
  export declare function createVisioShapeStub(props: {
3
3
  ID: string;
4
4
  Name?: string;
5
+ Type?: string;
5
6
  Text?: string;
6
7
  Cells?: Record<string, string | number>;
7
8
  }): VisioShape;
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createVisioShapeStub = createVisioShapeStub;
4
+ const VisioConstants_1 = require("../core/VisioConstants");
4
5
  function createVisioShapeStub(props) {
5
6
  return {
6
7
  ID: props.ID,
7
8
  Name: props.Name || `Sheet.${props.ID}`,
8
- Type: 'Shape',
9
+ Type: props.Type ?? VisioConstants_1.SHAPE_TYPES.Shape,
9
10
  Text: props.Text,
10
11
  Cells: Object.entries(props.Cells || {}).reduce((acc, [k, v]) => {
11
12
  acc[k] = { N: k, V: v.toString() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-visio",
3
- "version": "1.10.0",
3
+ "version": "1.13.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {