ts-visio 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -67,6 +67,7 @@ class VisioDocument {
67
67
  const pageStub = {
68
68
  ID: newId,
69
69
  Name: name,
70
+ xmlPath: `visio/pages/page${newId}.xml`,
70
71
  Shapes: [],
71
72
  Connects: []
72
73
  };
@@ -76,10 +77,12 @@ class VisioDocument {
76
77
  if (!this._pageCache) {
77
78
  const internalPages = this.pageManager.load();
78
79
  this._pageCache = internalPages.map(p => {
79
- // Adapter for VisioPage interface
80
80
  const pageStub = {
81
81
  ID: p.id.toString(),
82
82
  Name: p.name,
83
+ // Thread the relationship-resolved path so that loaded files
84
+ // with non-sequential page filenames are handled correctly.
85
+ xmlPath: p.xmlPath,
83
86
  Shapes: [],
84
87
  Connects: [],
85
88
  isBackground: p.isBackground,
@@ -99,6 +102,7 @@ class VisioDocument {
99
102
  const pageStub = {
100
103
  ID: newId,
101
104
  Name: name,
105
+ xmlPath: `visio/pages/page${newId}.xml`,
102
106
  Shapes: [],
103
107
  Connects: [],
104
108
  isBackground: true
@@ -112,6 +116,21 @@ class VisioDocument {
112
116
  await this.pageManager.setBackgroundPage(foregroundPage.id, backgroundPage.id);
113
117
  this._pageCache = null;
114
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
+ }
115
134
  async save(filename) {
116
135
  return this.pkg.save(filename);
117
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) {
@@ -24,7 +24,7 @@ class MasterManager {
24
24
  const parsed = this.parser.parse(content);
25
25
  let masterNodes = parsed.Masters ? parsed.Masters.Master : [];
26
26
  if (!Array.isArray(masterNodes)) {
27
- masterNodes = [masterNodes];
27
+ masterNodes = masterNodes ? [masterNodes] : [];
28
28
  }
29
29
  this.masters = masterNodes.map((node) => ({
30
30
  id: node['@_ID'],
@@ -8,9 +8,18 @@ exports.MIME_TYPES = {
8
8
  'jpeg': 'image/jpeg',
9
9
  'gif': 'image/gif',
10
10
  'bmp': 'image/bmp',
11
- 'tiff': 'image/tiff'
11
+ 'tiff': 'image/tiff',
12
+ 'emf': 'image/emf',
13
+ 'wmf': 'image/wmf',
14
+ 'svg': 'image/svg+xml',
15
+ 'ico': 'image/x-icon',
16
+ 'tif': 'image/tiff',
17
+ 'wdp': 'image/vnd.ms-photo'
12
18
  };
13
19
  exports.SUPPORTED_IMAGE_EXTENSIONS = Object.keys(exports.MIME_TYPES);
20
+ // Extensions that are decoded as UTF-8 text; everything else is treated as binary
21
+ // to prevent corruption of EMF, WMF, OLE, and other binary assets in .vsdx archives.
22
+ const TEXT_EXTENSIONS = new Set(['xml', 'rels']);
14
23
  function isBinaryExtension(ext) {
15
- return exports.SUPPORTED_IMAGE_EXTENSIONS.includes(ext.toLowerCase());
24
+ return !TEXT_EXTENSIONS.has(ext.toLowerCase());
16
25
  }
@@ -1,30 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MediaManager = void 0;
4
- const fast_xml_parser_1 = require("fast-xml-parser");
5
4
  const MediaConstants_1 = require("./MediaConstants");
5
+ const XmlHelper_1 = require("../utils/XmlHelper");
6
+ const node_crypto_1 = require("node:crypto");
6
7
  class MediaManager {
7
8
  constructor(pkg) {
8
9
  this.pkg = pkg;
9
10
  this.deduplicationMap = new Map(); // hash -> paths
10
11
  this.indexed = false;
11
- this.parser = new fast_xml_parser_1.XMLParser({
12
- ignoreAttributes: false,
13
- attributeNamePrefix: "@_"
14
- });
15
- this.builder = new fast_xml_parser_1.XMLBuilder({
16
- ignoreAttributes: false,
17
- attributeNamePrefix: "@_",
18
- format: true
19
- });
12
+ this.parser = (0, XmlHelper_1.createXmlParser)();
13
+ this.builder = (0, XmlHelper_1.createXmlBuilder)();
20
14
  }
21
15
  ensureIndex() {
22
16
  if (this.indexed)
23
17
  return;
24
- const crypto = require('crypto');
25
18
  for (const [path, content] of this.pkg.filesMap.entries()) {
26
19
  if (path.startsWith('visio/media/') && Buffer.isBuffer(content)) {
27
- const hash = crypto.createHash('sha1').update(content).digest('hex');
20
+ const hash = (0, node_crypto_1.createHash)('sha1').update(content).digest('hex');
28
21
  // Store relative path suitable for relationships
29
22
  const filename = path.split('/').pop();
30
23
  if (filename) {
@@ -39,8 +32,7 @@ class MediaManager {
39
32
  }
40
33
  addMedia(name, data) {
41
34
  this.ensureIndex();
42
- const crypto = require('crypto');
43
- const hash = crypto.createHash('sha1').update(data).digest('hex');
35
+ const hash = (0, node_crypto_1.createHash)('sha1').update(data).digest('hex');
44
36
  if (this.deduplicationMap.has(hash)) {
45
37
  return this.deduplicationMap.get(hash);
46
38
  }
@@ -73,14 +65,14 @@ class MediaManager {
73
65
  parsed.Types.Default = [parsed.Types.Default];
74
66
  }
75
67
  const defaults = parsed.Types.Default;
76
- const exists = defaults.some((d) => d['@_Extension']?.toLowerCase() === extension);
68
+ const exists = defaults.some((d) => d['@_Extension']?.toLowerCase() === extension.toLowerCase());
77
69
  if (!exists) {
78
70
  defaults.push({
79
71
  '@_Extension': extension,
80
72
  '@_ContentType': this.getContentType(extension)
81
73
  });
82
74
  // Array reference is already mutating object
83
- const newXml = this.builder.build(parsed);
75
+ const newXml = (0, XmlHelper_1.buildXml)(this.builder, parsed);
84
76
  this.pkg.updateFile(ctPath, newXml);
85
77
  }
86
78
  }
@@ -5,7 +5,7 @@ export interface PageEntry {
5
5
  relId: string;
6
6
  xmlPath: string;
7
7
  isBackground: boolean;
8
- backPageId?: number;
8
+ backPageId?: string;
9
9
  }
10
10
  export declare class PageManager {
11
11
  private pkg;
@@ -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
  */
@@ -1,23 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PageManager = void 0;
4
- const fast_xml_parser_1 = require("fast-xml-parser");
5
4
  const VisioConstants_1 = require("./VisioConstants");
6
5
  const RelsManager_1 = require("./RelsManager");
6
+ const XmlHelper_1 = require("../utils/XmlHelper");
7
7
  class PageManager {
8
8
  constructor(pkg) {
9
9
  this.pkg = pkg;
10
10
  this.pages = [];
11
11
  this.loaded = false;
12
- this.parser = new fast_xml_parser_1.XMLParser({
13
- ignoreAttributes: false,
14
- attributeNamePrefix: "@_"
15
- });
16
- this.builder = new fast_xml_parser_1.XMLBuilder({
17
- ignoreAttributes: false,
18
- attributeNamePrefix: "@_",
19
- format: true
20
- });
12
+ this.parser = (0, XmlHelper_1.createXmlParser)();
13
+ this.builder = (0, XmlHelper_1.createXmlBuilder)();
21
14
  this.relsManager = new RelsManager_1.RelsManager(pkg);
22
15
  }
23
16
  load(force = false) {
@@ -72,7 +65,7 @@ class PageManager {
72
65
  const bgAttr = node['@_Background'];
73
66
  const isBackground = bgAttr === 'true' || bgAttr === '1' || bgAttr === true || bgAttr === 1;
74
67
  const backPageAttr = node['@_BackPage'];
75
- const backPageId = backPageAttr ? parseInt(backPageAttr.toString()) : undefined;
68
+ const backPageId = backPageAttr ? backPageAttr.toString() : undefined;
76
69
  return {
77
70
  id: parseInt(node['@_ID']),
78
71
  name: node['@_Name'],
@@ -127,7 +120,7 @@ class PageManager {
127
120
  '@_PartName': `/${relativePath}`,
128
121
  '@_ContentType': VisioConstants_1.CONTENT_TYPES.VISIO_PAGE
129
122
  });
130
- this.pkg.updateFile(ctPath, this.builder.build(parsedCt));
123
+ this.pkg.updateFile(ctPath, (0, XmlHelper_1.buildXml)(this.builder, parsedCt));
131
124
  // 4. Update Relationships (pages.xml -> new page file)
132
125
  // Source is "visio/pages/pages.xml", Target is "page{ID}.xml" (relative to source dir)
133
126
  const rId = await this.relsManager.ensureRelationship('visio/pages/pages.xml', fileName, VisioConstants_1.RELATIONSHIP_TYPES.PAGE);
@@ -142,9 +135,10 @@ class PageManager {
142
135
  parsedPages.Pages.Page.push({
143
136
  '@_ID': newId.toString(),
144
137
  '@_Name': name,
138
+ '@_NameU': name,
145
139
  'Rel': { '@_r:id': rId }
146
140
  });
147
- this.pkg.updateFile(pagesPath, this.builder.build(parsedPages));
141
+ this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsedPages));
148
142
  // Reload to include new page
149
143
  this.load(true);
150
144
  return newId.toString();
@@ -177,6 +171,7 @@ class PageManager {
177
171
  <Cell N="PageDrawSizeType" V="0"/>
178
172
  </PageSheet>
179
173
  <Shapes/>
174
+ <Connects/>
180
175
  </PageContents>`;
181
176
  this.pkg.updateFile(relativePath, pageContent);
182
177
  // 3. Update Content Types
@@ -191,7 +186,7 @@ class PageManager {
191
186
  '@_PartName': `/${relativePath}`,
192
187
  '@_ContentType': VisioConstants_1.CONTENT_TYPES.VISIO_PAGE
193
188
  });
194
- this.pkg.updateFile(ctPath, this.builder.build(parsedCt));
189
+ this.pkg.updateFile(ctPath, (0, XmlHelper_1.buildXml)(this.builder, parsedCt));
195
190
  // 4. Update Relationships
196
191
  const rId = await this.relsManager.ensureRelationship('visio/pages/pages.xml', fileName, VisioConstants_1.RELATIONSHIP_TYPES.PAGE);
197
192
  // 5. Update Pages Index with Background="true"
@@ -205,13 +200,77 @@ class PageManager {
205
200
  parsedPages.Pages.Page.push({
206
201
  '@_ID': newId.toString(),
207
202
  '@_Name': name,
203
+ '@_NameU': name,
208
204
  '@_Background': '1', // Use '1' for boolean true attribute
209
205
  'Rel': { '@_r:id': rId }
210
206
  });
211
- this.pkg.updateFile(pagesPath, this.builder.build(parsedPages));
207
+ this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsedPages));
212
208
  this.load(true);
213
209
  return newId.toString();
214
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
+ }
215
274
  /**
216
275
  * Set a background page for a foreground page
217
276
  */
@@ -237,7 +296,7 @@ class PageManager {
237
296
  if (fgNode) {
238
297
  fgNode['@_BackPage'] = backgroundPageId.toString();
239
298
  }
240
- this.pkg.updateFile(pagesPath, this.builder.build(parsedPages));
299
+ this.pkg.updateFile(pagesPath, (0, XmlHelper_1.buildXml)(this.builder, parsedPages));
241
300
  this.load(true);
242
301
  }
243
302
  }
@@ -1,20 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RelsManager = void 0;
4
- const fast_xml_parser_1 = require("fast-xml-parser");
5
4
  const VisioConstants_1 = require("./VisioConstants");
5
+ const XmlHelper_1 = require("../utils/XmlHelper");
6
6
  class RelsManager {
7
7
  constructor(pkg) {
8
8
  this.pkg = pkg;
9
- this.parser = new fast_xml_parser_1.XMLParser({
10
- ignoreAttributes: false,
11
- attributeNamePrefix: "@_"
12
- });
13
- this.builder = new fast_xml_parser_1.XMLBuilder({
14
- ignoreAttributes: false,
15
- attributeNamePrefix: "@_",
16
- format: true
17
- });
9
+ this.parser = (0, XmlHelper_1.createXmlParser)();
10
+ this.builder = (0, XmlHelper_1.createXmlBuilder)();
18
11
  }
19
12
  getRelsPath(partPath) {
20
13
  // file.xml -> _rels/file.xml.rels
@@ -66,7 +59,7 @@ class RelsManager {
66
59
  '@_Target': target
67
60
  });
68
61
  // Save back
69
- const newXml = this.builder.build(parsed);
62
+ const newXml = (0, XmlHelper_1.buildXml)(this.builder, parsed);
70
63
  this.pkg.updateFile(relsPath, newXml);
71
64
  return newId;
72
65
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { VisioPackage } from './VisioPackage';
2
- export { PageManager } from './PageManager';
2
+ export { PageManager } from './core/PageManager';
3
+ export type { PageEntry } from './core/PageManager';
3
4
  export { ShapeReader } from './ShapeReader';
4
5
  export { ShapeModifier } from './ShapeModifier';
5
6
  export { VisioDocument } from './VisioDocument';
6
7
  export { Page } from './Page';
7
8
  export { Shape } from './Shape';
9
+ export type { ShapeData, ShapeHyperlink } from './Shape';
8
10
  export * from './types/VisioTypes';
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.Shape = exports.Page = exports.VisioDocument = exports.ShapeModifier = exports.ShapeReader = exports.PageManager = exports.VisioPackage = void 0;
18
18
  var VisioPackage_1 = require("./VisioPackage");
19
19
  Object.defineProperty(exports, "VisioPackage", { enumerable: true, get: function () { return VisioPackage_1.VisioPackage; } });
20
- var PageManager_1 = require("./PageManager");
20
+ var PageManager_1 = require("./core/PageManager");
21
21
  Object.defineProperty(exports, "PageManager", { enumerable: true, get: function () { return PageManager_1.PageManager; } });
22
22
  var ShapeReader_1 = require("./ShapeReader");
23
23
  Object.defineProperty(exports, "ShapeReader", { enumerable: true, get: function () { return ShapeReader_1.ShapeReader; } });
@@ -55,9 +55,8 @@ class ConnectorBuilder {
55
55
  }
56
56
  }
57
57
  };
58
- const topShapes = parsed.PageContents.Shapes ?
59
- (Array.isArray(parsed.PageContents.Shapes.Shape) ? parsed.PageContents.Shapes.Shape : [parsed.PageContents.Shapes.Shape])
60
- : [];
58
+ const rawShapes = parsed.PageContents.Shapes?.Shape;
59
+ const topShapes = Array.isArray(rawShapes) ? rawShapes : rawShapes ? [rawShapes] : [];
61
60
  mapHierarchy(topShapes, null);
62
61
  return shapeHierarchy;
63
62
  }
@@ -120,16 +119,16 @@ class ConnectorBuilder {
120
119
  { '@_N': 'ShapePermeableX', '@_V': '0' },
121
120
  { '@_N': 'ShapePermeableY', '@_V': '0' },
122
121
  { '@_N': 'ShapeRouteStyle', '@_V': '1' },
123
- { '@_N': 'ConFixedCode', '@_V': '0' }
122
+ { '@_N': 'ConFixedCode', '@_V': '0' },
123
+ { '@_N': 'BeginArrow', '@_V': beginArrow || '0' },
124
+ { '@_N': 'BeginArrowSize', '@_V': '2' },
125
+ { '@_N': 'EndArrow', '@_V': endArrow || '0' },
126
+ { '@_N': 'EndArrowSize', '@_V': '2' }
124
127
  ],
125
128
  Section: [
126
129
  (0, StyleHelpers_1.createLineSection)({
127
130
  color: '#000000',
128
- weight: '0.01',
129
- beginArrow: beginArrow || '0',
130
- beginArrowSize: '2',
131
- endArrow: endArrow || '0',
132
- endArrowSize: '2'
131
+ weight: '0.01'
133
132
  }),
134
133
  {
135
134
  '@_N': 'Geometry',
@@ -64,18 +64,20 @@ class ContainerBuilder {
64
64
  textXform.Cell = [];
65
65
  if (!Array.isArray(textXform.Cell))
66
66
  textXform.Cell = [textXform.Cell];
67
- const upsertCell = (name, val, unit) => {
67
+ // Extract the shape's current height for use as the static @_V on TxtPinY.
68
+ const heightVal = shape.Cell?.find((c) => c['@_N'] === 'Height')?.['@_V'] ?? '1';
69
+ const upsertCell = (name, formula, unit, val) => {
68
70
  const idx = textXform.Cell.findIndex((c) => c['@_N'] === name);
69
- const cell = { '@_N': name, '@_V': val, '@_U': unit };
71
+ const cell = { '@_N': name, '@_V': val, '@_F': formula, '@_U': unit };
70
72
  if (idx >= 0)
71
73
  textXform.Cell[idx] = cell;
72
74
  else
73
75
  textXform.Cell.push(cell);
74
76
  };
75
- // Move text to top of shape: TxtPinY = Height, TxtLocPinY = TxtHeight
76
- // Removed redundant '*1' from formulas
77
- upsertCell('TxtPinY', 'Height', 'DY');
78
- upsertCell('TxtLocPinY', 'TxtHeight', 'DY');
77
+ // Move text to top of shape: formula drives dynamic sizing; @_V is the static initial value.
78
+ // TxtHeight is Visio-computed, so its static value is left as '0'.
79
+ upsertCell('TxtPinY', 'Height', 'DY', heightVal);
80
+ upsertCell('TxtLocPinY', 'TxtHeight', 'DY', '0');
79
81
  }
80
82
  static makeList(shape, direction = 'vertical') {
81
83
  // 1. Convert basic container to List
@@ -8,14 +8,17 @@ class ForeignShapeBuilder {
8
8
  '@_NameU': `Sheet.${id}`,
9
9
  '@_Name': `Sheet.${id}`,
10
10
  '@_Type': 'Foreign',
11
- ForeignData: { '@_r:id': rId },
11
+ ForeignData: {
12
+ '@_ForeignType': 'Bitmap',
13
+ Rel: { '@_r:id': rId }
14
+ },
12
15
  Cell: [
13
16
  { '@_N': 'PinX', '@_V': props.x.toString() },
14
17
  { '@_N': 'PinY', '@_V': props.y.toString() },
15
18
  { '@_N': 'Width', '@_V': props.width.toString() },
16
19
  { '@_N': 'Height', '@_V': props.height.toString() },
17
- { '@_N': 'LocPinX', '@_V': (props.width / 2).toString() },
18
- { '@_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' }
19
22
  ],
20
23
  Section: [
21
24
  // Foreign shapes typically have no border (LinePattern=0)
@@ -34,9 +37,9 @@ class ForeignShapeBuilder {
34
37
  Cell: [{ '@_N': 'NoFill', '@_V': '1' }], // Images usually don't have a fill behind them
35
38
  Row: [
36
39
  { '@_T': 'MoveTo', '@_IX': '1', Cell: [{ '@_N': 'X', '@_V': '0' }, { '@_N': 'Y', '@_V': '0' }] },
37
- { '@_T': 'LineTo', '@_IX': '2', Cell: [{ '@_N': 'X', '@_V': props.width.toString() }, { '@_N': 'Y', '@_V': '0' }] },
38
- { '@_T': 'LineTo', '@_IX': '3', Cell: [{ '@_N': 'X', '@_V': props.width.toString() }, { '@_N': 'Y', '@_V': props.height.toString() }] },
39
- { '@_T': 'LineTo', '@_IX': '4', Cell: [{ '@_N': 'X', '@_V': '0' }, { '@_N': 'Y', '@_V': props.height.toString() }] },
40
+ { '@_T': 'LineTo', '@_IX': '2', Cell: [{ '@_N': 'X', '@_V': props.width.toString(), '@_F': 'Width' }, { '@_N': 'Y', '@_V': '0' }] },
41
+ { '@_T': 'LineTo', '@_IX': '3', Cell: [{ '@_N': 'X', '@_V': props.width.toString(), '@_F': 'Width' }, { '@_N': 'Y', '@_V': props.height.toString(), '@_F': 'Height' }] },
42
+ { '@_T': 'LineTo', '@_IX': '4', Cell: [{ '@_N': 'X', '@_V': '0' }, { '@_N': 'Y', '@_V': props.height.toString(), '@_F': 'Height' }] },
40
43
  { '@_T': 'LineTo', '@_IX': '5', Cell: [{ '@_N': 'X', '@_V': '0' }, { '@_N': 'Y', '@_V': '0' }] }
41
44
  ]
42
45
  }