ts-visio 1.4.0 → 1.6.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
@@ -18,13 +18,14 @@ Built using specific schema-level abstractions to handle the complex internal st
18
18
  - **Modify Content**: Update text content of shapes.
19
19
  - **Create Shapes**: Rectangles, ellipses, diamonds, rounded rectangles, triangles, parallelograms.
20
20
  - **Connect Shapes**: Dynamic connectors with arrow styles, line styling, and routing (straight / orthogonal / curved).
21
- - **Text Styling**: Font size, font family, bold, color, horizontal/vertical alignment.
21
+ - **Text Styling**: Font size, font family, bold, italic, underline, strikethrough, color, alignment, paragraph spacing, and text margins.
22
22
  - **Shape Transformations**: Rotate, flip (X/Y), and resize shapes via a fluent API.
23
23
  - **Deletion**: Remove shapes and pages cleanly (including orphaned connectors and relationships).
24
24
  - **Lookup API**: Find shapes by ID, predicate, or look up pages by name.
25
25
  - **Read-Back API**: Read custom properties, hyperlinks, and layer assignments from existing shapes.
26
26
  - **Page Size & Orientation**: Set canvas dimensions with named sizes (`Letter`, `A4`, …) or raw inches; rotate between portrait and landscape.
27
27
  - **Document Metadata**: Read and write document properties (title, author, description, keywords, company, dates) via `doc.getMetadata()` / `doc.setMetadata()`.
28
+ - **Named Connection Points**: Define specific ports on shapes (`Top`, `Right`, etc.) and connect to them precisely using `fromPort`/`toPort` on any connector API.
28
29
 
29
30
  Feature gaps are being tracked in [FEATURES.md](./FEATURES.md).
30
31
 
@@ -521,6 +522,94 @@ Fields map to OPC parts: `title`, `author`, `description`, `keywords`, `lastModi
521
522
 
522
523
  ---
523
524
 
525
+ #### 27. Rich Text Formatting
526
+ Italic, underline, strikethrough, paragraph spacing, and text block margins are available both at shape-creation time and via `shape.setStyle()`.
527
+
528
+ ```typescript
529
+ // At creation time
530
+ const shape = await page.addShape({
531
+ text: 'Important',
532
+ x: 2, y: 3, width: 3, height: 1,
533
+ bold: true,
534
+ italic: true,
535
+ underline: true,
536
+ strikethrough: false,
537
+ // Paragraph spacing (in points)
538
+ spaceBefore: 6,
539
+ spaceAfter: 6,
540
+ lineSpacing: 1.5, // 1.5× line height
541
+ // Text block margins (in inches)
542
+ textMarginTop: 0.1,
543
+ textMarginBottom: 0.1,
544
+ textMarginLeft: 0.1,
545
+ textMarginRight: 0.1,
546
+ });
547
+
548
+ // Post-creation via setStyle()
549
+ await shape.setStyle({
550
+ italic: true,
551
+ lineSpacing: 2.0, // double spacing
552
+ textMarginTop: 0.15,
553
+ textMarginLeft: 0.05,
554
+ });
555
+ ```
556
+
557
+ `lineSpacing` is a multiplier: `1.0` = single, `1.5` = 1.5×, `2.0` = double.
558
+ `spaceBefore` / `spaceAfter` are in **points**. Text margins are in **inches**.
559
+
560
+ ---
561
+
562
+ #### 28. Named Connection Points
563
+ Define specific ports on shapes and connect to them precisely instead of relying on edge-intersection.
564
+
565
+ ```typescript
566
+ import { StandardConnectionPoints } from 'ts-visio';
567
+
568
+ // 1. Add connection points at shape-creation time
569
+ const nodeA = await page.addShape({
570
+ text: 'A', x: 2, y: 3, width: 2, height: 1,
571
+ connectionPoints: StandardConnectionPoints.cardinal, // Top, Right, Bottom, Left
572
+ });
573
+
574
+ const nodeB = await page.addShape({
575
+ text: 'B', x: 6, y: 3, width: 2, height: 1,
576
+ connectionPoints: StandardConnectionPoints.cardinal,
577
+ });
578
+
579
+ // 2. Connect using named ports (Right of A → Left of B)
580
+ await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
581
+ { name: 'Right' }, // fromPort
582
+ { name: 'Left' }, // toPort
583
+ );
584
+
585
+ // 3. Fluent Shape API
586
+ await nodeA.connectTo(nodeB, undefined, undefined, undefined,
587
+ { name: 'Right' }, { name: 'Left' });
588
+
589
+ // 4. Add a point to an existing shape by index
590
+ const ix = nodeA.addConnectionPoint({
591
+ name: 'Center',
592
+ xFraction: 0.5, yFraction: 0.5,
593
+ type: 'both',
594
+ });
595
+
596
+ // 5. Connect by zero-based index instead of name
597
+ await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
598
+ { index: 1 }, // Right (IX=1 in cardinal preset)
599
+ { index: 3 }, // Left (IX=3 in cardinal preset)
600
+ );
601
+
602
+ // 6. 'center' target (default behaviour) works alongside named ports
603
+ await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
604
+ 'center', { name: 'Left' });
605
+ ```
606
+
607
+ `StandardConnectionPoints.cardinal` — 4 points: `Top`, `Right`, `Bottom`, `Left`.
608
+ `StandardConnectionPoints.full` — 8 points: cardinal + `TopLeft`, `TopRight`, `BottomRight`, `BottomLeft`.
609
+ Unknown port names fall back gracefully to edge-intersection routing without throwing.
610
+
611
+ ---
612
+
524
613
  ## Examples
525
614
 
526
615
  Check out the [examples](./examples) directory for complete scripts.
package/dist/Page.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { VisioPage, ConnectorStyle, PageOrientation, PageSizeName } from './types/VisioTypes';
1
+ import { VisioPage, ConnectorStyle, PageOrientation, PageSizeName, ConnectionTarget } from './types/VisioTypes';
2
2
  import { VisioPackage } from './VisioPackage';
3
3
  import { ShapeModifier } from './ShapeModifier';
4
4
  import { NewShapeProps } from './types/VisioTypes';
@@ -50,7 +50,7 @@ export declare class Page {
50
50
  */
51
51
  findShapes(predicate: (shape: Shape) => boolean): Shape[];
52
52
  addShape(props: NewShapeProps, parentId?: string): Promise<Shape>;
53
- connectShapes(fromShape: Shape, toShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): Promise<void>;
53
+ connectShapes(fromShape: Shape, toShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): Promise<void>;
54
54
  addImage(data: Buffer, name: string, x: number, y: number, width: number, height: number): Promise<Shape>;
55
55
  addContainer(props: NewShapeProps): Promise<Shape>;
56
56
  addList(props: NewShapeProps, direction?: 'vertical' | 'horizontal'): Promise<Shape>;
package/dist/Page.js CHANGED
@@ -124,8 +124,8 @@ class Page {
124
124
  });
125
125
  return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
126
126
  }
127
- async connectShapes(fromShape, toShape, beginArrow, endArrow, style) {
128
- await this.modifier.addConnector(this.id, fromShape.id, toShape.id, beginArrow, endArrow, style);
127
+ async connectShapes(fromShape, toShape, beginArrow, endArrow, style, fromPort, toPort) {
128
+ await this.modifier.addConnector(this.id, fromShape.id, toShape.id, beginArrow, endArrow, style, fromPort, toPort);
129
129
  }
130
130
  async addImage(data, name, x, y, width, height) {
131
131
  // 1. Upload Media
package/dist/Shape.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { VisioShape, ConnectorStyle } from './types/VisioTypes';
1
+ import { VisioShape, ConnectorStyle, ConnectionTarget, ConnectionPointDef } from './types/VisioTypes';
2
2
  import { VisioPackage } from './VisioPackage';
3
3
  import { ShapeModifier, ShapeStyle } from './ShapeModifier';
4
4
  import { VisioPropType } from './types/VisioTypes';
@@ -30,7 +30,12 @@ export declare class Shape {
30
30
  get x(): number;
31
31
  get y(): number;
32
32
  delete(): Promise<void>;
33
- connectTo(targetShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): Promise<this>;
33
+ connectTo(targetShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): Promise<this>;
34
+ /**
35
+ * Add a connection point to this shape.
36
+ * Returns the zero-based index (IX) of the newly added point.
37
+ */
38
+ addConnectionPoint(point: ConnectionPointDef): number;
34
39
  setStyle(style: ShapeStyle): Promise<this>;
35
40
  placeRightOf(targetShape: Shape, options?: {
36
41
  gap: number;
package/dist/Shape.js CHANGED
@@ -42,10 +42,17 @@ class Shape {
42
42
  async delete() {
43
43
  await this.modifier.deleteShape(this.pageId, this.id);
44
44
  }
45
- async connectTo(targetShape, beginArrow, endArrow, style) {
46
- await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow, style);
45
+ async connectTo(targetShape, beginArrow, endArrow, style, fromPort, toPort) {
46
+ await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow, style, fromPort, toPort);
47
47
  return this;
48
48
  }
49
+ /**
50
+ * Add a connection point to this shape.
51
+ * Returns the zero-based index (IX) of the newly added point.
52
+ */
53
+ addConnectionPoint(point) {
54
+ return this.modifier.addConnectionPoint(this.pageId, this.id, point);
55
+ }
49
56
  async setStyle(style) {
50
57
  await this.modifier.updateShapeStyle(this.pageId, this.id, style);
51
58
  return this;
@@ -1,6 +1,6 @@
1
1
  import { VisioPackage } from './VisioPackage';
2
2
  import { HorzAlign, VertAlign } from './utils/StyleHelpers';
3
- import { NewShapeProps, ConnectorStyle } from './types/VisioTypes';
3
+ import { NewShapeProps, ConnectorStyle, ConnectionTarget, ConnectionPointDef } from './types/VisioTypes';
4
4
  import type { ShapeData, ShapeHyperlink } from './Shape';
5
5
  export declare class ShapeModifier {
6
6
  private pkg;
@@ -31,7 +31,12 @@ export declare class ShapeModifier {
31
31
  private saveParsed;
32
32
  private performSave;
33
33
  flush(): void;
34
- addConnector(pageId: string, fromShapeId: string, toShapeId: string, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): Promise<string>;
34
+ addConnector(pageId: string, fromShapeId: string, toShapeId: string, beginArrow?: string, endArrow?: string, style?: ConnectorStyle, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): Promise<string>;
35
+ /**
36
+ * Add a single connection point to an existing shape.
37
+ * Returns the zero-based IX (row index) of the newly added point.
38
+ */
39
+ addConnectionPoint(pageId: string, shapeId: string, point: ConnectionPointDef): number;
35
40
  addShape(pageId: string, props: NewShapeProps, parentId?: string): Promise<string>;
36
41
  deleteShape(pageId: string, shapeId: string): Promise<void>;
37
42
  private removeShapeFromTree;
@@ -121,6 +126,12 @@ export interface ShapeStyle {
121
126
  fillColor?: string;
122
127
  fontColor?: string;
123
128
  bold?: boolean;
129
+ /** Italic text. */
130
+ italic?: boolean;
131
+ /** Underline text. */
132
+ underline?: boolean;
133
+ /** Strikethrough text. */
134
+ strikethrough?: boolean;
124
135
  /** Font size in points (e.g. 14 for 14pt). */
125
136
  fontSize?: number;
126
137
  /** Font family name (e.g. "Arial"). */
@@ -129,4 +140,18 @@ export interface ShapeStyle {
129
140
  horzAlign?: HorzAlign;
130
141
  /** Vertical text alignment. */
131
142
  verticalAlign?: VertAlign;
143
+ /** Space before each paragraph in **points**. */
144
+ spaceBefore?: number;
145
+ /** Space after each paragraph in **points**. */
146
+ spaceAfter?: number;
147
+ /** Line-height multiplier (1.0 = single, 1.5 = 1.5×, 2.0 = double). */
148
+ lineSpacing?: number;
149
+ /** Top text margin in inches. */
150
+ textMarginTop?: number;
151
+ /** Bottom text margin in inches. */
152
+ textMarginBottom?: number;
153
+ /** Left text margin in inches. */
154
+ textMarginLeft?: number;
155
+ /** Right text margin in inches. */
156
+ textMarginRight?: number;
132
157
  }
@@ -5,6 +5,7 @@ const RelsManager_1 = require("./core/RelsManager");
5
5
  const StyleHelpers_1 = require("./utils/StyleHelpers");
6
6
  const VisioConstants_1 = require("./core/VisioConstants");
7
7
  const VisioTypes_1 = require("./types/VisioTypes");
8
+ const ConnectionPointBuilder_1 = require("./shapes/ConnectionPointBuilder");
8
9
  const ForeignShapeBuilder_1 = require("./shapes/ForeignShapeBuilder");
9
10
  const ShapeBuilder_1 = require("./shapes/ShapeBuilder");
10
11
  const ConnectorBuilder_1 = require("./shapes/ConnectorBuilder");
@@ -184,7 +185,7 @@ class ShapeModifier {
184
185
  }
185
186
  this.dirtyPages.clear();
186
187
  }
187
- async addConnector(pageId, fromShapeId, toShapeId, beginArrow, endArrow, style) {
188
+ async addConnector(pageId, fromShapeId, toShapeId, beginArrow, endArrow, style, fromPort, toPort) {
188
189
  const parsed = this.getParsed(pageId);
189
190
  // Ensure Shapes collection exists
190
191
  if (!parsed.PageContents.Shapes) {
@@ -204,15 +205,45 @@ class ShapeModifier {
204
205
  return '0';
205
206
  return val;
206
207
  };
207
- const layout = ConnectorBuilder_1.ConnectorBuilder.calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy);
208
+ const layout = ConnectorBuilder_1.ConnectorBuilder.calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort);
208
209
  const connectorShape = ConnectorBuilder_1.ConnectorBuilder.createConnectorShapeObject(newId, layout, validateArrow(beginArrow), validateArrow(endArrow), style);
209
210
  const topLevelShapes = parsed.PageContents.Shapes.Shape;
210
211
  topLevelShapes.push(connectorShape);
211
212
  this.getShapeMap(parsed).set(newId, connectorShape);
212
- ConnectorBuilder_1.ConnectorBuilder.addConnectorToConnects(parsed, newId, fromShapeId, toShapeId);
213
+ ConnectorBuilder_1.ConnectorBuilder.addConnectorToConnects(parsed, newId, fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort);
213
214
  this.saveParsed(pageId, parsed);
214
215
  return newId;
215
216
  }
217
+ /**
218
+ * Add a single connection point to an existing shape.
219
+ * Returns the zero-based IX (row index) of the newly added point.
220
+ */
221
+ addConnectionPoint(pageId, shapeId, point) {
222
+ const parsed = this.getParsed(pageId);
223
+ const shapeMap = this.getShapeMap(parsed);
224
+ const shape = shapeMap.get(shapeId);
225
+ if (!shape)
226
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
227
+ // Ensure Section array exists
228
+ if (!shape.Section)
229
+ shape.Section = [];
230
+ if (!Array.isArray(shape.Section))
231
+ shape.Section = [shape.Section];
232
+ // Find or create Connection section
233
+ let connSection = shape.Section.find((s) => s['@_N'] === 'Connection');
234
+ if (!connSection) {
235
+ connSection = { '@_N': 'Connection', Row: [] };
236
+ shape.Section.push(connSection);
237
+ }
238
+ if (!connSection.Row)
239
+ connSection.Row = [];
240
+ if (!Array.isArray(connSection.Row))
241
+ connSection.Row = [connSection.Row];
242
+ const ix = connSection.Row.length;
243
+ connSection.Row.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildRow(point, ix));
244
+ this.saveParsed(pageId, parsed);
245
+ return ix;
246
+ }
216
247
  async addShape(pageId, props, parentId) {
217
248
  const parsed = this.getParsed(pageId);
218
249
  // Ensure Shapes container exists
@@ -349,19 +380,52 @@ class ShapeModifier {
349
380
  shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
350
381
  }
351
382
  // Update/Add Character (Font/Text Style)
352
- if (style.fontColor || style.bold !== undefined || style.fontSize !== undefined || style.fontFamily !== undefined) {
383
+ const hasCharProps = style.fontColor !== undefined
384
+ || style.bold !== undefined
385
+ || style.italic !== undefined
386
+ || style.underline !== undefined
387
+ || style.strikethrough !== undefined
388
+ || style.fontSize !== undefined
389
+ || style.fontFamily !== undefined;
390
+ if (hasCharProps) {
353
391
  shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Character');
354
392
  shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
355
393
  bold: style.bold,
394
+ italic: style.italic,
395
+ underline: style.underline,
396
+ strikethrough: style.strikethrough,
356
397
  color: style.fontColor,
357
398
  fontSize: style.fontSize,
358
399
  fontFamily: style.fontFamily,
359
400
  }));
360
401
  }
361
- // Update/Add Paragraph (Horizontal Alignment)
362
- if (style.horzAlign !== undefined) {
402
+ // Update/Add Paragraph
403
+ const hasParagraphProps = style.horzAlign !== undefined
404
+ || style.spaceBefore !== undefined
405
+ || style.spaceAfter !== undefined
406
+ || style.lineSpacing !== undefined;
407
+ if (hasParagraphProps) {
363
408
  shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Paragraph');
364
- shape.Section.push((0, StyleHelpers_1.createParagraphSection)(style.horzAlign));
409
+ shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
410
+ horzAlign: style.horzAlign,
411
+ spaceBefore: style.spaceBefore,
412
+ spaceAfter: style.spaceAfter,
413
+ lineSpacing: style.lineSpacing,
414
+ }));
415
+ }
416
+ // Update/Add TextBlock (text margins)
417
+ const hasTextBlockProps = style.textMarginTop !== undefined
418
+ || style.textMarginBottom !== undefined
419
+ || style.textMarginLeft !== undefined
420
+ || style.textMarginRight !== undefined;
421
+ if (hasTextBlockProps) {
422
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== 'TextBlock');
423
+ shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
424
+ topMargin: style.textMarginTop,
425
+ bottomMargin: style.textMarginBottom,
426
+ leftMargin: style.textMarginLeft,
427
+ rightMargin: style.textMarginRight,
428
+ }));
365
429
  }
366
430
  // Update/Add VerticalAlign (top-level shape Cell)
367
431
  if (style.verticalAlign !== undefined) {
@@ -0,0 +1,40 @@
1
+ import { ConnectionPointDef, ConnectionTarget } from '../types/VisioTypes';
2
+ /**
3
+ * Builds and resolves Visio Connection section XML structures.
4
+ *
5
+ * Connection points in Visio are stored as rows in a `<Section N="Connection">` element.
6
+ * Each row has an IX attribute (0-based) and an optional N (name) attribute.
7
+ * X and Y cells store fractions via formula: `Width*{xFraction}` / `Height*{yFraction}`.
8
+ *
9
+ * When connecting to a named point, the Connect element uses:
10
+ * ToPart = 100 + IX (instead of 3 for shape centre)
11
+ * ToCell = "Connections.X{IX+1}" (1-based)
12
+ */
13
+ export declare class ConnectionPointBuilder {
14
+ /** Build the raw XML object for a Connection section from a list of point definitions. */
15
+ static buildConnectionSection(points: ConnectionPointDef[]): any;
16
+ /** Build a single Connection row XML object. */
17
+ static buildRow(point: ConnectionPointDef, ix: number): any;
18
+ /**
19
+ * Resolve a `ConnectionTarget` against a raw shape XML object.
20
+ *
21
+ * Returns the `ToPart` / `ToCell` strings for the Connect element, plus the
22
+ * fractional X/Y position of the connection point (undefined when targeting
23
+ * the shape centre, which falls back to edge-intersection logic).
24
+ */
25
+ static resolveTarget(target: ConnectionTarget, shape: any): {
26
+ toPart: string;
27
+ toCell: string;
28
+ xFraction?: number;
29
+ yFraction?: number;
30
+ };
31
+ private static getConnectionSection;
32
+ private static getRows;
33
+ private static findIxByName;
34
+ private static getPointByIx;
35
+ /**
36
+ * Extract the numeric multiplier from a formula of the form `Width*0.5` or `Height*1`.
37
+ * Returns `undefined` if the formula does not match that pattern.
38
+ */
39
+ private static parseFraction;
40
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionPointBuilder = void 0;
4
+ const TYPE_VALUES = {
5
+ inward: '0',
6
+ outward: '1',
7
+ both: '2',
8
+ };
9
+ /**
10
+ * Builds and resolves Visio Connection section XML structures.
11
+ *
12
+ * Connection points in Visio are stored as rows in a `<Section N="Connection">` element.
13
+ * Each row has an IX attribute (0-based) and an optional N (name) attribute.
14
+ * X and Y cells store fractions via formula: `Width*{xFraction}` / `Height*{yFraction}`.
15
+ *
16
+ * When connecting to a named point, the Connect element uses:
17
+ * ToPart = 100 + IX (instead of 3 for shape centre)
18
+ * ToCell = "Connections.X{IX+1}" (1-based)
19
+ */
20
+ class ConnectionPointBuilder {
21
+ /** Build the raw XML object for a Connection section from a list of point definitions. */
22
+ static buildConnectionSection(points) {
23
+ return {
24
+ '@_N': 'Connection',
25
+ Row: points.map((pt, ix) => this.buildRow(pt, ix)),
26
+ };
27
+ }
28
+ /** Build a single Connection row XML object. */
29
+ static buildRow(point, ix) {
30
+ const row = {
31
+ '@_IX': ix.toString(),
32
+ Cell: [
33
+ { '@_N': 'X', '@_V': '0', '@_F': `Width*${point.xFraction}` },
34
+ { '@_N': 'Y', '@_V': '0', '@_F': `Height*${point.yFraction}` },
35
+ { '@_N': 'DirX', '@_V': point.direction ? point.direction.x.toString() : '0' },
36
+ { '@_N': 'DirY', '@_V': point.direction ? point.direction.y.toString() : '0' },
37
+ { '@_N': 'Type', '@_V': point.type ? TYPE_VALUES[point.type] : '0' },
38
+ { '@_N': 'AutoGen', '@_V': '0' },
39
+ ],
40
+ };
41
+ if (point.name)
42
+ row['@_N'] = point.name;
43
+ if (point.prompt)
44
+ row.Cell.push({ '@_N': 'Prompt', '@_V': point.prompt });
45
+ return row;
46
+ }
47
+ /**
48
+ * Resolve a `ConnectionTarget` against a raw shape XML object.
49
+ *
50
+ * Returns the `ToPart` / `ToCell` strings for the Connect element, plus the
51
+ * fractional X/Y position of the connection point (undefined when targeting
52
+ * the shape centre, which falls back to edge-intersection logic).
53
+ */
54
+ static resolveTarget(target, shape) {
55
+ if (target === 'center') {
56
+ return { toPart: '3', toCell: 'PinX' };
57
+ }
58
+ if ('index' in target) {
59
+ const ix = target.index;
60
+ const pt = this.getPointByIx(shape, ix);
61
+ return {
62
+ toPart: (100 + ix).toString(),
63
+ toCell: `Connections.X${ix + 1}`,
64
+ xFraction: pt?.xFraction,
65
+ yFraction: pt?.yFraction,
66
+ };
67
+ }
68
+ // name lookup
69
+ const ix = this.findIxByName(shape, target.name);
70
+ if (ix === -1) {
71
+ // Named point not found — fall back to centre
72
+ return { toPart: '3', toCell: 'PinX' };
73
+ }
74
+ const pt = this.getPointByIx(shape, ix);
75
+ return {
76
+ toPart: (100 + ix).toString(),
77
+ toCell: `Connections.X${ix + 1}`,
78
+ xFraction: pt?.xFraction,
79
+ yFraction: pt?.yFraction,
80
+ };
81
+ }
82
+ // ── private helpers ────────────────────────────────────────────────────────
83
+ static getConnectionSection(shape) {
84
+ const sections = shape?.Section;
85
+ if (!sections)
86
+ return null;
87
+ const arr = Array.isArray(sections) ? sections : [sections];
88
+ return arr.find((s) => s['@_N'] === 'Connection') ?? null;
89
+ }
90
+ static getRows(section) {
91
+ if (!section?.Row)
92
+ return [];
93
+ return Array.isArray(section.Row) ? section.Row : [section.Row];
94
+ }
95
+ static findIxByName(shape, name) {
96
+ const section = this.getConnectionSection(shape);
97
+ if (!section)
98
+ return -1;
99
+ const row = this.getRows(section).find((r) => r['@_N'] === name);
100
+ if (!row)
101
+ return -1;
102
+ return parseInt(row['@_IX'], 10);
103
+ }
104
+ static getPointByIx(shape, ix) {
105
+ const section = this.getConnectionSection(shape);
106
+ if (!section)
107
+ return undefined;
108
+ const row = this.getRows(section).find((r) => parseInt(r['@_IX'], 10) === ix);
109
+ if (!row)
110
+ return undefined;
111
+ const cells = Array.isArray(row.Cell) ? row.Cell : row.Cell ? [row.Cell] : [];
112
+ const xCell = cells.find((c) => c['@_N'] === 'X');
113
+ const yCell = cells.find((c) => c['@_N'] === 'Y');
114
+ // Prefer formula parsing (works for both freshly-created and loaded shapes).
115
+ // Fall back to @_V if no formula is present.
116
+ const xFraction = this.parseFraction(xCell?.['@_F']) ?? parseFloat(xCell?.['@_V'] ?? '0');
117
+ const yFraction = this.parseFraction(yCell?.['@_F']) ?? parseFloat(yCell?.['@_V'] ?? '0');
118
+ return { xFraction, yFraction };
119
+ }
120
+ /**
121
+ * Extract the numeric multiplier from a formula of the form `Width*0.5` or `Height*1`.
122
+ * Returns `undefined` if the formula does not match that pattern.
123
+ */
124
+ static parseFraction(formula) {
125
+ if (!formula)
126
+ return undefined;
127
+ const m = formula.match(/^(?:Width|Height)\*([0-9.]+)$/);
128
+ return m ? parseFloat(m[1]) : undefined;
129
+ }
130
+ }
131
+ exports.ConnectionPointBuilder = ConnectionPointBuilder;
@@ -1,4 +1,4 @@
1
- import { ConnectorStyle } from '../types/VisioTypes';
1
+ import { ConnectorStyle, ConnectionTarget } from '../types/VisioTypes';
2
2
  export declare class ConnectorBuilder {
3
3
  private static getCellVal;
4
4
  private static getAbsolutePos;
@@ -10,7 +10,7 @@ export declare class ConnectorBuilder {
10
10
  static calculateConnectorLayout(fromShapeId: string, toShapeId: string, shapeHierarchy: Map<string, {
11
11
  shape: any;
12
12
  parent: any;
13
- }>): {
13
+ }>, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): {
14
14
  beginX: number;
15
15
  beginY: number;
16
16
  endX: number;
@@ -18,6 +18,12 @@ export declare class ConnectorBuilder {
18
18
  width: number;
19
19
  angle: number;
20
20
  };
21
+ /**
22
+ * Resolve a ConnectionTarget to an absolute page position using the shape hierarchy.
23
+ * Returns null when the target is 'center' or the named/indexed point is not found,
24
+ * signalling the caller to fall back to edge-intersection logic.
25
+ */
26
+ private static resolveConnectionPointPos;
21
27
  static createConnectorShapeObject(id: string, layout: any, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): {
22
28
  '@_ID': string;
23
29
  '@_NameU': string;
@@ -34,5 +40,8 @@ export declare class ConnectorBuilder {
34
40
  })[];
35
41
  Section: import("../utils/StyleHelpers").VisioSection[];
36
42
  };
37
- static addConnectorToConnects(parsed: any, connectorId: string, fromShapeId: string, toShapeId: string): void;
43
+ static addConnectorToConnects(parsed: any, connectorId: string, fromShapeId: string, toShapeId: string, shapeHierarchy?: Map<string, {
44
+ shape: any;
45
+ parent: any;
46
+ }>, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): void;
38
47
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectorBuilder = void 0;
4
4
  const StyleHelpers_1 = require("../utils/StyleHelpers");
5
+ const ConnectionPointBuilder_1 = require("./ConnectionPointBuilder");
5
6
  const ROUTING_VALUES = {
6
7
  straight: '2',
7
8
  orthogonal: '1',
@@ -65,7 +66,7 @@ class ConnectorBuilder {
65
66
  mapHierarchy(topShapes, null);
66
67
  return shapeHierarchy;
67
68
  }
68
- static calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy) {
69
+ static calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort) {
69
70
  let beginX = 0, beginY = 0, endX = 0, endY = 0;
70
71
  let sourceGeom = null;
71
72
  let targetGeom = null;
@@ -88,12 +89,32 @@ class ConnectorBuilder {
88
89
  endY = abs.y;
89
90
  }
90
91
  if (sourceGeom && targetGeom) {
91
- const startNode = this.getEdgePoint(sourceGeom.x, sourceGeom.y, sourceGeom.w, sourceGeom.h, targetGeom.x, targetGeom.y);
92
- const endNode = this.getEdgePoint(targetGeom.x, targetGeom.y, targetGeom.w, targetGeom.h, sourceGeom.x, sourceGeom.y);
93
- beginX = startNode.x;
94
- beginY = startNode.y;
95
- endX = endNode.x;
96
- endY = endNode.y;
92
+ // Compute begin (from-side) endpoint
93
+ const beginPt = fromPort && fromPort !== 'center'
94
+ ? this.resolveConnectionPointPos(fromShapeId, fromPort, shapeHierarchy, sourceGeom)
95
+ : null;
96
+ if (beginPt) {
97
+ beginX = beginPt.x;
98
+ beginY = beginPt.y;
99
+ }
100
+ else {
101
+ const sp = this.getEdgePoint(sourceGeom.x, sourceGeom.y, sourceGeom.w, sourceGeom.h, targetGeom.x, targetGeom.y);
102
+ beginX = sp.x;
103
+ beginY = sp.y;
104
+ }
105
+ // Compute end (to-side) endpoint
106
+ const endPt = toPort && toPort !== 'center'
107
+ ? this.resolveConnectionPointPos(toShapeId, toPort, shapeHierarchy, targetGeom)
108
+ : null;
109
+ if (endPt) {
110
+ endX = endPt.x;
111
+ endY = endPt.y;
112
+ }
113
+ else {
114
+ const ep = this.getEdgePoint(targetGeom.x, targetGeom.y, targetGeom.w, targetGeom.h, sourceGeom.x, sourceGeom.y);
115
+ endX = ep.x;
116
+ endY = ep.y;
117
+ }
97
118
  }
98
119
  const dx = endX - beginX;
99
120
  const dy = endY - beginY;
@@ -101,6 +122,26 @@ class ConnectorBuilder {
101
122
  const angle = Math.atan2(dy, dx);
102
123
  return { beginX, beginY, endX, endY, width, angle };
103
124
  }
125
+ /**
126
+ * Resolve a ConnectionTarget to an absolute page position using the shape hierarchy.
127
+ * Returns null when the target is 'center' or the named/indexed point is not found,
128
+ * signalling the caller to fall back to edge-intersection logic.
129
+ */
130
+ static resolveConnectionPointPos(shapeId, target, shapeHierarchy, geom) {
131
+ const entry = shapeHierarchy.get(shapeId);
132
+ if (!entry)
133
+ return null;
134
+ const resolved = ConnectionPointBuilder_1.ConnectionPointBuilder.resolveTarget(target, entry.shape);
135
+ if (resolved.xFraction === undefined || resolved.yFraction === undefined)
136
+ return null;
137
+ const locPinX = parseFloat(this.getCellVal(entry.shape, 'LocPinX'));
138
+ const locPinY = parseFloat(this.getCellVal(entry.shape, 'LocPinY'));
139
+ const abs = this.getAbsolutePos(shapeId, shapeHierarchy);
140
+ return {
141
+ x: (abs.x - locPinX) + geom.w * resolved.xFraction,
142
+ y: (abs.y - locPinY) + geom.h * resolved.yFraction,
143
+ };
144
+ }
104
145
  static createConnectorShapeObject(id, layout, beginArrow, endArrow, style) {
105
146
  const { beginX, beginY, endX, endY, width, angle } = layout;
106
147
  const routeStyle = style?.routing ? (ROUTING_VALUES[style.routing] ?? '1') : '1';
@@ -151,7 +192,7 @@ class ConnectorBuilder {
151
192
  ]
152
193
  };
153
194
  }
154
- static addConnectorToConnects(parsed, connectorId, fromShapeId, toShapeId) {
195
+ static addConnectorToConnects(parsed, connectorId, fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort) {
155
196
  if (!parsed.PageContents.Connects) {
156
197
  parsed.PageContents.Connects = { Connect: [] };
157
198
  }
@@ -161,21 +202,43 @@ class ConnectorBuilder {
161
202
  connectCollection = connectCollection ? [connectCollection] : [];
162
203
  parsed.PageContents.Connects.Connect = connectCollection;
163
204
  }
205
+ // Resolve from-side ToPart/ToCell
206
+ let fromToCell = 'PinX';
207
+ let fromToPart = '3';
208
+ if (fromPort && fromPort !== 'center' && shapeHierarchy) {
209
+ const fromEntry = shapeHierarchy.get(fromShapeId);
210
+ if (fromEntry) {
211
+ const r = ConnectionPointBuilder_1.ConnectionPointBuilder.resolveTarget(fromPort, fromEntry.shape);
212
+ fromToCell = r.toCell;
213
+ fromToPart = r.toPart;
214
+ }
215
+ }
216
+ // Resolve to-side ToPart/ToCell
217
+ let toToCell = 'PinX';
218
+ let toToPart = '3';
219
+ if (toPort && toPort !== 'center' && shapeHierarchy) {
220
+ const toEntry = shapeHierarchy.get(toShapeId);
221
+ if (toEntry) {
222
+ const r = ConnectionPointBuilder_1.ConnectionPointBuilder.resolveTarget(toPort, toEntry.shape);
223
+ toToCell = r.toCell;
224
+ toToPart = r.toPart;
225
+ }
226
+ }
164
227
  connectCollection.push({
165
228
  '@_FromSheet': connectorId,
166
229
  '@_FromCell': 'BeginX',
167
230
  '@_FromPart': '9',
168
231
  '@_ToSheet': fromShapeId,
169
- '@_ToCell': 'PinX',
170
- '@_ToPart': '3'
232
+ '@_ToCell': fromToCell,
233
+ '@_ToPart': fromToPart,
171
234
  });
172
235
  connectCollection.push({
173
236
  '@_FromSheet': connectorId,
174
237
  '@_FromCell': 'EndX',
175
238
  '@_FromPart': '12',
176
239
  '@_ToSheet': toShapeId,
177
- '@_ToCell': 'PinX',
178
- '@_ToPart': '3'
240
+ '@_ToCell': toToCell,
241
+ '@_ToPart': toToPart,
179
242
  });
180
243
  }
181
244
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ShapeBuilder = void 0;
4
4
  const StyleHelpers_1 = require("../utils/StyleHelpers");
5
5
  const GeometryBuilder_1 = require("./GeometryBuilder");
6
+ const ConnectionPointBuilder_1 = require("./ConnectionPointBuilder");
6
7
  class ShapeBuilder {
7
8
  static createStandardShape(id, props) {
8
9
  // Validate dimensions
@@ -37,20 +38,55 @@ class ShapeBuilder {
37
38
  weight: '0.01'
38
39
  }));
39
40
  }
40
- if (props.fontColor || props.bold || props.fontSize !== undefined || props.fontFamily !== undefined) {
41
+ const hasCharProps = props.fontColor !== undefined
42
+ || props.bold !== undefined
43
+ || props.italic !== undefined
44
+ || props.underline !== undefined
45
+ || props.strikethrough !== undefined
46
+ || props.fontSize !== undefined
47
+ || props.fontFamily !== undefined;
48
+ if (hasCharProps) {
41
49
  shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
42
50
  bold: props.bold,
51
+ italic: props.italic,
52
+ underline: props.underline,
53
+ strikethrough: props.strikethrough,
43
54
  color: props.fontColor,
44
55
  fontSize: props.fontSize,
45
56
  fontFamily: props.fontFamily,
46
57
  }));
47
58
  }
48
- if (props.horzAlign !== undefined) {
49
- shape.Section.push((0, StyleHelpers_1.createParagraphSection)(props.horzAlign));
59
+ const hasParagraphProps = props.horzAlign !== undefined
60
+ || props.spaceBefore !== undefined
61
+ || props.spaceAfter !== undefined
62
+ || props.lineSpacing !== undefined;
63
+ if (hasParagraphProps) {
64
+ shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
65
+ horzAlign: props.horzAlign,
66
+ spaceBefore: props.spaceBefore,
67
+ spaceAfter: props.spaceAfter,
68
+ lineSpacing: props.lineSpacing,
69
+ }));
70
+ }
71
+ const hasTextBlockProps = props.textMarginTop !== undefined
72
+ || props.textMarginBottom !== undefined
73
+ || props.textMarginLeft !== undefined
74
+ || props.textMarginRight !== undefined;
75
+ if (hasTextBlockProps) {
76
+ shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
77
+ topMargin: props.textMarginTop,
78
+ bottomMargin: props.textMarginBottom,
79
+ leftMargin: props.textMarginLeft,
80
+ rightMargin: props.textMarginRight,
81
+ }));
50
82
  }
51
83
  if (props.verticalAlign !== undefined) {
52
84
  shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(props.verticalAlign) });
53
85
  }
86
+ // Connection points
87
+ if (props.connectionPoints && props.connectionPoints.length > 0) {
88
+ shape.Section.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildConnectionSection(props.connectionPoints));
89
+ }
54
90
  // Add Geometry
55
91
  // Only if NOT a Group AND NOT a Master Instance
56
92
  if (props.type !== 'Group' && !props.masterId) {
@@ -146,6 +146,48 @@ export interface ConnectorStyle {
146
146
  }
147
147
  /** Non-rectangular geometry variants supported by ShapeBuilder. */
148
148
  export type ShapeGeometry = 'rectangle' | 'ellipse' | 'diamond' | 'rounded-rectangle' | 'triangle' | 'parallelogram';
149
+ /** Whether a connection point accepts incoming glue, outgoing glue, or both. */
150
+ export type ConnectionPointType = 'inward' | 'outward' | 'both';
151
+ /**
152
+ * Definition of a single connection point on a shape.
153
+ * Positions are expressed as fractions of the shape's width/height
154
+ * (0.0 = left/bottom, 1.0 = right/top in Visio's coordinate system).
155
+ */
156
+ export interface ConnectionPointDef {
157
+ /** Optional display name (e.g. `'Top'`, `'Right'`). Referenced when connecting by name. */
158
+ name?: string;
159
+ /** Horizontal position as a fraction of shape width. 0 = left edge, 1 = right edge. */
160
+ xFraction: number;
161
+ /** Vertical position as a fraction of shape height. 0 = bottom edge, 1 = top edge. */
162
+ yFraction: number;
163
+ /** Glue direction vector. Defaults to `{ x: 0, y: 0 }` (no preferred direction). */
164
+ direction?: {
165
+ x: number;
166
+ y: number;
167
+ };
168
+ /** Connection point type. Defaults to `'inward'`. */
169
+ type?: ConnectionPointType;
170
+ /** Optional tooltip shown in the Visio UI. */
171
+ prompt?: string;
172
+ }
173
+ /**
174
+ * Specifies which connection point to use when attaching a connector endpoint.
175
+ * - `'center'` → shape centre (default behaviour, `ToPart=3`)
176
+ * - `{ name }` → named connection point (e.g. `{ name: 'Top' }`)
177
+ * - `{ index }` → connection point by zero-based row index
178
+ */
179
+ export type ConnectionTarget = 'center' | {
180
+ name: string;
181
+ } | {
182
+ index: number;
183
+ };
184
+ /** Ready-made connection-point presets. */
185
+ export declare const StandardConnectionPoints: {
186
+ /** Four cardinal points: Top, Right, Bottom, Left. */
187
+ cardinal: ConnectionPointDef[];
188
+ /** Eight points: four cardinal + four corners. */
189
+ full: ConnectionPointDef[];
190
+ };
149
191
  export interface NewShapeProps {
150
192
  text: string;
151
193
  x: number;
@@ -172,4 +214,30 @@ export interface NewShapeProps {
172
214
  masterId?: string;
173
215
  imgRelId?: string;
174
216
  lineColor?: string;
217
+ /** Italic text. */
218
+ italic?: boolean;
219
+ /** Underline text. */
220
+ underline?: boolean;
221
+ /** Strikethrough text. */
222
+ strikethrough?: boolean;
223
+ /** Space before each paragraph in **points**. */
224
+ spaceBefore?: number;
225
+ /** Space after each paragraph in **points**. */
226
+ spaceAfter?: number;
227
+ /** Line-height multiplier (1.0 = single, 1.5 = 1.5×, 2.0 = double). */
228
+ lineSpacing?: number;
229
+ /** Top text margin in inches. */
230
+ textMarginTop?: number;
231
+ /** Bottom text margin in inches. */
232
+ textMarginBottom?: number;
233
+ /** Left text margin in inches. */
234
+ textMarginLeft?: number;
235
+ /** Right text margin in inches. */
236
+ textMarginRight?: number;
237
+ /**
238
+ * Connection points to add to the shape.
239
+ * Use `StandardConnectionPoints.cardinal` for the four cardinal points,
240
+ * or `StandardConnectionPoints.full` for eight points.
241
+ */
242
+ connectionPoints?: ConnectionPointDef[];
175
243
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PageSizes = exports.VisioPropType = void 0;
3
+ exports.StandardConnectionPoints = exports.PageSizes = exports.VisioPropType = void 0;
4
4
  var VisioPropType;
5
5
  (function (VisioPropType) {
6
6
  VisioPropType[VisioPropType["String"] = 0] = "String";
@@ -21,3 +21,22 @@ exports.PageSizes = {
21
21
  A4: { width: 8.268, height: 11.693 },
22
22
  A5: { width: 5.827, height: 8.268 },
23
23
  };
24
+ /** Ready-made connection-point presets. */
25
+ exports.StandardConnectionPoints = {
26
+ cardinal: [
27
+ { name: 'Top', xFraction: 0.5, yFraction: 1.0, direction: { x: 0, y: 1 } },
28
+ { name: 'Right', xFraction: 1.0, yFraction: 0.5, direction: { x: 1, y: 0 } },
29
+ { name: 'Bottom', xFraction: 0.5, yFraction: 0.0, direction: { x: 0, y: -1 } },
30
+ { name: 'Left', xFraction: 0.0, yFraction: 0.5, direction: { x: -1, y: 0 } },
31
+ ],
32
+ full: [
33
+ { name: 'Top', xFraction: 0.5, yFraction: 1.0, direction: { x: 0, y: 1 } },
34
+ { name: 'Right', xFraction: 1.0, yFraction: 0.5, direction: { x: 1, y: 0 } },
35
+ { name: 'Bottom', xFraction: 0.5, yFraction: 0.0, direction: { x: 0, y: -1 } },
36
+ { name: 'Left', xFraction: 0.0, yFraction: 0.5, direction: { x: -1, y: 0 } },
37
+ { name: 'TopLeft', xFraction: 0.0, yFraction: 1.0, direction: { x: -1, y: 1 } },
38
+ { name: 'TopRight', xFraction: 1.0, yFraction: 1.0, direction: { x: 1, y: 1 } },
39
+ { name: 'BottomRight', xFraction: 1.0, yFraction: 0.0, direction: { x: 1, y: -1 } },
40
+ { name: 'BottomLeft', xFraction: 0.0, yFraction: 0.0, direction: { x: -1, y: -1 } },
41
+ ],
42
+ };
@@ -32,13 +32,50 @@ export declare function horzAlignValue(align: HorzAlign): string;
32
32
  export declare function vertAlignValue(align: VertAlign): string;
33
33
  export declare function createCharacterSection(props: {
34
34
  bold?: boolean;
35
+ /** Italic text (Style bit 2). */
36
+ italic?: boolean;
37
+ /** Underline text (Style bit 4). */
38
+ underline?: boolean;
39
+ /** Strikethrough text (Style bit 8). */
40
+ strikethrough?: boolean;
35
41
  color?: string;
36
42
  /** Font size in points (e.g. 12 for 12pt). Stored internally as inches (pt / 72). */
37
43
  fontSize?: number;
38
44
  /** Font family name (e.g. "Arial"). Uses FONT() formula for portability. */
39
45
  fontFamily?: string;
40
46
  }): VisioSection;
41
- export declare function createParagraphSection(horzAlign: HorzAlign): VisioSection;
47
+ export interface ParagraphProps {
48
+ /** Horizontal text alignment within the paragraph. */
49
+ horzAlign?: HorzAlign;
50
+ /**
51
+ * Space before each paragraph in **points**.
52
+ * Converted to inches internally (pt / 72).
53
+ */
54
+ spaceBefore?: number;
55
+ /**
56
+ * Space after each paragraph in **points**.
57
+ * Converted to inches internally (pt / 72).
58
+ */
59
+ spaceAfter?: number;
60
+ /**
61
+ * Line-height multiplier (1.0 = single, 1.5 = 1.5×, 2.0 = double).
62
+ * Stored as a negative value in Visio's `SpLine` cell (negative means
63
+ * proportional; positive means absolute in inches).
64
+ */
65
+ lineSpacing?: number;
66
+ }
67
+ export declare function createParagraphSection(props: ParagraphProps): VisioSection;
68
+ export interface TextBlockProps {
69
+ /** Top text margin in inches. */
70
+ topMargin?: number;
71
+ /** Bottom text margin in inches. */
72
+ bottomMargin?: number;
73
+ /** Left text margin in inches. */
74
+ leftMargin?: number;
75
+ /** Right text margin in inches. */
76
+ rightMargin?: number;
77
+ }
78
+ export declare function createTextBlockSection(props: TextBlockProps): VisioSection;
42
79
  export declare function createLineSection(props: {
43
80
  color?: string;
44
81
  pattern?: string;
@@ -6,6 +6,7 @@ exports.horzAlignValue = horzAlignValue;
6
6
  exports.vertAlignValue = vertAlignValue;
7
7
  exports.createCharacterSection = createCharacterSection;
8
8
  exports.createParagraphSection = createParagraphSection;
9
+ exports.createTextBlockSection = createTextBlockSection;
9
10
  exports.createLineSection = createLineSection;
10
11
  const hexToRgb = (hex) => {
11
12
  // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
@@ -57,9 +58,14 @@ function vertAlignValue(align) {
57
58
  }
58
59
  function createCharacterSection(props) {
59
60
  let styleVal = 0;
60
- if (props.bold) {
61
- styleVal += 1; // Bold bit
62
- }
61
+ if (props.bold)
62
+ styleVal |= 1;
63
+ if (props.italic)
64
+ styleVal |= 2;
65
+ if (props.underline)
66
+ styleVal |= 4;
67
+ if (props.strikethrough)
68
+ styleVal |= 8;
63
69
  const colorVal = props.color || '#000000';
64
70
  const cells = [
65
71
  { '@_N': 'Color', '@_V': colorVal, '@_F': hexToRgb(colorVal) },
@@ -89,20 +95,47 @@ function createCharacterSection(props) {
89
95
  ]
90
96
  };
91
97
  }
92
- function createParagraphSection(horzAlign) {
98
+ function createParagraphSection(props) {
99
+ const cells = [];
100
+ if (props.horzAlign !== undefined) {
101
+ cells.push({ '@_N': 'HorzAlign', '@_V': HORZ_ALIGN_VALUES[props.horzAlign] });
102
+ }
103
+ if (props.spaceBefore !== undefined) {
104
+ cells.push({ '@_N': 'SpBefore', '@_V': (props.spaceBefore / 72).toString(), '@_U': 'PT' });
105
+ }
106
+ if (props.spaceAfter !== undefined) {
107
+ cells.push({ '@_N': 'SpAfter', '@_V': (props.spaceAfter / 72).toString(), '@_U': 'PT' });
108
+ }
109
+ if (props.lineSpacing !== undefined) {
110
+ // Negative value = proportional multiplier; positive = absolute (inches)
111
+ cells.push({ '@_N': 'SpLine', '@_V': (-props.lineSpacing).toString() });
112
+ }
93
113
  return {
94
114
  '@_N': 'Paragraph',
95
115
  Row: [
96
116
  {
97
117
  '@_T': 'Paragraph',
98
118
  '@_IX': '0',
99
- Cell: [
100
- { '@_N': 'HorzAlign', '@_V': HORZ_ALIGN_VALUES[horzAlign] },
101
- ]
119
+ Cell: cells,
102
120
  }
103
121
  ]
104
122
  };
105
123
  }
124
+ function createTextBlockSection(props) {
125
+ const cells = [];
126
+ if (props.topMargin !== undefined)
127
+ cells.push({ '@_N': 'TopMargin', '@_V': props.topMargin.toString(), '@_U': 'IN' });
128
+ if (props.bottomMargin !== undefined)
129
+ cells.push({ '@_N': 'BottomMargin', '@_V': props.bottomMargin.toString(), '@_U': 'IN' });
130
+ if (props.leftMargin !== undefined)
131
+ cells.push({ '@_N': 'LeftMargin', '@_V': props.leftMargin.toString(), '@_U': 'IN' });
132
+ if (props.rightMargin !== undefined)
133
+ cells.push({ '@_N': 'RightMargin', '@_V': props.rightMargin.toString(), '@_U': 'IN' });
134
+ return {
135
+ '@_N': 'TextBlock',
136
+ Cell: cells,
137
+ };
138
+ }
106
139
  function createLineSection(props) {
107
140
  const cells = [
108
141
  { '@_N': 'LineColor', '@_V': props.color || '#000000' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-visio",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {