docxmlater 10.3.6 → 10.4.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 +158 -7
- package/dist/core/Document.d.ts +97 -3
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +727 -50
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentContent.d.ts.map +1 -1
- package/dist/core/DocumentContent.js +0 -8
- package/dist/core/DocumentContent.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +9 -5
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +573 -101
- package/dist/core/DocumentParser.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +4 -3
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/elements/Bookmark.d.ts +7 -0
- package/dist/elements/Bookmark.d.ts.map +1 -1
- package/dist/elements/Bookmark.js +24 -4
- package/dist/elements/Bookmark.js.map +1 -1
- package/dist/elements/BookmarkManager.d.ts.map +1 -1
- package/dist/elements/BookmarkManager.js +4 -3
- package/dist/elements/BookmarkManager.js.map +1 -1
- package/dist/elements/CommonTypes.d.ts +2 -2
- package/dist/elements/CommonTypes.d.ts.map +1 -1
- package/dist/elements/CommonTypes.js +14 -1
- package/dist/elements/CommonTypes.js.map +1 -1
- package/dist/elements/Field.d.ts +1 -1
- package/dist/elements/Field.d.ts.map +1 -1
- package/dist/elements/Field.js +1 -1
- package/dist/elements/Field.js.map +1 -1
- package/dist/elements/Footer.d.ts +2 -0
- package/dist/elements/Footer.d.ts.map +1 -1
- package/dist/elements/Footer.js +6 -0
- package/dist/elements/Footer.js.map +1 -1
- package/dist/elements/Header.d.ts +2 -0
- package/dist/elements/Header.d.ts.map +1 -1
- package/dist/elements/Header.js +6 -0
- package/dist/elements/Header.js.map +1 -1
- package/dist/elements/Image.d.ts.map +1 -1
- package/dist/elements/Image.js +3 -0
- package/dist/elements/Image.js.map +1 -1
- package/dist/elements/Paragraph.d.ts +81 -1
- package/dist/elements/Paragraph.d.ts.map +1 -1
- package/dist/elements/Paragraph.js +515 -21
- package/dist/elements/Paragraph.js.map +1 -1
- package/dist/elements/Revision.d.ts +0 -1
- package/dist/elements/Revision.d.ts.map +1 -1
- package/dist/elements/Revision.js +0 -12
- package/dist/elements/Revision.js.map +1 -1
- package/dist/elements/RevisionManager.d.ts +0 -1
- package/dist/elements/RevisionManager.d.ts.map +1 -1
- package/dist/elements/RevisionManager.js +0 -2
- package/dist/elements/RevisionManager.js.map +1 -1
- package/dist/elements/Run.d.ts +16 -4
- package/dist/elements/Run.d.ts.map +1 -1
- package/dist/elements/Run.js +114 -22
- package/dist/elements/Run.js.map +1 -1
- package/dist/elements/Section.d.ts +7 -1
- package/dist/elements/Section.d.ts.map +1 -1
- package/dist/elements/Section.js +185 -4
- package/dist/elements/Section.js.map +1 -1
- package/dist/elements/Shape.js.map +1 -1
- package/dist/elements/Table.d.ts +30 -1
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +357 -40
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableCell.d.ts +3 -0
- package/dist/elements/TableCell.d.ts.map +1 -1
- package/dist/elements/TableCell.js +30 -3
- package/dist/elements/TableCell.js.map +1 -1
- package/dist/elements/TableGridChange.d.ts +0 -1
- package/dist/elements/TableGridChange.d.ts.map +1 -1
- package/dist/elements/TableGridChange.js +0 -10
- package/dist/elements/TableGridChange.js.map +1 -1
- package/dist/elements/TableRow.d.ts +4 -0
- package/dist/elements/TableRow.d.ts.map +1 -1
- package/dist/elements/TableRow.js +31 -3
- package/dist/elements/TableRow.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts +5 -0
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +22 -0
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +3 -3
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/Style.d.ts +1 -0
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +25 -59
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +1 -0
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js +12 -0
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.d.ts.map +1 -1
- package/dist/images/ImageOptimizer.js +0 -1
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/DrawingManager.d.ts.map +1 -1
- package/dist/managers/DrawingManager.js +4 -2
- package/dist/managers/DrawingManager.js.map +1 -1
- package/dist/types/formatting.d.ts +2 -2
- package/dist/types/formatting.d.ts.map +1 -1
- package/dist/types/formatting.js.map +1 -1
- package/dist/utils/ChangelogGenerator.d.ts +2 -2
- package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
- package/dist/utils/ChangelogGenerator.js +4 -5
- package/dist/utils/ChangelogGenerator.js.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.js +0 -1
- package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/utils/RevisionAwareProcessor.d.ts +2 -2
- package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
- package/dist/utils/RevisionAwareProcessor.js +2 -2
- package/dist/utils/RevisionAwareProcessor.js.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.d.ts +0 -2
- package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.js +0 -26
- package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/utils/ShadingResolver.d.ts.map +1 -1
- package/dist/utils/ShadingResolver.js.map +1 -1
- package/dist/utils/acceptRevisions.js +1 -1
- package/dist/utils/acceptRevisions.js.map +1 -1
- package/dist/utils/stripTrackedChanges.js +1 -1
- package/dist/utils/stripTrackedChanges.js.map +1 -1
- package/dist/utils/units.d.ts.map +1 -1
- package/dist/utils/units.js +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.d.ts +2 -1
- package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/package.json +10 -1
- package/src/constants/CLAUDE.md +28 -0
- package/src/core/CLAUDE.md +4 -0
- package/src/core/Document.ts +1755 -85
- package/src/core/DocumentContent.ts +0 -11
- package/src/core/DocumentGenerator.ts +11 -12
- package/src/core/DocumentParser.ts +599 -138
- package/src/core/RelationshipManager.ts +6 -3
- package/src/elements/Bookmark.ts +39 -4
- package/src/elements/BookmarkManager.ts +4 -3
- package/src/elements/CLAUDE.md +18 -2
- package/src/elements/CommonTypes.ts +35 -8
- package/src/elements/Field.ts +1 -1
- package/src/elements/Footer.ts +23 -0
- package/src/elements/Header.ts +25 -0
- package/src/elements/Image.ts +5 -0
- package/src/elements/Paragraph.ts +1069 -41
- package/src/elements/Revision.ts +0 -19
- package/src/elements/RevisionManager.ts +1 -3
- package/src/elements/Run.ts +265 -35
- package/src/elements/Section.ts +214 -8
- package/src/elements/Shape.ts +1 -1
- package/src/elements/Table.ts +850 -61
- package/src/elements/TableCell.ts +84 -10
- package/src/elements/TableGridChange.ts +2 -16
- package/src/elements/TableRow.ts +94 -9
- package/src/formatting/AbstractNumbering.ts +42 -1
- package/src/formatting/CLAUDE.md +4 -0
- package/src/formatting/NumberingLevel.ts +11 -7
- package/src/formatting/Style.ts +39 -71
- package/src/formatting/StylesManager.ts +36 -0
- package/src/helpers/CleanupHelper.ts +1 -1
- package/src/images/ImageOptimizer.ts +0 -3
- package/src/index.ts +1 -1
- package/src/managers/DrawingManager.ts +5 -3
- package/src/tracking/CLAUDE.md +30 -0
- package/src/types/CLAUDE.md +39 -0
- package/src/types/formatting.ts +2 -2
- package/src/utils/CLAUDE.md +15 -0
- package/src/utils/ChangelogGenerator.ts +4 -5
- package/src/utils/InMemoryRevisionAcceptor.ts +0 -9
- package/src/utils/RevisionAwareProcessor.ts +2 -3
- package/src/utils/SelectiveRevisionAcceptor.ts +0 -39
- package/src/utils/ShadingResolver.ts +0 -1
- package/src/utils/acceptRevisions.ts +1 -1
- package/src/utils/stripTrackedChanges.ts +1 -1
- package/src/utils/units.ts +2 -1
- package/src/validation/CLAUDE.md +40 -0
- package/src/validation/RevisionAutoFixer.ts +2 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { deepClone } from '../utils/deepClone';
|
|
7
7
|
import { formatDateForXml } from '../utils/dateFormatting';
|
|
8
8
|
import { logParagraphContent, logTextDirection } from '../utils/diagnostics';
|
|
9
|
+
import { isEqualFormatting } from '../utils/formatting';
|
|
9
10
|
import { defaultLogger } from '../utils/logger';
|
|
10
11
|
import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
|
|
11
12
|
import { Bookmark } from './Bookmark';
|
|
@@ -14,14 +15,11 @@ import {
|
|
|
14
15
|
// Import common types
|
|
15
16
|
ParagraphAlignment as CommonParagraphAlignment,
|
|
16
17
|
BorderStyle as CommonBorderStyle,
|
|
17
|
-
ShadingPattern as CommonShadingPattern,
|
|
18
18
|
BasicShadingPattern,
|
|
19
19
|
TabAlignment as CommonTabAlignment,
|
|
20
20
|
TabLeader as CommonTabLeader,
|
|
21
21
|
TextDirection as CommonTextDirection,
|
|
22
22
|
TextVerticalAlignment,
|
|
23
|
-
BorderDefinition as CommonBorderDefinition,
|
|
24
|
-
TabStop as CommonTabStop,
|
|
25
23
|
ShadingConfig,
|
|
26
24
|
buildShadingAttributes,
|
|
27
25
|
} from './CommonTypes';
|
|
@@ -138,8 +136,8 @@ export interface FrameProperties {
|
|
|
138
136
|
hSpace?: number;
|
|
139
137
|
/** Vertical padding in twips */
|
|
140
138
|
vSpace?: number;
|
|
141
|
-
/** Text wrapping around frame */
|
|
142
|
-
wrap?: 'around' | '
|
|
139
|
+
/** Text wrapping around frame per ECMA-376 ST_Wrap (§17.18.104) */
|
|
140
|
+
wrap?: 'around' | 'auto' | 'none' | 'notBeside' | 'through' | 'tight';
|
|
143
141
|
/** Drop cap style */
|
|
144
142
|
dropCap?: 'none' | 'drop' | 'margin';
|
|
145
143
|
/** Drop cap height in lines */
|
|
@@ -187,12 +185,20 @@ export interface ParagraphFormatting {
|
|
|
187
185
|
firstLine?: number;
|
|
188
186
|
hanging?: number;
|
|
189
187
|
};
|
|
190
|
-
/** Spacing
|
|
188
|
+
/** Spacing per ECMA-376 §17.3.1.33 (CT_Spacing) */
|
|
191
189
|
spacing?: {
|
|
192
190
|
before?: number;
|
|
193
191
|
after?: number;
|
|
194
192
|
line?: number;
|
|
195
193
|
lineRule?: 'auto' | 'exact' | 'atLeast';
|
|
194
|
+
/** Spacing before in hundredths of a line */
|
|
195
|
+
beforeLines?: number;
|
|
196
|
+
/** Spacing after in hundredths of a line */
|
|
197
|
+
afterLines?: number;
|
|
198
|
+
/** Auto-calculate before spacing (overrides w:before when true) */
|
|
199
|
+
beforeAutospacing?: boolean;
|
|
200
|
+
/** Auto-calculate after spacing (overrides w:after when true) */
|
|
201
|
+
afterAutospacing?: boolean;
|
|
196
202
|
};
|
|
197
203
|
/** Keep with next paragraph */
|
|
198
204
|
keepNext?: boolean;
|
|
@@ -211,6 +217,8 @@ export interface ParagraphFormatting {
|
|
|
211
217
|
contextualSpacing?: boolean;
|
|
212
218
|
/** Paragraph ID (Word 2010+) - required by modern Word for change tracking */
|
|
213
219
|
paraId?: string;
|
|
220
|
+
/** Text ID (Word 2010+) - tracks text modifications for merge conflict resolution */
|
|
221
|
+
textId?: string;
|
|
214
222
|
/** Paragraph borders (top, bottom, left, right, between, bar) */
|
|
215
223
|
borders?: {
|
|
216
224
|
top?: BorderDefinition;
|
|
@@ -682,13 +690,27 @@ export class Paragraph {
|
|
|
682
690
|
*/
|
|
683
691
|
/**
|
|
684
692
|
* Adds a hyperlink to the paragraph
|
|
685
|
-
*
|
|
686
|
-
*
|
|
693
|
+
* NOTE: This method has overloaded return types:
|
|
694
|
+
* - `addHyperlink(url: string)` returns the `Hyperlink` object (for configuring the link)
|
|
695
|
+
* - `addHyperlink(hyperlink: Hyperlink)` returns `this` (for paragraph chaining)
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* ```typescript
|
|
699
|
+
* // Pattern 1: Create and configure a new hyperlink
|
|
700
|
+
* const link = para.addHyperlink('https://example.com');
|
|
701
|
+
* link.setText('Visit Example');
|
|
702
|
+
*
|
|
703
|
+
* // Pattern 2: Add pre-built hyperlink (returns paragraph for chaining)
|
|
704
|
+
* para.addHyperlink(new Hyperlink({ url: 'https://example.com', text: 'Link' }))
|
|
705
|
+
* .addText(' more text after the link');
|
|
706
|
+
* ```
|
|
707
|
+
*
|
|
708
|
+
* @param url - URL string to create a new hyperlink
|
|
709
|
+
* @returns Hyperlink object for configuring the link
|
|
687
710
|
*/
|
|
688
711
|
addHyperlink(url?: string): Hyperlink;
|
|
689
712
|
/**
|
|
690
|
-
*
|
|
691
|
-
* @param hyperlink - Existing Hyperlink object
|
|
713
|
+
* @param hyperlink - Existing Hyperlink object to add
|
|
692
714
|
* @returns This paragraph for chaining
|
|
693
715
|
*/
|
|
694
716
|
addHyperlink(hyperlink: Hyperlink): this;
|
|
@@ -1029,6 +1051,44 @@ export class Paragraph {
|
|
|
1029
1051
|
return this;
|
|
1030
1052
|
}
|
|
1031
1053
|
|
|
1054
|
+
/**
|
|
1055
|
+
* Adds a line break to the paragraph
|
|
1056
|
+
*
|
|
1057
|
+
* Creates a Run containing a line break element (`w:br`). This produces
|
|
1058
|
+
* a soft return (line break within the same paragraph), unlike creating
|
|
1059
|
+
* a new paragraph which produces a hard return.
|
|
1060
|
+
*
|
|
1061
|
+
* @returns This paragraph for chaining
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```typescript
|
|
1065
|
+
* para.addText('Line one');
|
|
1066
|
+
* para.addLineBreak();
|
|
1067
|
+
* para.addText('Line two (same paragraph)');
|
|
1068
|
+
* ```
|
|
1069
|
+
*/
|
|
1070
|
+
addLineBreak(): this {
|
|
1071
|
+
const run = new Run('');
|
|
1072
|
+
run.addBreak();
|
|
1073
|
+
this.content.push(run);
|
|
1074
|
+
return this;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Adds a column break to the paragraph
|
|
1079
|
+
*
|
|
1080
|
+
* Inserts a column break element (`w:br w:type="column"`). In multi-column
|
|
1081
|
+
* layouts, forces subsequent content to the next column.
|
|
1082
|
+
*
|
|
1083
|
+
* @returns This paragraph for chaining
|
|
1084
|
+
*/
|
|
1085
|
+
addColumnBreak(): this {
|
|
1086
|
+
const run = new Run('');
|
|
1087
|
+
run.addBreak('column');
|
|
1088
|
+
this.content.push(run);
|
|
1089
|
+
return this;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1032
1092
|
/**
|
|
1033
1093
|
* Sets the paragraph text content (replaces all existing content)
|
|
1034
1094
|
*
|
|
@@ -1772,6 +1832,29 @@ export class Paragraph {
|
|
|
1772
1832
|
return this.getText().trim().length === 0;
|
|
1773
1833
|
}
|
|
1774
1834
|
|
|
1835
|
+
/**
|
|
1836
|
+
* Checks whether the paragraph text contains a substring
|
|
1837
|
+
*
|
|
1838
|
+
* Case-insensitive by default. A simpler alternative to `findText()`
|
|
1839
|
+
* when you just need a boolean check.
|
|
1840
|
+
*
|
|
1841
|
+
* @param text - Substring to search for
|
|
1842
|
+
* @param caseSensitive - Match case exactly (default: false)
|
|
1843
|
+
* @returns True if the paragraph contains the text
|
|
1844
|
+
*
|
|
1845
|
+
* @example
|
|
1846
|
+
* ```typescript
|
|
1847
|
+
* if (para.contains('TODO')) {
|
|
1848
|
+
* console.log('Found a TODO item');
|
|
1849
|
+
* }
|
|
1850
|
+
* ```
|
|
1851
|
+
*/
|
|
1852
|
+
contains(text: string, caseSensitive = false): boolean {
|
|
1853
|
+
const paraText = caseSensitive ? this.getText() : this.getText().toLowerCase();
|
|
1854
|
+
const search = caseSensitive ? text : text.toLowerCase();
|
|
1855
|
+
return paraText.includes(search);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1775
1858
|
/**
|
|
1776
1859
|
* Gets the paragraph style ID
|
|
1777
1860
|
*
|
|
@@ -2222,7 +2305,7 @@ export class Paragraph {
|
|
|
2222
2305
|
|
|
2223
2306
|
// Resolve property conflicts: keepNext contradicts pageBreakBefore
|
|
2224
2307
|
if (keepNext) {
|
|
2225
|
-
this.formatting.pageBreakBefore =
|
|
2308
|
+
this.formatting.pageBreakBefore = undefined;
|
|
2226
2309
|
}
|
|
2227
2310
|
|
|
2228
2311
|
if (this.trackingContext?.isEnabled() && previousValue !== keepNext) {
|
|
@@ -2250,7 +2333,7 @@ export class Paragraph {
|
|
|
2250
2333
|
|
|
2251
2334
|
// Resolve property conflicts: keepLines contradicts pageBreakBefore
|
|
2252
2335
|
if (keepLines) {
|
|
2253
|
-
this.formatting.pageBreakBefore =
|
|
2336
|
+
this.formatting.pageBreakBefore = undefined;
|
|
2254
2337
|
}
|
|
2255
2338
|
|
|
2256
2339
|
if (this.trackingContext?.isEnabled() && previousValue !== keepLines) {
|
|
@@ -2983,14 +3066,18 @@ export class Paragraph {
|
|
|
2983
3066
|
pPrChildren.push(XMLBuilder.wSelf('pStyle', { 'w:val': this.formatting.style }));
|
|
2984
3067
|
}
|
|
2985
3068
|
|
|
2986
|
-
// 2. Keep with next paragraph
|
|
2987
|
-
if (this.formatting.keepNext) {
|
|
2988
|
-
pPrChildren.push(
|
|
3069
|
+
// 2. Keep with next paragraph (CT_OnOff — emit val="0" to override style inheritance)
|
|
3070
|
+
if (this.formatting.keepNext !== undefined) {
|
|
3071
|
+
pPrChildren.push(
|
|
3072
|
+
XMLBuilder.wSelf('keepNext', { 'w:val': this.formatting.keepNext ? '1' : '0' })
|
|
3073
|
+
);
|
|
2989
3074
|
}
|
|
2990
3075
|
|
|
2991
3076
|
// 3. Keep lines together
|
|
2992
|
-
if (this.formatting.keepLines) {
|
|
2993
|
-
pPrChildren.push(
|
|
3077
|
+
if (this.formatting.keepLines !== undefined) {
|
|
3078
|
+
pPrChildren.push(
|
|
3079
|
+
XMLBuilder.wSelf('keepLines', { 'w:val': this.formatting.keepLines ? '1' : '0' })
|
|
3080
|
+
);
|
|
2994
3081
|
}
|
|
2995
3082
|
|
|
2996
3083
|
// CT_PPrBase element order per ECMA-376:
|
|
@@ -3002,8 +3089,12 @@ export class Paragraph {
|
|
|
3002
3089
|
// outlineLvl → divId → cnfStyle
|
|
3003
3090
|
|
|
3004
3091
|
// 4. Page break before
|
|
3005
|
-
if (this.formatting.pageBreakBefore) {
|
|
3006
|
-
pPrChildren.push(
|
|
3092
|
+
if (this.formatting.pageBreakBefore !== undefined) {
|
|
3093
|
+
pPrChildren.push(
|
|
3094
|
+
XMLBuilder.wSelf('pageBreakBefore', {
|
|
3095
|
+
'w:val': this.formatting.pageBreakBefore ? '1' : '0',
|
|
3096
|
+
})
|
|
3097
|
+
);
|
|
3007
3098
|
}
|
|
3008
3099
|
|
|
3009
3100
|
// 5. Text frame properties (framePr)
|
|
@@ -3050,11 +3141,22 @@ export class Paragraph {
|
|
|
3050
3141
|
}),
|
|
3051
3142
|
]);
|
|
3052
3143
|
pPrChildren.push(numPr);
|
|
3144
|
+
} else if (this.formatting.numberingSuppressed) {
|
|
3145
|
+
// Per ECMA-376 §17.3.1.19, numId=0 explicitly removes numbering inherited from style
|
|
3146
|
+
const numPr = XMLBuilder.w('numPr', undefined, [
|
|
3147
|
+
XMLBuilder.wSelf('ilvl', { 'w:val': '0' }),
|
|
3148
|
+
XMLBuilder.wSelf('numId', { 'w:val': '0' }),
|
|
3149
|
+
]);
|
|
3150
|
+
pPrChildren.push(numPr);
|
|
3053
3151
|
}
|
|
3054
3152
|
|
|
3055
3153
|
// 8. Suppress line numbers
|
|
3056
|
-
if (this.formatting.suppressLineNumbers) {
|
|
3057
|
-
pPrChildren.push(
|
|
3154
|
+
if (this.formatting.suppressLineNumbers !== undefined) {
|
|
3155
|
+
pPrChildren.push(
|
|
3156
|
+
XMLBuilder.wSelf('suppressLineNumbers', {
|
|
3157
|
+
'w:val': this.formatting.suppressLineNumbers ? '1' : '0',
|
|
3158
|
+
})
|
|
3159
|
+
);
|
|
3058
3160
|
}
|
|
3059
3161
|
|
|
3060
3162
|
// 9. Paragraph borders
|
|
@@ -3126,8 +3228,12 @@ export class Paragraph {
|
|
|
3126
3228
|
}
|
|
3127
3229
|
|
|
3128
3230
|
// 12. Suppress automatic hyphenation
|
|
3129
|
-
if (this.formatting.suppressAutoHyphens) {
|
|
3130
|
-
pPrChildren.push(
|
|
3231
|
+
if (this.formatting.suppressAutoHyphens !== undefined) {
|
|
3232
|
+
pPrChildren.push(
|
|
3233
|
+
XMLBuilder.wSelf('suppressAutoHyphens', {
|
|
3234
|
+
'w:val': this.formatting.suppressAutoHyphens ? '1' : '0',
|
|
3235
|
+
})
|
|
3236
|
+
);
|
|
3131
3237
|
}
|
|
3132
3238
|
|
|
3133
3239
|
// 13. CJK paragraph properties
|
|
@@ -3176,12 +3282,18 @@ export class Paragraph {
|
|
|
3176
3282
|
);
|
|
3177
3283
|
}
|
|
3178
3284
|
|
|
3179
|
-
// 16. Spacing (before/after/line)
|
|
3285
|
+
// 16. Spacing (before/after/line + autospacing/lines per ECMA-376 §17.3.1.33)
|
|
3180
3286
|
if (this.formatting.spacing) {
|
|
3181
3287
|
const spc = this.formatting.spacing;
|
|
3182
3288
|
const attributes: Record<string, number | string> = {};
|
|
3183
3289
|
if (spc.before !== undefined) attributes['w:before'] = spc.before;
|
|
3290
|
+
if (spc.beforeLines !== undefined) attributes['w:beforeLines'] = spc.beforeLines;
|
|
3291
|
+
if (spc.beforeAutospacing !== undefined)
|
|
3292
|
+
attributes['w:beforeAutospacing'] = spc.beforeAutospacing ? '1' : '0';
|
|
3184
3293
|
if (spc.after !== undefined) attributes['w:after'] = spc.after;
|
|
3294
|
+
if (spc.afterLines !== undefined) attributes['w:afterLines'] = spc.afterLines;
|
|
3295
|
+
if (spc.afterAutospacing !== undefined)
|
|
3296
|
+
attributes['w:afterAutospacing'] = spc.afterAutospacing ? '1' : '0';
|
|
3185
3297
|
if (spc.line !== undefined) attributes['w:line'] = spc.line;
|
|
3186
3298
|
if (spc.lineRule) attributes['w:lineRule'] = spc.lineRule;
|
|
3187
3299
|
if (Object.keys(attributes).length > 0) {
|
|
@@ -3190,31 +3302,45 @@ export class Paragraph {
|
|
|
3190
3302
|
}
|
|
3191
3303
|
|
|
3192
3304
|
// 17. Indentation (left/right/firstLine/hanging)
|
|
3305
|
+
// Per ECMA-376 §17.3.1.12, firstLine and hanging are mutually exclusive — hanging takes precedence
|
|
3193
3306
|
if (this.formatting.indentation) {
|
|
3194
3307
|
const ind = this.formatting.indentation;
|
|
3195
3308
|
const attributes: Record<string, number> = {};
|
|
3196
3309
|
if (ind.left !== undefined) attributes['w:left'] = ind.left;
|
|
3197
3310
|
if (ind.right !== undefined) attributes['w:right'] = ind.right;
|
|
3198
|
-
if (ind.
|
|
3199
|
-
|
|
3311
|
+
if (ind.hanging !== undefined) {
|
|
3312
|
+
attributes['w:hanging'] = ind.hanging;
|
|
3313
|
+
} else if (ind.firstLine !== undefined) {
|
|
3314
|
+
attributes['w:firstLine'] = ind.firstLine;
|
|
3315
|
+
}
|
|
3200
3316
|
if (Object.keys(attributes).length > 0) {
|
|
3201
3317
|
pPrChildren.push(XMLBuilder.wSelf('ind', attributes));
|
|
3202
3318
|
}
|
|
3203
3319
|
}
|
|
3204
3320
|
|
|
3205
3321
|
// 18. Contextual spacing
|
|
3206
|
-
if (this.formatting.contextualSpacing) {
|
|
3207
|
-
pPrChildren.push(
|
|
3322
|
+
if (this.formatting.contextualSpacing !== undefined) {
|
|
3323
|
+
pPrChildren.push(
|
|
3324
|
+
XMLBuilder.wSelf('contextualSpacing', {
|
|
3325
|
+
'w:val': this.formatting.contextualSpacing ? '1' : '0',
|
|
3326
|
+
})
|
|
3327
|
+
);
|
|
3208
3328
|
}
|
|
3209
3329
|
|
|
3210
3330
|
// 19. Mirror indents
|
|
3211
|
-
if (this.formatting.mirrorIndents) {
|
|
3212
|
-
pPrChildren.push(
|
|
3331
|
+
if (this.formatting.mirrorIndents !== undefined) {
|
|
3332
|
+
pPrChildren.push(
|
|
3333
|
+
XMLBuilder.wSelf('mirrorIndents', { 'w:val': this.formatting.mirrorIndents ? '1' : '0' })
|
|
3334
|
+
);
|
|
3213
3335
|
}
|
|
3214
3336
|
|
|
3215
3337
|
// 20. Suppress text frame overlap
|
|
3216
|
-
if (this.formatting.suppressOverlap) {
|
|
3217
|
-
pPrChildren.push(
|
|
3338
|
+
if (this.formatting.suppressOverlap !== undefined) {
|
|
3339
|
+
pPrChildren.push(
|
|
3340
|
+
XMLBuilder.wSelf('suppressOverlap', {
|
|
3341
|
+
'w:val': this.formatting.suppressOverlap ? '1' : '0',
|
|
3342
|
+
})
|
|
3343
|
+
);
|
|
3218
3344
|
}
|
|
3219
3345
|
|
|
3220
3346
|
// 21. Justification/Alignment
|
|
@@ -3361,9 +3487,11 @@ export class Paragraph {
|
|
|
3361
3487
|
|
|
3362
3488
|
// Build child w:pPr element with previous properties
|
|
3363
3489
|
// Per CT_PPrBase schema order: pStyle, keepNext, keepLines, pageBreakBefore,
|
|
3364
|
-
// widowControl, numPr, suppressLineNumbers, pBdr, shd, tabs,
|
|
3365
|
-
// suppressAutoHyphens,
|
|
3366
|
-
//
|
|
3490
|
+
// framePr, widowControl, numPr, suppressLineNumbers, pBdr, shd, tabs,
|
|
3491
|
+
// suppressAutoHyphens, kinsoku, wordWrap, overflowPunct, topLinePunct,
|
|
3492
|
+
// autoSpaceDE, autoSpaceDN, bidi, adjustRightInd, spacing, ind,
|
|
3493
|
+
// contextualSpacing, mirrorIndents, suppressOverlap, jc, textDirection,
|
|
3494
|
+
// textAlignment, outlineLvl
|
|
3367
3495
|
const prevPPrChildren: XMLElement[] = [];
|
|
3368
3496
|
if (change.previousProperties) {
|
|
3369
3497
|
const prev = change.previousProperties;
|
|
@@ -3397,7 +3525,31 @@ export class Paragraph {
|
|
|
3397
3525
|
);
|
|
3398
3526
|
}
|
|
3399
3527
|
|
|
3400
|
-
// 5.
|
|
3528
|
+
// 5. framePr (text frame properties)
|
|
3529
|
+
if (prev.framePr) {
|
|
3530
|
+
const fAttrs: Record<string, string> = {};
|
|
3531
|
+
const f = prev.framePr;
|
|
3532
|
+
if (f.w !== undefined) fAttrs['w:w'] = f.w.toString();
|
|
3533
|
+
if (f.h !== undefined) fAttrs['w:h'] = f.h.toString();
|
|
3534
|
+
if (f.hRule) fAttrs['w:hRule'] = f.hRule;
|
|
3535
|
+
if (f.x !== undefined) fAttrs['w:x'] = f.x.toString();
|
|
3536
|
+
if (f.y !== undefined) fAttrs['w:y'] = f.y.toString();
|
|
3537
|
+
if (f.xAlign) fAttrs['w:xAlign'] = f.xAlign;
|
|
3538
|
+
if (f.yAlign) fAttrs['w:yAlign'] = f.yAlign;
|
|
3539
|
+
if (f.hAnchor) fAttrs['w:hAnchor'] = f.hAnchor;
|
|
3540
|
+
if (f.vAnchor) fAttrs['w:vAnchor'] = f.vAnchor;
|
|
3541
|
+
if (f.hSpace !== undefined) fAttrs['w:hSpace'] = f.hSpace.toString();
|
|
3542
|
+
if (f.vSpace !== undefined) fAttrs['w:vSpace'] = f.vSpace.toString();
|
|
3543
|
+
if (f.wrap) fAttrs['w:wrap'] = f.wrap;
|
|
3544
|
+
if (f.dropCap) fAttrs['w:dropCap'] = f.dropCap;
|
|
3545
|
+
if (f.lines !== undefined) fAttrs['w:lines'] = f.lines.toString();
|
|
3546
|
+
if (f.anchorLock !== undefined) fAttrs['w:anchorLock'] = f.anchorLock ? '1' : '0';
|
|
3547
|
+
if (Object.keys(fAttrs).length > 0) {
|
|
3548
|
+
prevPPrChildren.push(XMLBuilder.wSelf('framePr', fAttrs));
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
// 6. widowControl
|
|
3401
3553
|
if (prev.widowControl !== undefined) {
|
|
3402
3554
|
prevPPrChildren.push(
|
|
3403
3555
|
XMLBuilder.wSelf('widowControl', {
|
|
@@ -3422,6 +3574,13 @@ export class Paragraph {
|
|
|
3422
3574
|
if (numPrChildren.length > 0) {
|
|
3423
3575
|
prevPPrChildren.push(XMLBuilder.w('numPr', undefined, numPrChildren));
|
|
3424
3576
|
}
|
|
3577
|
+
} else if (prev.numberingSuppressed) {
|
|
3578
|
+
prevPPrChildren.push(
|
|
3579
|
+
XMLBuilder.w('numPr', undefined, [
|
|
3580
|
+
XMLBuilder.wSelf('ilvl', { 'w:val': '0' }),
|
|
3581
|
+
XMLBuilder.wSelf('numId', { 'w:val': '0' }),
|
|
3582
|
+
])
|
|
3583
|
+
);
|
|
3425
3584
|
}
|
|
3426
3585
|
|
|
3427
3586
|
// 7. suppressLineNumbers
|
|
@@ -3481,7 +3640,37 @@ export class Paragraph {
|
|
|
3481
3640
|
}
|
|
3482
3641
|
}
|
|
3483
3642
|
|
|
3484
|
-
// 12.
|
|
3643
|
+
// 12. CJK paragraph properties
|
|
3644
|
+
if (prev.kinsoku !== undefined) {
|
|
3645
|
+
prevPPrChildren.push(XMLBuilder.wSelf('kinsoku', { 'w:val': prev.kinsoku ? '1' : '0' }));
|
|
3646
|
+
}
|
|
3647
|
+
if (prev.wordWrap !== undefined) {
|
|
3648
|
+
prevPPrChildren.push(
|
|
3649
|
+
XMLBuilder.wSelf('wordWrap', { 'w:val': prev.wordWrap ? '1' : '0' })
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
if (prev.overflowPunct !== undefined) {
|
|
3653
|
+
prevPPrChildren.push(
|
|
3654
|
+
XMLBuilder.wSelf('overflowPunct', { 'w:val': prev.overflowPunct ? '1' : '0' })
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
if (prev.topLinePunct !== undefined) {
|
|
3658
|
+
prevPPrChildren.push(
|
|
3659
|
+
XMLBuilder.wSelf('topLinePunct', { 'w:val': prev.topLinePunct ? '1' : '0' })
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
if (prev.autoSpaceDE !== undefined) {
|
|
3663
|
+
prevPPrChildren.push(
|
|
3664
|
+
XMLBuilder.wSelf('autoSpaceDE', { 'w:val': prev.autoSpaceDE ? '1' : '0' })
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
if (prev.autoSpaceDN !== undefined) {
|
|
3668
|
+
prevPPrChildren.push(
|
|
3669
|
+
XMLBuilder.wSelf('autoSpaceDN', { 'w:val': prev.autoSpaceDN ? '1' : '0' })
|
|
3670
|
+
);
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// 13. bidi
|
|
3485
3674
|
if (prev.bidi !== undefined) {
|
|
3486
3675
|
prevPPrChildren.push(XMLBuilder.wSelf('bidi', { 'w:val': prev.bidi ? '1' : '0' }));
|
|
3487
3676
|
}
|
|
@@ -3495,13 +3684,21 @@ export class Paragraph {
|
|
|
3495
3684
|
);
|
|
3496
3685
|
}
|
|
3497
3686
|
|
|
3498
|
-
// 14. spacing
|
|
3687
|
+
// 14. spacing (all 8 CT_Spacing attributes)
|
|
3499
3688
|
if (prev.spacing) {
|
|
3500
3689
|
const spacingAttrs: Record<string, string> = {};
|
|
3501
3690
|
if (prev.spacing.before !== undefined)
|
|
3502
3691
|
spacingAttrs['w:before'] = prev.spacing.before.toString();
|
|
3692
|
+
if (prev.spacing.beforeLines !== undefined)
|
|
3693
|
+
spacingAttrs['w:beforeLines'] = prev.spacing.beforeLines.toString();
|
|
3694
|
+
if (prev.spacing.beforeAutospacing !== undefined)
|
|
3695
|
+
spacingAttrs['w:beforeAutospacing'] = prev.spacing.beforeAutospacing ? '1' : '0';
|
|
3503
3696
|
if (prev.spacing.after !== undefined)
|
|
3504
3697
|
spacingAttrs['w:after'] = prev.spacing.after.toString();
|
|
3698
|
+
if (prev.spacing.afterLines !== undefined)
|
|
3699
|
+
spacingAttrs['w:afterLines'] = prev.spacing.afterLines.toString();
|
|
3700
|
+
if (prev.spacing.afterAutospacing !== undefined)
|
|
3701
|
+
spacingAttrs['w:afterAutospacing'] = prev.spacing.afterAutospacing ? '1' : '0';
|
|
3505
3702
|
if (prev.spacing.line !== undefined)
|
|
3506
3703
|
spacingAttrs['w:line'] = prev.spacing.line.toString();
|
|
3507
3704
|
if (prev.spacing.lineRule) spacingAttrs['w:lineRule'] = prev.spacing.lineRule;
|
|
@@ -3542,7 +3739,16 @@ export class Paragraph {
|
|
|
3542
3739
|
);
|
|
3543
3740
|
}
|
|
3544
3741
|
|
|
3545
|
-
// 18.
|
|
3742
|
+
// 18. suppressOverlap
|
|
3743
|
+
if (prev.suppressOverlap !== undefined) {
|
|
3744
|
+
prevPPrChildren.push(
|
|
3745
|
+
XMLBuilder.wSelf('suppressOverlap', {
|
|
3746
|
+
'w:val': prev.suppressOverlap ? '1' : '0',
|
|
3747
|
+
})
|
|
3748
|
+
);
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
// 19. jc (alignment)
|
|
3546
3752
|
if (prev.alignment) {
|
|
3547
3753
|
// Map 'justify' to 'both' per ECMA-376 ST_Jc enumeration
|
|
3548
3754
|
const alignmentValue = prev.alignment === 'justify' ? 'both' : prev.alignment;
|
|
@@ -3559,12 +3765,29 @@ export class Paragraph {
|
|
|
3559
3765
|
prevPPrChildren.push(XMLBuilder.wSelf('textAlignment', { 'w:val': prev.textAlignment }));
|
|
3560
3766
|
}
|
|
3561
3767
|
|
|
3562
|
-
// 21.
|
|
3768
|
+
// 21. textboxTightWrap
|
|
3769
|
+
if (prev.textboxTightWrap) {
|
|
3770
|
+
prevPPrChildren.push(
|
|
3771
|
+
XMLBuilder.wSelf('textboxTightWrap', { 'w:val': prev.textboxTightWrap })
|
|
3772
|
+
);
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
// 22. outlineLvl
|
|
3563
3776
|
if (prev.outlineLevel !== undefined) {
|
|
3564
3777
|
prevPPrChildren.push(
|
|
3565
3778
|
XMLBuilder.wSelf('outlineLvl', { 'w:val': prev.outlineLevel.toString() })
|
|
3566
3779
|
);
|
|
3567
3780
|
}
|
|
3781
|
+
|
|
3782
|
+
// 23. divId
|
|
3783
|
+
if (prev.divId !== undefined) {
|
|
3784
|
+
prevPPrChildren.push(XMLBuilder.wSelf('divId', { 'w:val': prev.divId.toString() }));
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
// 24. cnfStyle
|
|
3788
|
+
if (prev.cnfStyle) {
|
|
3789
|
+
prevPPrChildren.push(XMLBuilder.wSelf('cnfStyle', { 'w:val': prev.cnfStyle }));
|
|
3790
|
+
}
|
|
3568
3791
|
}
|
|
3569
3792
|
|
|
3570
3793
|
// Create w:pPrChange element with child w:pPr
|
|
@@ -3682,11 +3905,14 @@ export class Paragraph {
|
|
|
3682
3905
|
paragraphChildren.push(bookmark.toEndXML());
|
|
3683
3906
|
}
|
|
3684
3907
|
|
|
3685
|
-
// Add paragraph-level attributes (Word 2010+
|
|
3908
|
+
// Add paragraph-level attributes (Word 2010+ w14:paraId and w14:textId)
|
|
3686
3909
|
const paragraphAttributes: Record<string, string> = {};
|
|
3687
3910
|
if (this.formatting.paraId) {
|
|
3688
3911
|
paragraphAttributes['w14:paraId'] = this.formatting.paraId;
|
|
3689
3912
|
}
|
|
3913
|
+
if (this.formatting.textId) {
|
|
3914
|
+
paragraphAttributes['w14:textId'] = this.formatting.textId;
|
|
3915
|
+
}
|
|
3690
3916
|
|
|
3691
3917
|
return XMLBuilder.w(
|
|
3692
3918
|
'p',
|
|
@@ -3789,6 +4015,233 @@ export class Paragraph {
|
|
|
3789
4015
|
return clonedParagraph;
|
|
3790
4016
|
}
|
|
3791
4017
|
|
|
4018
|
+
/**
|
|
4019
|
+
* Serializes the paragraph to a portable JSON object
|
|
4020
|
+
*
|
|
4021
|
+
* Returns a plain object representing the paragraph's text content, inline
|
|
4022
|
+
* formatting, style, and paragraph-level formatting. Only Run content is
|
|
4023
|
+
* serialized — hyperlinks, revisions, and other non-Run elements are
|
|
4024
|
+
* represented by their text only.
|
|
4025
|
+
*
|
|
4026
|
+
* The output is designed for data pipelines, API responses, storage,
|
|
4027
|
+
* and debugging. Use `Paragraph.fromJSON()` to reconstruct.
|
|
4028
|
+
*
|
|
4029
|
+
* @returns JSON-serializable paragraph representation
|
|
4030
|
+
*
|
|
4031
|
+
* @example
|
|
4032
|
+
* ```typescript
|
|
4033
|
+
* const para = new Paragraph().addText('Hello ', { bold: true }).addText('World');
|
|
4034
|
+
* para.setStyle('Heading1').setAlignment('center');
|
|
4035
|
+
*
|
|
4036
|
+
* const json = para.toJSON();
|
|
4037
|
+
* // {
|
|
4038
|
+
* // text: 'Hello World',
|
|
4039
|
+
* // style: 'Heading1',
|
|
4040
|
+
* // alignment: 'center',
|
|
4041
|
+
* // runs: [
|
|
4042
|
+
* // { text: 'Hello ', formatting: { bold: true } },
|
|
4043
|
+
* // { text: 'World', formatting: {} }
|
|
4044
|
+
* // ]
|
|
4045
|
+
* // }
|
|
4046
|
+
* ```
|
|
4047
|
+
*/
|
|
4048
|
+
toJSON(): {
|
|
4049
|
+
text: string;
|
|
4050
|
+
style?: string;
|
|
4051
|
+
alignment?: string;
|
|
4052
|
+
runs: { text: string; formatting: RunFormatting }[];
|
|
4053
|
+
numbering?: { numId: number; level: number };
|
|
4054
|
+
indentation?: { left?: number; right?: number; firstLine?: number; hanging?: number };
|
|
4055
|
+
spacing?: { before?: number; after?: number; line?: number; lineRule?: string };
|
|
4056
|
+
} {
|
|
4057
|
+
const runs = this.getRuns().map((run) => ({
|
|
4058
|
+
text: run.getText(),
|
|
4059
|
+
formatting: run.getFormatting(),
|
|
4060
|
+
}));
|
|
4061
|
+
|
|
4062
|
+
const result: ReturnType<Paragraph['toJSON']> = {
|
|
4063
|
+
text: this.getText(),
|
|
4064
|
+
runs,
|
|
4065
|
+
};
|
|
4066
|
+
|
|
4067
|
+
if (this.formatting.style) result.style = this.formatting.style;
|
|
4068
|
+
if (this.formatting.alignment) result.alignment = this.formatting.alignment;
|
|
4069
|
+
if (this.formatting.numbering) result.numbering = { ...this.formatting.numbering };
|
|
4070
|
+
if (this.formatting.indentation) result.indentation = { ...this.formatting.indentation };
|
|
4071
|
+
if (this.formatting.spacing) {
|
|
4072
|
+
result.spacing = {
|
|
4073
|
+
before: this.formatting.spacing.before,
|
|
4074
|
+
after: this.formatting.spacing.after,
|
|
4075
|
+
line: this.formatting.spacing.line,
|
|
4076
|
+
lineRule: this.formatting.spacing.lineRule,
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
return result;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
/**
|
|
4084
|
+
* Creates a Paragraph from a JSON object (as produced by `toJSON()`)
|
|
4085
|
+
*
|
|
4086
|
+
* Reconstructs a paragraph with runs, formatting, style, and layout
|
|
4087
|
+
* from a serialized representation. This enables round-trip serialization.
|
|
4088
|
+
*
|
|
4089
|
+
* @param data - JSON object matching the `toJSON()` output shape
|
|
4090
|
+
* @returns New Paragraph instance
|
|
4091
|
+
*
|
|
4092
|
+
* @example
|
|
4093
|
+
* ```typescript
|
|
4094
|
+
* const json = { text: 'Hello', style: 'Heading1', runs: [{ text: 'Hello', formatting: { bold: true } }] };
|
|
4095
|
+
* const para = Paragraph.fromJSON(json);
|
|
4096
|
+
* para.getText(); // 'Hello'
|
|
4097
|
+
* para.getStyle(); // 'Heading1'
|
|
4098
|
+
* ```
|
|
4099
|
+
*/
|
|
4100
|
+
static fromJSON(data: {
|
|
4101
|
+
text?: string;
|
|
4102
|
+
style?: string;
|
|
4103
|
+
alignment?: string;
|
|
4104
|
+
runs?: { text: string; formatting?: RunFormatting }[];
|
|
4105
|
+
numbering?: { numId: number; level: number };
|
|
4106
|
+
indentation?: { left?: number; right?: number; firstLine?: number; hanging?: number };
|
|
4107
|
+
spacing?: { before?: number; after?: number; line?: number; lineRule?: string };
|
|
4108
|
+
}): Paragraph {
|
|
4109
|
+
const para = new Paragraph();
|
|
4110
|
+
|
|
4111
|
+
if (data.runs && data.runs.length > 0) {
|
|
4112
|
+
for (const runData of data.runs) {
|
|
4113
|
+
para.addText(runData.text, runData.formatting);
|
|
4114
|
+
}
|
|
4115
|
+
} else if (data.text) {
|
|
4116
|
+
para.addText(data.text);
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
if (data.style) para.setStyle(data.style);
|
|
4120
|
+
if (data.alignment) para.setAlignment(data.alignment as ParagraphAlignment);
|
|
4121
|
+
if (data.numbering) para.setNumbering(data.numbering.numId, data.numbering.level);
|
|
4122
|
+
if (data.indentation) {
|
|
4123
|
+
if (data.indentation.left !== undefined) para.setLeftIndent(data.indentation.left);
|
|
4124
|
+
if (data.indentation.right !== undefined) para.setRightIndent(data.indentation.right);
|
|
4125
|
+
if (data.indentation.firstLine !== undefined)
|
|
4126
|
+
para.setFirstLineIndent(data.indentation.firstLine);
|
|
4127
|
+
if (data.indentation.hanging !== undefined) para.setHangingIndent(data.indentation.hanging);
|
|
4128
|
+
}
|
|
4129
|
+
if (data.spacing) {
|
|
4130
|
+
if (data.spacing.before !== undefined) para.setSpaceBefore(data.spacing.before);
|
|
4131
|
+
if (data.spacing.after !== undefined) para.setSpaceAfter(data.spacing.after);
|
|
4132
|
+
if (data.spacing.line !== undefined) {
|
|
4133
|
+
para.setLineSpacing(
|
|
4134
|
+
data.spacing.line,
|
|
4135
|
+
(data.spacing.lineRule as 'auto' | 'exact' | 'atLeast') ?? 'auto'
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
return para;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
/**
|
|
4144
|
+
* Splits this paragraph at a character offset, returning the tail as a new paragraph
|
|
4145
|
+
*
|
|
4146
|
+
* Content from the offset onward is moved to a new Paragraph. The new paragraph
|
|
4147
|
+
* inherits this paragraph's style and formatting (deep-cloned). If the split point
|
|
4148
|
+
* falls within a Run, that run is split using `Run.splitAt()`.
|
|
4149
|
+
*
|
|
4150
|
+
* Only Run elements contribute to the character offset count. Non-Run content
|
|
4151
|
+
* (hyperlinks, revisions, fields) that appears after all affected runs is moved
|
|
4152
|
+
* to the new paragraph as-is.
|
|
4153
|
+
*
|
|
4154
|
+
* @param offset - Character position to split at (0-based). Content from this
|
|
4155
|
+
* position onward moves to the new paragraph.
|
|
4156
|
+
* @returns A new Paragraph containing content from `offset` onward
|
|
4157
|
+
*
|
|
4158
|
+
* @example
|
|
4159
|
+
* ```typescript
|
|
4160
|
+
* const para = new Paragraph().addText('Hello World');
|
|
4161
|
+
* const tail = para.splitAt(5);
|
|
4162
|
+
* para.getText(); // "Hello"
|
|
4163
|
+
* tail.getText(); // " World"
|
|
4164
|
+
* ```
|
|
4165
|
+
*
|
|
4166
|
+
* @example
|
|
4167
|
+
* ```typescript
|
|
4168
|
+
* // Insert a table between two halves of a paragraph
|
|
4169
|
+
* const tail = para.splitAt(offset);
|
|
4170
|
+
* const paraIndex = doc.getParagraphIndex(para);
|
|
4171
|
+
* doc.insertTableAt(paraIndex + 1, table);
|
|
4172
|
+
* doc.insertParagraphAt(paraIndex + 2, tail);
|
|
4173
|
+
* ```
|
|
4174
|
+
*/
|
|
4175
|
+
splitAt(offset: number): Paragraph {
|
|
4176
|
+
// Create new paragraph with cloned formatting and style
|
|
4177
|
+
const tailPara = new Paragraph(deepClone(this.formatting));
|
|
4178
|
+
|
|
4179
|
+
// Edge case: split at or past end — return empty paragraph
|
|
4180
|
+
const totalLength = this.getText().length;
|
|
4181
|
+
if (offset >= totalLength) {
|
|
4182
|
+
return tailPara;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
// Edge case: split at or before start — move everything
|
|
4186
|
+
if (offset <= 0) {
|
|
4187
|
+
tailPara.content = this.content;
|
|
4188
|
+
this.content = [];
|
|
4189
|
+
return tailPara;
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
// Walk content to find the split point
|
|
4193
|
+
let charPos = 0;
|
|
4194
|
+
let splitContentIndex = -1;
|
|
4195
|
+
|
|
4196
|
+
for (let i = 0; i < this.content.length; i++) {
|
|
4197
|
+
const item = this.content[i]!;
|
|
4198
|
+
|
|
4199
|
+
if (item instanceof Run) {
|
|
4200
|
+
const runLen = item.getText().length;
|
|
4201
|
+
const runEnd = charPos + runLen;
|
|
4202
|
+
|
|
4203
|
+
if (runEnd <= offset) {
|
|
4204
|
+
// Entire run stays in this paragraph
|
|
4205
|
+
charPos = runEnd;
|
|
4206
|
+
continue;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
if (charPos >= offset) {
|
|
4210
|
+
// Entire run goes to tail paragraph
|
|
4211
|
+
splitContentIndex = i;
|
|
4212
|
+
break;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
// Split falls within this run
|
|
4216
|
+
const localOffset = offset - charPos;
|
|
4217
|
+
const tailRun = item.splitAt(localOffset);
|
|
4218
|
+
|
|
4219
|
+
// Insert the tail run and mark everything after it for the new paragraph
|
|
4220
|
+
this.content.splice(i + 1, 0, tailRun);
|
|
4221
|
+
splitContentIndex = i + 1;
|
|
4222
|
+
break;
|
|
4223
|
+
} else {
|
|
4224
|
+
// Non-Run content: if we haven't reached the offset yet, keep it here
|
|
4225
|
+
// Once we've passed the offset, it moves to tail
|
|
4226
|
+
if (charPos >= offset) {
|
|
4227
|
+
splitContentIndex = i;
|
|
4228
|
+
break;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
// If no split point found (shouldn't happen given offset < totalLength),
|
|
4234
|
+
// but guard against it
|
|
4235
|
+
if (splitContentIndex === -1) {
|
|
4236
|
+
return tailPara;
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
// Move content from splitContentIndex onward to the tail paragraph
|
|
4240
|
+
tailPara.content = this.content.splice(splitContentIndex);
|
|
4241
|
+
|
|
4242
|
+
return tailPara;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
3792
4245
|
/**
|
|
3793
4246
|
* Sets paragraph borders
|
|
3794
4247
|
* @param borders - Border definitions for each side
|
|
@@ -4066,6 +4519,581 @@ export class Paragraph {
|
|
|
4066
4519
|
return replacementCount;
|
|
4067
4520
|
}
|
|
4068
4521
|
|
|
4522
|
+
/**
|
|
4523
|
+
* Replaces all occurrences of a string, searching across run boundaries
|
|
4524
|
+
*
|
|
4525
|
+
* A simplified alias for `replaceTextCrossRun()` with no options — just
|
|
4526
|
+
* find and replace. Case-insensitive. Handles Word-fragmented text.
|
|
4527
|
+
*
|
|
4528
|
+
* @param find - Text to search for
|
|
4529
|
+
* @param replace - Replacement text
|
|
4530
|
+
* @returns Number of replacements made
|
|
4531
|
+
*
|
|
4532
|
+
* @example
|
|
4533
|
+
* ```typescript
|
|
4534
|
+
* para.replaceAll('old', 'new');
|
|
4535
|
+
* para.replaceAll('{{name}}', 'Alice');
|
|
4536
|
+
* ```
|
|
4537
|
+
*/
|
|
4538
|
+
replaceAll(find: string, replace: string): number {
|
|
4539
|
+
return this.replaceTextCrossRun(find, replace);
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
/**
|
|
4543
|
+
* Searches for text across run boundaries, returning match positions
|
|
4544
|
+
*
|
|
4545
|
+
* Concatenates text from all runs and searches the combined string,
|
|
4546
|
+
* finding matches that may span multiple runs (e.g., `{{name}}` split
|
|
4547
|
+
* across runs as `{{` + `name` + `}}`). This is the read-only
|
|
4548
|
+
* counterpart to `replaceTextCrossRun()`.
|
|
4549
|
+
*
|
|
4550
|
+
* @param find - Text to search for (plain string)
|
|
4551
|
+
* @param options - Search options
|
|
4552
|
+
* @param options.caseSensitive - Match case exactly (default: false)
|
|
4553
|
+
* @returns Array of match objects with offset and matched text
|
|
4554
|
+
*
|
|
4555
|
+
* @example
|
|
4556
|
+
* ```typescript
|
|
4557
|
+
* // Find all placeholders, even if fragmented across runs
|
|
4558
|
+
* const matches = para.findTextCrossRun('{{name}}');
|
|
4559
|
+
* console.log(`Found ${matches.length} matches`);
|
|
4560
|
+
* for (const m of matches) {
|
|
4561
|
+
* console.log(` at offset ${m.offset}: "${m.text}"`);
|
|
4562
|
+
* }
|
|
4563
|
+
* ```
|
|
4564
|
+
*/
|
|
4565
|
+
findTextCrossRun(
|
|
4566
|
+
find: string,
|
|
4567
|
+
options?: { caseSensitive?: boolean }
|
|
4568
|
+
): { offset: number; text: string }[] {
|
|
4569
|
+
const caseSensitive = options?.caseSensitive ?? false;
|
|
4570
|
+
|
|
4571
|
+
// Build full text from runs only
|
|
4572
|
+
let fullText = '';
|
|
4573
|
+
for (const item of this.content) {
|
|
4574
|
+
if (item instanceof Run) {
|
|
4575
|
+
fullText += item.getText();
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
if (fullText.length === 0) return [];
|
|
4580
|
+
|
|
4581
|
+
const escaped = find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4582
|
+
const regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
|
|
4583
|
+
const results: { offset: number; text: string }[] = [];
|
|
4584
|
+
let m: RegExpExecArray | null;
|
|
4585
|
+
|
|
4586
|
+
while ((m = regex.exec(fullText)) !== null) {
|
|
4587
|
+
results.push({ offset: m.index, text: m[0]! });
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
return results;
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
/**
|
|
4594
|
+
* Finds and replaces text that may span across multiple runs
|
|
4595
|
+
*
|
|
4596
|
+
* Unlike `replaceText()` which searches each run independently, this method
|
|
4597
|
+
* concatenates text across all runs and searches the combined string. This
|
|
4598
|
+
* handles the common case where Word splits text like `{{name}}` across
|
|
4599
|
+
* multiple runs (e.g., `{{` in one run, `name` in another, `}}` in a third).
|
|
4600
|
+
*
|
|
4601
|
+
* The replacement text inherits formatting from the first run of the match.
|
|
4602
|
+
* Runs that are fully consumed by the match are removed; partially consumed
|
|
4603
|
+
* runs are trimmed.
|
|
4604
|
+
*
|
|
4605
|
+
* @param find - Text to search for (plain string, case-insensitive by default)
|
|
4606
|
+
* @param replace - Replacement text
|
|
4607
|
+
* @param options - Search options
|
|
4608
|
+
* @param options.caseSensitive - Match case exactly (default: false)
|
|
4609
|
+
* @returns Number of replacements made
|
|
4610
|
+
*
|
|
4611
|
+
* @example
|
|
4612
|
+
* ```typescript
|
|
4613
|
+
* // Handle Word-fragmented placeholders
|
|
4614
|
+
* // Paragraph has runs: ["{{", "name", "}}"]
|
|
4615
|
+
* para.replaceTextCrossRun('{{name}}', 'Alice');
|
|
4616
|
+
* // Now has single run: "Alice"
|
|
4617
|
+
* ```
|
|
4618
|
+
*
|
|
4619
|
+
* @example
|
|
4620
|
+
* ```typescript
|
|
4621
|
+
* // Works with normal (non-fragmented) text too
|
|
4622
|
+
* para.replaceTextCrossRun('old text', 'new text');
|
|
4623
|
+
* ```
|
|
4624
|
+
*/
|
|
4625
|
+
replaceTextCrossRun(
|
|
4626
|
+
find: string,
|
|
4627
|
+
replace: string,
|
|
4628
|
+
options?: { caseSensitive?: boolean }
|
|
4629
|
+
): number {
|
|
4630
|
+
const caseSensitive = options?.caseSensitive ?? false;
|
|
4631
|
+
|
|
4632
|
+
// Build run map: [{run, contentIndex, startOffset, endOffset}]
|
|
4633
|
+
const runMap: { run: Run; contentIndex: number; start: number; end: number }[] = [];
|
|
4634
|
+
let charPos = 0;
|
|
4635
|
+
for (let i = 0; i < this.content.length; i++) {
|
|
4636
|
+
const item = this.content[i]!;
|
|
4637
|
+
if (item instanceof Run) {
|
|
4638
|
+
const len = item.getText().length;
|
|
4639
|
+
if (len > 0) {
|
|
4640
|
+
runMap.push({ run: item, contentIndex: i, start: charPos, end: charPos + len });
|
|
4641
|
+
}
|
|
4642
|
+
charPos += len;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
if (runMap.length === 0) return 0;
|
|
4647
|
+
|
|
4648
|
+
// Get full concatenated text
|
|
4649
|
+
const fullText = runMap.map((r) => r.run.getText()).join('');
|
|
4650
|
+
|
|
4651
|
+
// Find all match positions
|
|
4652
|
+
const escaped = find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4653
|
+
const regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
|
|
4654
|
+
const matches: { start: number; end: number }[] = [];
|
|
4655
|
+
let m: RegExpExecArray | null;
|
|
4656
|
+
while ((m = regex.exec(fullText)) !== null) {
|
|
4657
|
+
matches.push({ start: m.index, end: m.index + m[0]!.length });
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
if (matches.length === 0) return 0;
|
|
4661
|
+
|
|
4662
|
+
// Process matches in reverse order to preserve earlier offsets
|
|
4663
|
+
for (let mi = matches.length - 1; mi >= 0; mi--) {
|
|
4664
|
+
const match = matches[mi]!;
|
|
4665
|
+
|
|
4666
|
+
// Find all runs that overlap this match
|
|
4667
|
+
const affectedRuns = runMap.filter((r) => r.end > match.start && r.start < match.end);
|
|
4668
|
+
|
|
4669
|
+
if (affectedRuns.length === 0) continue;
|
|
4670
|
+
|
|
4671
|
+
const firstAffected = affectedRuns[0]!;
|
|
4672
|
+
const lastAffected = affectedRuns[affectedRuns.length - 1]!;
|
|
4673
|
+
|
|
4674
|
+
// Calculate what to keep from the first and last runs
|
|
4675
|
+
const keepBefore =
|
|
4676
|
+
match.start > firstAffected.start
|
|
4677
|
+
? firstAffected.run.getText().slice(0, match.start - firstAffected.start)
|
|
4678
|
+
: '';
|
|
4679
|
+
const keepAfter =
|
|
4680
|
+
match.end < lastAffected.end
|
|
4681
|
+
? lastAffected.run.getText().slice(match.end - lastAffected.start)
|
|
4682
|
+
: '';
|
|
4683
|
+
|
|
4684
|
+
// Set the first affected run to: kept-before + replacement + kept-after
|
|
4685
|
+
firstAffected.run.setText(keepBefore + replace + keepAfter);
|
|
4686
|
+
|
|
4687
|
+
// Remove any middle/last runs that were fully or partially consumed
|
|
4688
|
+
// (iterate in reverse to preserve indices)
|
|
4689
|
+
for (let r = affectedRuns.length - 1; r >= 1; r--) {
|
|
4690
|
+
const runEntry = affectedRuns[r]!;
|
|
4691
|
+
const idx = this.content.indexOf(runEntry.run);
|
|
4692
|
+
if (idx !== -1) {
|
|
4693
|
+
this.content.splice(idx, 1);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
return matches.length;
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
/**
|
|
4702
|
+
* Deletes a character range from the paragraph
|
|
4703
|
+
*
|
|
4704
|
+
* Removes text in the [start, end) range. Runs fully within the range
|
|
4705
|
+
* are removed entirely; boundary runs are trimmed. Non-Run content is
|
|
4706
|
+
* not affected.
|
|
4707
|
+
*
|
|
4708
|
+
* @param start - Start character offset (0-based, inclusive)
|
|
4709
|
+
* @param end - End character offset (0-based, exclusive)
|
|
4710
|
+
* @returns This paragraph for chaining
|
|
4711
|
+
*
|
|
4712
|
+
* @example
|
|
4713
|
+
* ```typescript
|
|
4714
|
+
* const para = new Paragraph().addText('Hello Beautiful World');
|
|
4715
|
+
* para.deleteRange(5, 15);
|
|
4716
|
+
* para.getText(); // "Hello World"
|
|
4717
|
+
* ```
|
|
4718
|
+
*/
|
|
4719
|
+
deleteRange(start: number, end: number): this {
|
|
4720
|
+
if (start >= end) return this;
|
|
4721
|
+
|
|
4722
|
+
// Build run map
|
|
4723
|
+
const runMap: { index: number; start: number; end: number }[] = [];
|
|
4724
|
+
let charPos = 0;
|
|
4725
|
+
for (let i = 0; i < this.content.length; i++) {
|
|
4726
|
+
const item = this.content[i]!;
|
|
4727
|
+
if (item instanceof Run) {
|
|
4728
|
+
const len = item.getText().length;
|
|
4729
|
+
if (len > 0) {
|
|
4730
|
+
runMap.push({ index: i, start: charPos, end: charPos + len });
|
|
4731
|
+
}
|
|
4732
|
+
charPos += len;
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
// Process in reverse to preserve indices
|
|
4737
|
+
for (let m = runMap.length - 1; m >= 0; m--) {
|
|
4738
|
+
const entry = runMap[m]!;
|
|
4739
|
+
const run = this.content[entry.index] as Run;
|
|
4740
|
+
|
|
4741
|
+
// Skip runs entirely outside the range
|
|
4742
|
+
if (entry.end <= start || entry.start >= end) continue;
|
|
4743
|
+
|
|
4744
|
+
const runStart = entry.start;
|
|
4745
|
+
const runEnd = entry.end;
|
|
4746
|
+
|
|
4747
|
+
// How much of this run is in the delete range?
|
|
4748
|
+
const delStart = Math.max(0, start - runStart);
|
|
4749
|
+
const delEnd = Math.min(runEnd - runStart, end - runStart);
|
|
4750
|
+
const runLen = runEnd - runStart;
|
|
4751
|
+
|
|
4752
|
+
if (delStart === 0 && delEnd === runLen) {
|
|
4753
|
+
// Entire run is deleted
|
|
4754
|
+
this.content.splice(entry.index, 1);
|
|
4755
|
+
} else {
|
|
4756
|
+
// Partial deletion — keep the parts outside the range
|
|
4757
|
+
const textBefore = run.getText().slice(0, delStart);
|
|
4758
|
+
const textAfter = run.getText().slice(delEnd);
|
|
4759
|
+
run.setText(textBefore + textAfter);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
return this;
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
/**
|
|
4767
|
+
* Truncates paragraph text to a maximum length
|
|
4768
|
+
*
|
|
4769
|
+
* If the paragraph text exceeds `maxLength`, content beyond that point
|
|
4770
|
+
* is removed and an optional suffix (default `'...'`) is appended.
|
|
4771
|
+
* The suffix counts toward the total length, so the final text is at
|
|
4772
|
+
* most `maxLength` characters.
|
|
4773
|
+
*
|
|
4774
|
+
* @param maxLength - Maximum total character length (including suffix)
|
|
4775
|
+
* @param suffix - Text to append when truncated (default: '...')
|
|
4776
|
+
* @returns This paragraph for chaining
|
|
4777
|
+
*
|
|
4778
|
+
* @example
|
|
4779
|
+
* ```typescript
|
|
4780
|
+
* const para = new Paragraph().addText('The quick brown fox jumps over the lazy dog');
|
|
4781
|
+
* para.truncate(20);
|
|
4782
|
+
* para.getText(); // "The quick brown f..."
|
|
4783
|
+
*
|
|
4784
|
+
* para.truncate(10, ' [more]');
|
|
4785
|
+
* // getText() => "The [more]"
|
|
4786
|
+
* ```
|
|
4787
|
+
*/
|
|
4788
|
+
truncate(maxLength: number, suffix = '...'): this {
|
|
4789
|
+
const text = this.getText();
|
|
4790
|
+
if (text.length <= maxLength) return this;
|
|
4791
|
+
|
|
4792
|
+
const cutoff = Math.max(0, maxLength - suffix.length);
|
|
4793
|
+
this.deleteRange(cutoff, text.length);
|
|
4794
|
+
|
|
4795
|
+
// Append suffix as a new run (inherits no formatting — plain text indicator)
|
|
4796
|
+
if (suffix) {
|
|
4797
|
+
this.addText(suffix);
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
return this;
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4803
|
+
/**
|
|
4804
|
+
* Wraps existing paragraph content with prefix and/or suffix text
|
|
4805
|
+
*
|
|
4806
|
+
* Inserts a Run with the prefix text before existing content and
|
|
4807
|
+
* a Run with the suffix text after it. Both inherit no formatting
|
|
4808
|
+
* by default, but optional formatting can be provided.
|
|
4809
|
+
*
|
|
4810
|
+
* @param prefix - Text to prepend (empty string to skip)
|
|
4811
|
+
* @param suffix - Text to append (empty string to skip)
|
|
4812
|
+
* @param formatting - Optional formatting for both prefix and suffix runs
|
|
4813
|
+
* @returns This paragraph for chaining
|
|
4814
|
+
*
|
|
4815
|
+
* @example
|
|
4816
|
+
* ```typescript
|
|
4817
|
+
* const para = new Paragraph().addText('important');
|
|
4818
|
+
* para.wrap('[', ']');
|
|
4819
|
+
* para.getText(); // "[important]"
|
|
4820
|
+
*
|
|
4821
|
+
* // With formatting
|
|
4822
|
+
* para.wrap('NOTE: ', '', { bold: true });
|
|
4823
|
+
* ```
|
|
4824
|
+
*/
|
|
4825
|
+
wrap(prefix: string, suffix: string, formatting?: RunFormatting): this {
|
|
4826
|
+
if (prefix) {
|
|
4827
|
+
const prefixRun = new Run(prefix, formatting);
|
|
4828
|
+
this.content.unshift(prefixRun);
|
|
4829
|
+
}
|
|
4830
|
+
if (suffix) {
|
|
4831
|
+
const suffixRun = new Run(suffix, formatting);
|
|
4832
|
+
this.content.push(suffixRun);
|
|
4833
|
+
}
|
|
4834
|
+
return this;
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
/**
|
|
4838
|
+
* Returns the Run that contains the character at the given offset
|
|
4839
|
+
*
|
|
4840
|
+
* Walks through Run elements in the paragraph, counting characters,
|
|
4841
|
+
* and returns the Run that contains the specified offset along with
|
|
4842
|
+
* the local offset within that run. Non-Run content is skipped.
|
|
4843
|
+
*
|
|
4844
|
+
* @param offset - Character position (0-based)
|
|
4845
|
+
* @returns Object with the Run and the local offset within it, or
|
|
4846
|
+
* undefined if the offset is out of range or hits non-Run content
|
|
4847
|
+
*
|
|
4848
|
+
* @example
|
|
4849
|
+
* ```typescript
|
|
4850
|
+
* const para = new Paragraph();
|
|
4851
|
+
* para.addRun(new Run('Hello ', { bold: true }));
|
|
4852
|
+
* para.addRun(new Run('World'));
|
|
4853
|
+
*
|
|
4854
|
+
* const result = para.getRunAtOffset(8);
|
|
4855
|
+
* // result.run.getText() === 'World'
|
|
4856
|
+
* // result.localOffset === 2 (the 'r' in 'World')
|
|
4857
|
+
* ```
|
|
4858
|
+
*/
|
|
4859
|
+
getRunAtOffset(offset: number): { run: Run; localOffset: number } | undefined {
|
|
4860
|
+
if (offset < 0) return undefined;
|
|
4861
|
+
|
|
4862
|
+
let charPos = 0;
|
|
4863
|
+
for (const item of this.content) {
|
|
4864
|
+
if (!(item instanceof Run)) continue;
|
|
4865
|
+
|
|
4866
|
+
const len = item.getText().length;
|
|
4867
|
+
if (charPos + len > offset) {
|
|
4868
|
+
return { run: item, localOffset: offset - charPos };
|
|
4869
|
+
}
|
|
4870
|
+
charPos += len;
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
return undefined;
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
/**
|
|
4877
|
+
* Returns the RunFormatting active at a given character position
|
|
4878
|
+
*
|
|
4879
|
+
* Finds the Run containing the character at `offset` and returns its
|
|
4880
|
+
* formatting. Useful for format inspection, format painting, and building
|
|
4881
|
+
* rich text cursors.
|
|
4882
|
+
*
|
|
4883
|
+
* @param offset - Character position (0-based)
|
|
4884
|
+
* @returns RunFormatting at that position, or undefined if out of range
|
|
4885
|
+
*
|
|
4886
|
+
* @example
|
|
4887
|
+
* ```typescript
|
|
4888
|
+
* const para = new Paragraph();
|
|
4889
|
+
* para.addRun(new Run('Bold text', { bold: true, color: 'FF0000' }));
|
|
4890
|
+
* para.addRun(new Run(' plain text'));
|
|
4891
|
+
*
|
|
4892
|
+
* const fmt = para.getFormattingAtOffset(3);
|
|
4893
|
+
* // fmt.bold === true
|
|
4894
|
+
* // fmt.color === 'FF0000'
|
|
4895
|
+
*
|
|
4896
|
+
* const fmt2 = para.getFormattingAtOffset(12);
|
|
4897
|
+
* // fmt2.bold === undefined
|
|
4898
|
+
* ```
|
|
4899
|
+
*/
|
|
4900
|
+
getFormattingAtOffset(offset: number): RunFormatting | undefined {
|
|
4901
|
+
const result = this.getRunAtOffset(offset);
|
|
4902
|
+
return result ? result.run.getFormatting() : undefined;
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
/**
|
|
4906
|
+
* Applies formatting to a character range across runs
|
|
4907
|
+
*
|
|
4908
|
+
* Splits runs at the range boundaries as needed, then applies the given
|
|
4909
|
+
* formatting properties to every run that falls within [start, end).
|
|
4910
|
+
* Only modifies Run elements — hyperlinks, revisions, fields, and other
|
|
4911
|
+
* non-Run content are skipped (their characters still count toward offsets).
|
|
4912
|
+
*
|
|
4913
|
+
* Each property in the formatting object is applied via the corresponding
|
|
4914
|
+
* setter method (e.g. `bold` → `setBold()`, `color` → `setColor()`).
|
|
4915
|
+
*
|
|
4916
|
+
* @param start - Start character offset (0-based, inclusive)
|
|
4917
|
+
* @param end - End character offset (0-based, exclusive)
|
|
4918
|
+
* @param formatting - Partial RunFormatting to apply to the range
|
|
4919
|
+
* @returns This paragraph for chaining
|
|
4920
|
+
*
|
|
4921
|
+
* @example
|
|
4922
|
+
* ```typescript
|
|
4923
|
+
* // Bold "World" in "Hello World"
|
|
4924
|
+
* const para = new Paragraph().addText('Hello World');
|
|
4925
|
+
* para.applyFormattingToRange(6, 11, { bold: true });
|
|
4926
|
+
* ```
|
|
4927
|
+
*
|
|
4928
|
+
* @example
|
|
4929
|
+
* ```typescript
|
|
4930
|
+
* // Highlight a middle section
|
|
4931
|
+
* const para = new Paragraph().addText('The quick brown fox');
|
|
4932
|
+
* para.applyFormattingToRange(4, 9, {
|
|
4933
|
+
* bold: true,
|
|
4934
|
+
* color: 'FF0000',
|
|
4935
|
+
* highlight: 'yellow',
|
|
4936
|
+
* });
|
|
4937
|
+
* // Results in 3 runs: "The " | "quick" (bold red highlighted) | " brown fox"
|
|
4938
|
+
* ```
|
|
4939
|
+
*/
|
|
4940
|
+
applyFormattingToRange(start: number, end: number, formatting: Partial<RunFormatting>): this {
|
|
4941
|
+
if (start >= end) return this;
|
|
4942
|
+
|
|
4943
|
+
// Build a map of content indices to their character offset ranges,
|
|
4944
|
+
// then work backwards to avoid index shifting during splicing.
|
|
4945
|
+
const contentMap: { index: number; start: number; end: number }[] = [];
|
|
4946
|
+
let charPos = 0;
|
|
4947
|
+
for (let i = 0; i < this.content.length; i++) {
|
|
4948
|
+
const item = this.content[i]!;
|
|
4949
|
+
let len = 0;
|
|
4950
|
+
if (item instanceof Run) {
|
|
4951
|
+
len = item.getText().length;
|
|
4952
|
+
}
|
|
4953
|
+
// Non-Run items don't contribute characters for our purposes
|
|
4954
|
+
// (they're skipped but still occupy content array slots)
|
|
4955
|
+
if (len > 0) {
|
|
4956
|
+
contentMap.push({ index: i, start: charPos, end: charPos + len });
|
|
4957
|
+
}
|
|
4958
|
+
charPos += len;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
// Process runs in reverse order so splice indices stay valid
|
|
4962
|
+
for (let m = contentMap.length - 1; m >= 0; m--) {
|
|
4963
|
+
const entry = contentMap[m]!;
|
|
4964
|
+
const run = this.content[entry.index] as Run;
|
|
4965
|
+
|
|
4966
|
+
// Skip runs entirely outside the range
|
|
4967
|
+
if (entry.end <= start || entry.start >= end) continue;
|
|
4968
|
+
|
|
4969
|
+
const runStart = entry.start;
|
|
4970
|
+
const runEnd = entry.end;
|
|
4971
|
+
|
|
4972
|
+
// Determine split points relative to this run
|
|
4973
|
+
const splitStart = Math.max(0, start - runStart);
|
|
4974
|
+
const splitEnd = Math.min(runEnd - runStart, end - runStart);
|
|
4975
|
+
const runLen = runEnd - runStart;
|
|
4976
|
+
|
|
4977
|
+
if (splitStart === 0 && splitEnd === runLen) {
|
|
4978
|
+
// Entire run is within range — just apply formatting
|
|
4979
|
+
this.applyFormattingToRun(run, formatting);
|
|
4980
|
+
} else if (splitStart === 0) {
|
|
4981
|
+
// Range covers beginning of run — split off the tail
|
|
4982
|
+
const tail = run.splitAt(splitEnd);
|
|
4983
|
+
this.applyFormattingToRun(run, formatting);
|
|
4984
|
+
this.content.splice(entry.index + 1, 0, tail);
|
|
4985
|
+
} else if (splitEnd === runLen) {
|
|
4986
|
+
// Range covers end of run — split off the head
|
|
4987
|
+
const tail = run.splitAt(splitStart);
|
|
4988
|
+
this.applyFormattingToRun(tail, formatting);
|
|
4989
|
+
this.content.splice(entry.index + 1, 0, tail);
|
|
4990
|
+
} else {
|
|
4991
|
+
// Range is in the middle — three-way split
|
|
4992
|
+
const mid = run.splitAt(splitStart);
|
|
4993
|
+
const tail = mid.splitAt(splitEnd - splitStart);
|
|
4994
|
+
this.applyFormattingToRun(mid, formatting);
|
|
4995
|
+
this.content.splice(entry.index + 1, 0, mid, tail);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
return this;
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
/**
|
|
5003
|
+
* Applies partial RunFormatting to a run via setter methods.
|
|
5004
|
+
* @internal
|
|
5005
|
+
*/
|
|
5006
|
+
private applyFormattingToRun(run: Run, formatting: Partial<RunFormatting>): void {
|
|
5007
|
+
if (formatting.bold !== undefined) run.setBold(formatting.bold);
|
|
5008
|
+
if (formatting.italic !== undefined) run.setItalic(formatting.italic);
|
|
5009
|
+
if (formatting.underline !== undefined) run.setUnderline(formatting.underline);
|
|
5010
|
+
if (formatting.strike !== undefined) run.setStrike(formatting.strike);
|
|
5011
|
+
if (formatting.subscript !== undefined) run.setSubscript(formatting.subscript);
|
|
5012
|
+
if (formatting.superscript !== undefined) run.setSuperscript(formatting.superscript);
|
|
5013
|
+
if (formatting.smallCaps !== undefined) run.setSmallCaps(formatting.smallCaps);
|
|
5014
|
+
if (formatting.allCaps !== undefined) run.setAllCaps(formatting.allCaps);
|
|
5015
|
+
if (formatting.font !== undefined) run.setFont(formatting.font);
|
|
5016
|
+
if (formatting.size !== undefined) run.setSize(formatting.size);
|
|
5017
|
+
if (formatting.color !== undefined) run.setColor(formatting.color);
|
|
5018
|
+
if (formatting.highlight !== undefined) run.setHighlight(formatting.highlight);
|
|
5019
|
+
if (formatting.characterSpacing !== undefined)
|
|
5020
|
+
run.setCharacterSpacing(formatting.characterSpacing);
|
|
5021
|
+
if (formatting.vanish !== undefined) run.setVanish(formatting.vanish);
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
/**
|
|
5025
|
+
* Merges adjacent runs that have identical formatting
|
|
5026
|
+
*
|
|
5027
|
+
* Walks the content array and combines consecutive Run elements whose
|
|
5028
|
+
* formatting is deeply equal. Non-Run content (hyperlinks, revisions,
|
|
5029
|
+
* fields, etc.) acts as a boundary — runs on opposite sides are never
|
|
5030
|
+
* merged even if their formatting matches.
|
|
5031
|
+
*
|
|
5032
|
+
* This is useful after operations that fragment runs, such as
|
|
5033
|
+
* `applyFormattingToRange()` or `Run.splitAt()`, to reduce file size
|
|
5034
|
+
* and simplify the content model.
|
|
5035
|
+
*
|
|
5036
|
+
* @returns Number of runs that were eliminated by merging
|
|
5037
|
+
*
|
|
5038
|
+
* @example
|
|
5039
|
+
* ```typescript
|
|
5040
|
+
* const para = new Paragraph();
|
|
5041
|
+
* para.addRun(new Run('Hello ', { bold: true }));
|
|
5042
|
+
* para.addRun(new Run('World', { bold: true }));
|
|
5043
|
+
* para.addRun(new Run('!'));
|
|
5044
|
+
*
|
|
5045
|
+
* const merged = para.consolidateRuns();
|
|
5046
|
+
* // merged === 1 (two bold runs became one)
|
|
5047
|
+
* // para now has 2 runs: "Hello World" (bold) and "!"
|
|
5048
|
+
* ```
|
|
5049
|
+
*
|
|
5050
|
+
* @example
|
|
5051
|
+
* ```typescript
|
|
5052
|
+
* // Clean up after applyFormattingToRange
|
|
5053
|
+
* para.applyFormattingToRange(0, 5, { bold: true });
|
|
5054
|
+
* para.applyFormattingToRange(5, 10, { bold: true });
|
|
5055
|
+
* para.consolidateRuns(); // merges the two bold fragments
|
|
5056
|
+
* ```
|
|
5057
|
+
*/
|
|
5058
|
+
consolidateRuns(): number {
|
|
5059
|
+
if (this.content.length < 2) return 0;
|
|
5060
|
+
|
|
5061
|
+
const consolidated: ParagraphContent[] = [];
|
|
5062
|
+
let eliminated = 0;
|
|
5063
|
+
|
|
5064
|
+
for (const item of this.content) {
|
|
5065
|
+
if (!(item instanceof Run)) {
|
|
5066
|
+
// Non-run content acts as a merge boundary
|
|
5067
|
+
consolidated.push(item);
|
|
5068
|
+
continue;
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
const prev = consolidated.length > 0 ? consolidated[consolidated.length - 1] : undefined;
|
|
5072
|
+
|
|
5073
|
+
if (
|
|
5074
|
+
prev instanceof Run &&
|
|
5075
|
+
isEqualFormatting(
|
|
5076
|
+
prev.getFormatting() as unknown as Record<string, unknown>,
|
|
5077
|
+
item.getFormatting() as unknown as Record<string, unknown>
|
|
5078
|
+
)
|
|
5079
|
+
) {
|
|
5080
|
+
// Merge: append item's content to prev run
|
|
5081
|
+
const mergedContent = [...prev.getContent(), ...item.getContent()];
|
|
5082
|
+
const mergedRun = Run.createFromContent(mergedContent, deepClone(prev.getFormatting()));
|
|
5083
|
+
consolidated[consolidated.length - 1] = mergedRun;
|
|
5084
|
+
eliminated++;
|
|
5085
|
+
} else {
|
|
5086
|
+
consolidated.push(item);
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
if (eliminated > 0) {
|
|
5091
|
+
this.content = consolidated;
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
return eliminated;
|
|
5095
|
+
}
|
|
5096
|
+
|
|
4069
5097
|
/**
|
|
4070
5098
|
* Merges another paragraph's content into this one
|
|
4071
5099
|
*
|