ts-visio 1.3.0 → 1.5.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,12 +18,13 @@ 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
+ - **Document Metadata**: Read and write document properties (title, author, description, keywords, company, dates) via `doc.getMetadata()` / `doc.setMetadata()`.
27
28
 
28
29
  Feature gaps are being tracked in [FEATURES.md](./FEATURES.md).
29
30
 
@@ -490,6 +491,73 @@ Available named sizes in `PageSizes`: `Letter`, `Legal`, `Tabloid`, `A3`, `A4`,
490
491
 
491
492
  ---
492
493
 
494
+ #### 26. Document Metadata
495
+ Set and read document-level properties that appear in Visio's Document Properties dialog.
496
+
497
+ ```typescript
498
+ // Write metadata (only supplied fields are changed)
499
+ doc.setMetadata({
500
+ title: 'Network Topology',
501
+ author: 'Alice',
502
+ description: 'Data-centre interconnect diagram',
503
+ keywords: 'network datacenter cloud',
504
+ lastModifiedBy: 'CI pipeline',
505
+ company: 'ACME Corp',
506
+ manager: 'Bob',
507
+ created: new Date('2025-01-01T00:00:00Z'),
508
+ modified: new Date(),
509
+ });
510
+
511
+ // Read back all metadata fields
512
+ const meta = doc.getMetadata();
513
+ console.log(meta.title); // 'Network Topology'
514
+ console.log(meta.author); // 'Alice'
515
+ console.log(meta.company); // 'ACME Corp'
516
+ console.log(meta.created); // Date object
517
+ ```
518
+
519
+ Fields map to OPC parts: `title`, `author`, `description`, `keywords`, `lastModifiedBy`,
520
+ `created`, `modified` → `docProps/core.xml`; `company`, `manager` → `docProps/app.xml`.
521
+
522
+ ---
523
+
524
+ #### 27. Rich Text Formatting
525
+ Italic, underline, strikethrough, paragraph spacing, and text block margins are available both at shape-creation time and via `shape.setStyle()`.
526
+
527
+ ```typescript
528
+ // At creation time
529
+ const shape = await page.addShape({
530
+ text: 'Important',
531
+ x: 2, y: 3, width: 3, height: 1,
532
+ bold: true,
533
+ italic: true,
534
+ underline: true,
535
+ strikethrough: false,
536
+ // Paragraph spacing (in points)
537
+ spaceBefore: 6,
538
+ spaceAfter: 6,
539
+ lineSpacing: 1.5, // 1.5× line height
540
+ // Text block margins (in inches)
541
+ textMarginTop: 0.1,
542
+ textMarginBottom: 0.1,
543
+ textMarginLeft: 0.1,
544
+ textMarginRight: 0.1,
545
+ });
546
+
547
+ // Post-creation via setStyle()
548
+ await shape.setStyle({
549
+ italic: true,
550
+ lineSpacing: 2.0, // double spacing
551
+ textMarginTop: 0.15,
552
+ textMarginLeft: 0.05,
553
+ });
554
+ ```
555
+
556
+ `lineSpacing` is a multiplier: `1.0` = single, `1.5` = 1.5×, `2.0` = double.
557
+ `spaceBefore` / `spaceAfter` are in **points**. Text margins are in **inches**.
558
+
559
+ ---
560
+
493
561
  ## Examples
494
562
 
495
563
  Check out the [examples](./examples) directory for complete scripts.
@@ -121,6 +121,12 @@ export interface ShapeStyle {
121
121
  fillColor?: string;
122
122
  fontColor?: string;
123
123
  bold?: boolean;
124
+ /** Italic text. */
125
+ italic?: boolean;
126
+ /** Underline text. */
127
+ underline?: boolean;
128
+ /** Strikethrough text. */
129
+ strikethrough?: boolean;
124
130
  /** Font size in points (e.g. 14 for 14pt). */
125
131
  fontSize?: number;
126
132
  /** Font family name (e.g. "Arial"). */
@@ -129,4 +135,18 @@ export interface ShapeStyle {
129
135
  horzAlign?: HorzAlign;
130
136
  /** Vertical text alignment. */
131
137
  verticalAlign?: VertAlign;
138
+ /** Space before each paragraph in **points**. */
139
+ spaceBefore?: number;
140
+ /** Space after each paragraph in **points**. */
141
+ spaceAfter?: number;
142
+ /** Line-height multiplier (1.0 = single, 1.5 = 1.5×, 2.0 = double). */
143
+ lineSpacing?: number;
144
+ /** Top text margin in inches. */
145
+ textMarginTop?: number;
146
+ /** Bottom text margin in inches. */
147
+ textMarginBottom?: number;
148
+ /** Left text margin in inches. */
149
+ textMarginLeft?: number;
150
+ /** Right text margin in inches. */
151
+ textMarginRight?: number;
132
152
  }
@@ -349,19 +349,52 @@ class ShapeModifier {
349
349
  shape.Section.push((0, StyleHelpers_1.createFillSection)(style.fillColor));
350
350
  }
351
351
  // Update/Add Character (Font/Text Style)
352
- if (style.fontColor || style.bold !== undefined || style.fontSize !== undefined || style.fontFamily !== undefined) {
352
+ const hasCharProps = style.fontColor !== undefined
353
+ || style.bold !== undefined
354
+ || style.italic !== undefined
355
+ || style.underline !== undefined
356
+ || style.strikethrough !== undefined
357
+ || style.fontSize !== undefined
358
+ || style.fontFamily !== undefined;
359
+ if (hasCharProps) {
353
360
  shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Character');
354
361
  shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
355
362
  bold: style.bold,
363
+ italic: style.italic,
364
+ underline: style.underline,
365
+ strikethrough: style.strikethrough,
356
366
  color: style.fontColor,
357
367
  fontSize: style.fontSize,
358
368
  fontFamily: style.fontFamily,
359
369
  }));
360
370
  }
361
- // Update/Add Paragraph (Horizontal Alignment)
362
- if (style.horzAlign !== undefined) {
371
+ // Update/Add Paragraph
372
+ const hasParagraphProps = style.horzAlign !== undefined
373
+ || style.spaceBefore !== undefined
374
+ || style.spaceAfter !== undefined
375
+ || style.lineSpacing !== undefined;
376
+ if (hasParagraphProps) {
363
377
  shape.Section = shape.Section.filter((s) => s['@_N'] !== 'Paragraph');
364
- shape.Section.push((0, StyleHelpers_1.createParagraphSection)(style.horzAlign));
378
+ shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
379
+ horzAlign: style.horzAlign,
380
+ spaceBefore: style.spaceBefore,
381
+ spaceAfter: style.spaceAfter,
382
+ lineSpacing: style.lineSpacing,
383
+ }));
384
+ }
385
+ // Update/Add TextBlock (text margins)
386
+ const hasTextBlockProps = style.textMarginTop !== undefined
387
+ || style.textMarginBottom !== undefined
388
+ || style.textMarginLeft !== undefined
389
+ || style.textMarginRight !== undefined;
390
+ if (hasTextBlockProps) {
391
+ shape.Section = shape.Section.filter((s) => s['@_N'] !== 'TextBlock');
392
+ shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
393
+ topMargin: style.textMarginTop,
394
+ bottomMargin: style.textMarginBottom,
395
+ leftMargin: style.textMarginLeft,
396
+ rightMargin: style.textMarginRight,
397
+ }));
365
398
  }
366
399
  // Update/Add VerticalAlign (top-level shape Cell)
367
400
  if (style.verticalAlign !== undefined) {
@@ -1,9 +1,11 @@
1
1
  import { Page } from './Page';
2
+ import { DocumentMetadata } from './types/VisioTypes';
2
3
  export declare class VisioDocument {
3
4
  private pkg;
4
5
  private pageManager;
5
6
  private _pageCache;
6
7
  private mediaManager;
8
+ private metadataManager;
7
9
  private constructor();
8
10
  static create(): Promise<VisioDocument>;
9
11
  static load(pathOrBuffer: string | Buffer | ArrayBuffer | Uint8Array): Promise<VisioDocument>;
@@ -27,5 +29,18 @@ export declare class VisioDocument {
27
29
  * and any BackPage references from other pages.
28
30
  */
29
31
  deletePage(page: Page): Promise<void>;
32
+ /**
33
+ * Read document metadata from `docProps/core.xml` and `docProps/app.xml`.
34
+ * Fields not present in the file are returned as `undefined`.
35
+ */
36
+ getMetadata(): DocumentMetadata;
37
+ /**
38
+ * Write document metadata. Only the supplied fields are changed;
39
+ * all other fields keep their existing values.
40
+ *
41
+ * @example
42
+ * doc.setMetadata({ title: 'My Diagram', author: 'Alice', company: 'ACME' });
43
+ */
44
+ setMetadata(props: Partial<DocumentMetadata>): void;
30
45
  save(filename?: string): Promise<Buffer>;
31
46
  }
@@ -38,12 +38,14 @@ const VisioPackage_1 = require("./VisioPackage");
38
38
  const PageManager_1 = require("./core/PageManager");
39
39
  const Page_1 = require("./Page");
40
40
  const MediaManager_1 = require("./core/MediaManager");
41
+ const MetadataManager_1 = require("./core/MetadataManager");
41
42
  class VisioDocument {
42
43
  constructor(pkg) {
43
44
  this.pkg = pkg;
44
45
  this._pageCache = null;
45
46
  this.pageManager = new PageManager_1.PageManager(pkg);
46
47
  this.mediaManager = new MediaManager_1.MediaManager(pkg);
48
+ this.metadataManager = new MetadataManager_1.MetadataManager(pkg);
47
49
  }
48
50
  static async create() {
49
51
  const pkg = await VisioPackage_1.VisioPackage.create();
@@ -131,6 +133,23 @@ class VisioDocument {
131
133
  await this.pageManager.deletePage(page.id);
132
134
  this._pageCache = null;
133
135
  }
136
+ /**
137
+ * Read document metadata from `docProps/core.xml` and `docProps/app.xml`.
138
+ * Fields not present in the file are returned as `undefined`.
139
+ */
140
+ getMetadata() {
141
+ return this.metadataManager.getMetadata();
142
+ }
143
+ /**
144
+ * Write document metadata. Only the supplied fields are changed;
145
+ * all other fields keep their existing values.
146
+ *
147
+ * @example
148
+ * doc.setMetadata({ title: 'My Diagram', author: 'Alice', company: 'ACME' });
149
+ */
150
+ setMetadata(props) {
151
+ this.metadataManager.setMetadata(props);
152
+ }
134
153
  async save(filename) {
135
154
  return this.pkg.save(filename);
136
155
  }
@@ -0,0 +1,22 @@
1
+ import { VisioPackage } from '../VisioPackage';
2
+ import { DocumentMetadata } from '../types/VisioTypes';
3
+ export declare class MetadataManager {
4
+ private pkg;
5
+ private parser;
6
+ private builder;
7
+ constructor(pkg: VisioPackage);
8
+ /** Read document metadata from `docProps/core.xml` and `docProps/app.xml`. */
9
+ getMetadata(): DocumentMetadata;
10
+ /** Merge the supplied fields into the existing metadata and persist to the package. */
11
+ setMetadata(props: Partial<DocumentMetadata>): void;
12
+ /** Extract a string from a parsed XML node (handles plain strings and #text objects). */
13
+ private str;
14
+ /** Parse an ISO datetime string into a Date (returns undefined on failure). */
15
+ private date;
16
+ private parsedCore;
17
+ private parsedApp;
18
+ private writeCore;
19
+ private writeApp;
20
+ private blankCoreParsed;
21
+ private blankAppParsed;
22
+ }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetadataManager = void 0;
4
+ const VisioConstants_1 = require("./VisioConstants");
5
+ const XmlHelper_1 = require("../utils/XmlHelper");
6
+ class MetadataManager {
7
+ constructor(pkg) {
8
+ this.pkg = pkg;
9
+ this.parser = (0, XmlHelper_1.createXmlParser)();
10
+ this.builder = (0, XmlHelper_1.createXmlBuilder)();
11
+ }
12
+ // ---- public API --------------------------------------------------------
13
+ /** Read document metadata from `docProps/core.xml` and `docProps/app.xml`. */
14
+ getMetadata() {
15
+ const core = this.parsedCore();
16
+ const app = this.parsedApp();
17
+ return {
18
+ title: this.str(core['dc:title']),
19
+ author: this.str(core['dc:creator']),
20
+ description: this.str(core['dc:description']),
21
+ keywords: this.str(core['cp:keywords']),
22
+ lastModifiedBy: this.str(core['cp:lastModifiedBy']),
23
+ created: this.date(core['dcterms:created']),
24
+ modified: this.date(core['dcterms:modified']),
25
+ company: this.str(app['Company']),
26
+ manager: this.str(app['Manager']),
27
+ };
28
+ }
29
+ /** Merge the supplied fields into the existing metadata and persist to the package. */
30
+ setMetadata(props) {
31
+ const coreKeys = [
32
+ 'title', 'author', 'description', 'keywords', 'lastModifiedBy', 'created', 'modified',
33
+ ];
34
+ const appKeys = ['company', 'manager'];
35
+ if (coreKeys.some(k => k in props))
36
+ this.writeCore(props);
37
+ if (appKeys.some(k => k in props))
38
+ this.writeApp(props);
39
+ }
40
+ // ---- private helpers ---------------------------------------------------
41
+ /** Extract a string from a parsed XML node (handles plain strings and #text objects). */
42
+ str(val) {
43
+ if (val === undefined || val === null)
44
+ return undefined;
45
+ if (typeof val === 'string')
46
+ return val || undefined;
47
+ if (typeof val === 'object' && '#text' in val) {
48
+ const t = val['#text'];
49
+ const s = typeof t === 'string' ? t : String(t);
50
+ return s || undefined;
51
+ }
52
+ return undefined;
53
+ }
54
+ /** Parse an ISO datetime string into a Date (returns undefined on failure). */
55
+ date(val) {
56
+ const text = this.str(val);
57
+ if (!text)
58
+ return undefined;
59
+ const d = new Date(text);
60
+ return isNaN(d.getTime()) ? undefined : d;
61
+ }
62
+ parsedCore() {
63
+ try {
64
+ const xml = this.pkg.getFileText('docProps/core.xml');
65
+ const parsed = this.parser.parse(xml);
66
+ return parsed['cp:coreProperties'] ?? {};
67
+ }
68
+ catch {
69
+ return {};
70
+ }
71
+ }
72
+ parsedApp() {
73
+ try {
74
+ const xml = this.pkg.getFileText('docProps/app.xml');
75
+ const parsed = this.parser.parse(xml);
76
+ return parsed['Properties'] ?? {};
77
+ }
78
+ catch {
79
+ return {};
80
+ }
81
+ }
82
+ writeCore(props) {
83
+ let parsed;
84
+ try {
85
+ const xml = this.pkg.getFileText('docProps/core.xml');
86
+ parsed = this.parser.parse(xml);
87
+ }
88
+ catch {
89
+ parsed = this.blankCoreParsed();
90
+ }
91
+ const root = parsed['cp:coreProperties'];
92
+ if ('title' in props)
93
+ root['dc:title'] = props.title ?? '';
94
+ if ('author' in props)
95
+ root['dc:creator'] = props.author ?? '';
96
+ if ('description' in props)
97
+ root['dc:description'] = props.description ?? '';
98
+ if ('keywords' in props)
99
+ root['cp:keywords'] = props.keywords ?? '';
100
+ if ('lastModifiedBy' in props)
101
+ root['cp:lastModifiedBy'] = props.lastModifiedBy ?? '';
102
+ if ('created' in props && props.created !== undefined) {
103
+ root['dcterms:created'] = {
104
+ '@_xsi:type': 'dcterms:W3CDTF',
105
+ '#text': props.created.toISOString(),
106
+ };
107
+ }
108
+ if ('modified' in props && props.modified !== undefined) {
109
+ root['dcterms:modified'] = {
110
+ '@_xsi:type': 'dcterms:W3CDTF',
111
+ '#text': props.modified.toISOString(),
112
+ };
113
+ }
114
+ this.pkg.updateFile('docProps/core.xml', (0, XmlHelper_1.buildXml)(this.builder, parsed));
115
+ }
116
+ writeApp(props) {
117
+ let parsed;
118
+ try {
119
+ const xml = this.pkg.getFileText('docProps/app.xml');
120
+ parsed = this.parser.parse(xml);
121
+ }
122
+ catch {
123
+ parsed = this.blankAppParsed();
124
+ }
125
+ const root = parsed['Properties'];
126
+ if ('company' in props)
127
+ root['Company'] = props.company ?? '';
128
+ if ('manager' in props)
129
+ root['Manager'] = props.manager ?? '';
130
+ this.pkg.updateFile('docProps/app.xml', (0, XmlHelper_1.buildXml)(this.builder, parsed));
131
+ }
132
+ blankCoreParsed() {
133
+ return {
134
+ 'cp:coreProperties': {
135
+ '@_xmlns:cp': VisioConstants_1.XML_NAMESPACES.CORE_PROPERTIES,
136
+ '@_xmlns:dc': VisioConstants_1.XML_NAMESPACES.DC_ELEMENTS,
137
+ '@_xmlns:dcterms': VisioConstants_1.XML_NAMESPACES.DC_TERMS,
138
+ '@_xmlns:dcmitype': VisioConstants_1.XML_NAMESPACES.DC_DCMITYPE,
139
+ '@_xmlns:xsi': VisioConstants_1.XML_NAMESPACES.XSI,
140
+ },
141
+ };
142
+ }
143
+ blankAppParsed() {
144
+ return {
145
+ 'Properties': {
146
+ '@_xmlns': VisioConstants_1.XML_NAMESPACES.EXTENDED_PROPERTIES,
147
+ '@_xmlns:vt': VisioConstants_1.XML_NAMESPACES.DOC_PROPS_VTYPES,
148
+ 'Application': 'ts-visio',
149
+ 'Template': 'Basic',
150
+ },
151
+ };
152
+ }
153
+ }
154
+ exports.MetadataManager = MetadataManager;
@@ -37,16 +37,47 @@ class ShapeBuilder {
37
37
  weight: '0.01'
38
38
  }));
39
39
  }
40
- if (props.fontColor || props.bold || props.fontSize !== undefined || props.fontFamily !== undefined) {
40
+ const hasCharProps = props.fontColor !== undefined
41
+ || props.bold !== undefined
42
+ || props.italic !== undefined
43
+ || props.underline !== undefined
44
+ || props.strikethrough !== undefined
45
+ || props.fontSize !== undefined
46
+ || props.fontFamily !== undefined;
47
+ if (hasCharProps) {
41
48
  shape.Section.push((0, StyleHelpers_1.createCharacterSection)({
42
49
  bold: props.bold,
50
+ italic: props.italic,
51
+ underline: props.underline,
52
+ strikethrough: props.strikethrough,
43
53
  color: props.fontColor,
44
54
  fontSize: props.fontSize,
45
55
  fontFamily: props.fontFamily,
46
56
  }));
47
57
  }
48
- if (props.horzAlign !== undefined) {
49
- shape.Section.push((0, StyleHelpers_1.createParagraphSection)(props.horzAlign));
58
+ const hasParagraphProps = props.horzAlign !== undefined
59
+ || props.spaceBefore !== undefined
60
+ || props.spaceAfter !== undefined
61
+ || props.lineSpacing !== undefined;
62
+ if (hasParagraphProps) {
63
+ shape.Section.push((0, StyleHelpers_1.createParagraphSection)({
64
+ horzAlign: props.horzAlign,
65
+ spaceBefore: props.spaceBefore,
66
+ spaceAfter: props.spaceAfter,
67
+ lineSpacing: props.lineSpacing,
68
+ }));
69
+ }
70
+ const hasTextBlockProps = props.textMarginTop !== undefined
71
+ || props.textMarginBottom !== undefined
72
+ || props.textMarginLeft !== undefined
73
+ || props.textMarginRight !== undefined;
74
+ if (hasTextBlockProps) {
75
+ shape.Section.push((0, StyleHelpers_1.createTextBlockSection)({
76
+ topMargin: props.textMarginTop,
77
+ bottomMargin: props.textMarginBottom,
78
+ leftMargin: props.textMarginLeft,
79
+ rightMargin: props.textMarginRight,
80
+ }));
50
81
  }
51
82
  if (props.verticalAlign !== undefined) {
52
83
  shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(props.verticalAlign) });
@@ -72,6 +72,27 @@ export declare enum VisioPropType {
72
72
  Duration = 6,
73
73
  Currency = 7
74
74
  }
75
+ /** Document-level metadata that maps to `docProps/core.xml` and `docProps/app.xml`. */
76
+ export interface DocumentMetadata {
77
+ /** Document title (`dc:title`). */
78
+ title?: string;
79
+ /** Author / creator (`dc:creator`). */
80
+ author?: string;
81
+ /** Short description (`dc:description`). */
82
+ description?: string;
83
+ /** Space-separated keywords (`cp:keywords`). */
84
+ keywords?: string;
85
+ /** Last-modified-by user (`cp:lastModifiedBy`). */
86
+ lastModifiedBy?: string;
87
+ /** Company name from `app.xml` `<Company>`. */
88
+ company?: string;
89
+ /** Manager name from `app.xml` `<Manager>`. */
90
+ manager?: string;
91
+ /** Document creation timestamp (`dcterms:created`). */
92
+ created?: Date;
93
+ /** Last-modified timestamp (`dcterms:modified`). */
94
+ modified?: Date;
95
+ }
75
96
  export type PageOrientation = 'portrait' | 'landscape';
76
97
  /** Common paper sizes in inches (width × height in portrait orientation). */
77
98
  export declare const PageSizes: {
@@ -151,4 +172,24 @@ export interface NewShapeProps {
151
172
  masterId?: string;
152
173
  imgRelId?: string;
153
174
  lineColor?: string;
175
+ /** Italic text. */
176
+ italic?: boolean;
177
+ /** Underline text. */
178
+ underline?: boolean;
179
+ /** Strikethrough text. */
180
+ strikethrough?: boolean;
181
+ /** Space before each paragraph in **points**. */
182
+ spaceBefore?: number;
183
+ /** Space after each paragraph in **points**. */
184
+ spaceAfter?: number;
185
+ /** Line-height multiplier (1.0 = single, 1.5 = 1.5×, 2.0 = double). */
186
+ lineSpacing?: number;
187
+ /** Top text margin in inches. */
188
+ textMarginTop?: number;
189
+ /** Bottom text margin in inches. */
190
+ textMarginBottom?: number;
191
+ /** Left text margin in inches. */
192
+ textMarginLeft?: number;
193
+ /** Right text margin in inches. */
194
+ textMarginRight?: number;
154
195
  }
@@ -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.3.0",
3
+ "version": "1.5.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {