@docx4j/core-ts 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +34 -3
  3. package/dist/html/convert.mjs +40 -15
  4. package/dist/html/elements.mjs +2 -1
  5. package/dist/model/color/ColorMath.d.mts +29 -0
  6. package/dist/model/color/ColorMath.mjs +146 -0
  7. package/dist/model/color/ColorTables.d.mts +6 -0
  8. package/dist/model/color/ColorTables.mjs +83 -0
  9. package/dist/model/color/DrawingMLColors.d.mts +52 -0
  10. package/dist/model/color/DrawingMLColors.mjs +374 -0
  11. package/dist/model/color/ResolvedColor.d.mts +36 -0
  12. package/dist/model/color/ResolvedColor.mjs +65 -0
  13. package/dist/model/color/SpreadsheetColors.d.mts +36 -0
  14. package/dist/model/color/SpreadsheetColors.mjs +84 -0
  15. package/dist/model/color/StyleFill.d.mts +28 -0
  16. package/dist/model/color/StyleFill.mjs +21 -0
  17. package/dist/model/color/ThemeColors.d.mts +86 -0
  18. package/dist/model/color/ThemeColors.mjs +304 -0
  19. package/dist/model/color/WordColors.d.mts +71 -0
  20. package/dist/model/color/WordColors.mjs +121 -0
  21. package/dist/model/color/index.d.mts +6 -0
  22. package/dist/model/color/index.mjs +30 -0
  23. package/dist/model/content/Body.d.mts +153 -19
  24. package/dist/model/content/Body.mjs +570 -131
  25. package/dist/model/content/Comment.d.mts +21 -3
  26. package/dist/model/content/Comment.mjs +202 -19
  27. package/dist/model/content/ContentControl.d.mts +197 -18
  28. package/dist/model/content/ContentControl.mjs +664 -95
  29. package/dist/model/content/Font.d.mts +67 -17
  30. package/dist/model/content/Font.mjs +114 -35
  31. package/dist/model/content/InlinePicture.d.mts +82 -7
  32. package/dist/model/content/InlinePicture.mjs +221 -28
  33. package/dist/model/content/List.mjs +13 -1
  34. package/dist/model/content/Paragraph.d.mts +213 -14
  35. package/dist/model/content/Paragraph.mjs +936 -100
  36. package/dist/model/content/Range.d.mts +242 -11
  37. package/dist/model/content/Range.mjs +975 -119
  38. package/dist/model/content/ResolvedView.d.mts +104 -0
  39. package/dist/model/content/ResolvedView.mjs +596 -0
  40. package/dist/model/content/Table.d.mts +29 -3
  41. package/dist/model/content/Table.mjs +109 -33
  42. package/dist/model/content/TrackedChange.d.mts +301 -16
  43. package/dist/model/content/TrackedChange.mjs +1284 -158
  44. package/dist/model/content/binder.d.mts +2 -1
  45. package/dist/model/content/binder.mjs +48 -1
  46. package/dist/model/content/comments.d.mts +20 -6
  47. package/dist/model/content/comments.mjs +67 -57
  48. package/dist/model/content/generalException.d.mts +18 -0
  49. package/dist/model/content/generalException.mjs +24 -0
  50. package/dist/model/content/index.d.mts +8 -3
  51. package/dist/model/content/index.mjs +7 -2
  52. package/dist/model/content/mathText.d.mts +21 -0
  53. package/dist/model/content/mathText.mjs +267 -0
  54. package/dist/model/content/officeText.d.mts +27 -0
  55. package/dist/model/content/officeText.mjs +74 -0
  56. package/dist/model/content/outline.d.mts +5 -0
  57. package/dist/model/content/outline.mjs +13 -0
  58. package/dist/model/content/placeholders.d.mts +110 -0
  59. package/dist/model/content/placeholders.mjs +253 -0
  60. package/dist/model/content/relationships.d.mts +30 -0
  61. package/dist/model/content/relationships.mjs +133 -0
  62. package/dist/model/content/search.d.mts +72 -0
  63. package/dist/model/content/search.mjs +204 -2
  64. package/dist/model/content/spanEdits.d.mts +159 -0
  65. package/dist/model/content/spanEdits.mjs +728 -0
  66. package/dist/model/content/story.d.mts +44 -0
  67. package/dist/model/content/story.mjs +92 -0
  68. package/dist/model/content/tableColumns.d.mts +1 -1
  69. package/dist/model/content/tableColumns.mjs +108 -80
  70. package/dist/model/content/tableRevisions.d.mts +11 -3
  71. package/dist/model/content/tableRevisions.mjs +72 -42
  72. package/dist/model/content/tableRules.d.mts +30 -0
  73. package/dist/model/content/tableRules.mjs +100 -0
  74. package/dist/model/content/textBoxes.d.mts +18 -0
  75. package/dist/model/content/textBoxes.mjs +105 -0
  76. package/dist/model/content/trackedChangeFilter.d.mts +39 -0
  77. package/dist/model/content/trackedChangeFilter.mjs +81 -0
  78. package/dist/model/content/tracking.d.mts +28 -0
  79. package/dist/model/content/tracking.mjs +149 -10
  80. package/dist/model/content/tree.d.mts +212 -13
  81. package/dist/model/content/tree.mjs +356 -35
  82. package/dist/model/customxml/CustomXmlPartCollection.d.mts +8 -0
  83. package/dist/model/customxml/CustomXmlPartCollection.mjs +34 -11
  84. package/dist/model/customxml/XmlMapping.d.mts +1 -1
  85. package/dist/model/customxml/XmlMapping.mjs +11 -0
  86. package/dist/model/customxml/bindings.d.mts +21 -2
  87. package/dist/model/customxml/bindings.mjs +76 -32
  88. package/dist/model/customxml/kinds.d.mts +8 -3
  89. package/dist/model/customxml/kinds.mjs +26 -5
  90. package/dist/model/customxml/picturePlaceholder.d.mts +17 -0
  91. package/dist/model/customxml/picturePlaceholder.mjs +32 -0
  92. package/dist/model/customxml/pictures.d.mts +9 -5
  93. package/dist/model/customxml/pictures.mjs +31 -13
  94. package/dist/model/customxml/xhtml.mjs +8 -2
  95. package/dist/model/fonts/families.generated.mjs +2 -2
  96. package/dist/model/fonts/registry.mjs +7 -1
  97. package/dist/model/fonts/substitutions.generated.mjs +2 -2
  98. package/dist/model/fonts/themes.generated.mjs +1 -1
  99. package/dist/model/index.d.mts +2 -0
  100. package/dist/model/index.mjs +4 -0
  101. package/dist/model/listnumbering/Emulator.d.mts +37 -5
  102. package/dist/model/listnumbering/Emulator.mjs +63 -9
  103. package/dist/model/listnumbering/definitions.mjs +18 -0
  104. package/dist/model/properties/PropertyResolver.d.mts +87 -7
  105. package/dist/model/properties/PropertyResolver.mjs +197 -17
  106. package/dist/model/properties/catalogue.d.mts +32 -4
  107. package/dist/model/properties/catalogue.mjs +115 -36
  108. package/dist/model/properties/log.d.mts +1 -1
  109. package/dist/model/properties/log.mjs +5 -5
  110. package/dist/model/properties/table.d.mts +16 -2
  111. package/dist/model/properties/table.mjs +17 -5
  112. package/dist/model/text/Extractor.d.mts +289 -0
  113. package/dist/model/text/Extractor.mjs +2162 -0
  114. package/dist/model/text/LinkTargets.d.mts +45 -0
  115. package/dist/model/text/LinkTargets.mjs +170 -0
  116. package/dist/model/text/MarkdownExporter.d.mts +140 -0
  117. package/dist/model/text/MarkdownExporter.mjs +255 -0
  118. package/dist/model/text/PlainText.d.mts +29 -0
  119. package/dist/model/text/PlainText.mjs +539 -0
  120. package/dist/model/text/TextContext.d.mts +161 -0
  121. package/dist/model/text/TextContext.mjs +425 -0
  122. package/dist/model/text/TextOptions.d.mts +150 -0
  123. package/dist/model/text/TextOptions.mjs +278 -0
  124. package/dist/model/text/TextResult.d.mts +504 -0
  125. package/dist/model/text/TextResult.mjs +526 -0
  126. package/dist/model/text/Texts.d.mts +99 -0
  127. package/dist/model/text/Texts.mjs +187 -0
  128. package/dist/model/text/WmlToMarkdown.d.mts +120 -0
  129. package/dist/model/text/WmlToMarkdown.mjs +911 -0
  130. package/dist/model/text/commonmark.d.mts +121 -0
  131. package/dist/model/text/commonmark.mjs +655 -0
  132. package/dist/model/text/index.d.mts +7 -0
  133. package/dist/model/text/index.mjs +8 -0
  134. package/dist/model/text/normalize.d.mts +8 -0
  135. package/dist/model/text/normalize.mjs +13 -0
  136. package/dist/model/text/outline.d.mts +4 -0
  137. package/dist/model/text/outline.mjs +71 -0
  138. package/dist/office-js/fragment.d.mts +7 -5
  139. package/dist/office-js/fragment.mjs +63 -14
  140. package/dist/office-js/proxy.d.mts +8 -0
  141. package/dist/office-js/proxy.mjs +29 -0
  142. package/dist/office-js/run.mjs +2 -0
  143. package/dist/office-js/supported.generated.mjs +16 -0
  144. package/dist/office-js/toApiScript.mjs +21 -5
  145. package/dist/opc/Load.d.mts +9 -2
  146. package/dist/opc/Save.d.mts +17 -0
  147. package/dist/opc/Save.mjs +12 -2
  148. package/dist/opc/index.d.mts +3 -3
  149. package/dist/opc/index.mjs +2 -2
  150. package/dist/opc/mce/preprocessor.d.mts +88 -7
  151. package/dist/opc/mce/preprocessor.mjs +144 -37
  152. package/dist/opc/mce/understood.d.mts +34 -0
  153. package/dist/opc/mce/understood.mjs +55 -3
  154. package/dist/packages/OpcPackage.d.mts +20 -0
  155. package/dist/packages/OpcPackage.mjs +16 -3
  156. package/dist/packages/WordprocessingMLPackage.d.mts +77 -7
  157. package/dist/packages/WordprocessingMLPackage.mjs +242 -11
  158. package/dist/parts/DefaultXmlPart.mjs +2 -2
  159. package/dist/parts/Part.d.mts +10 -2
  160. package/dist/parts/XmlPart.d.mts +35 -32
  161. package/dist/parts/XmlPart.mjs +107 -107
  162. package/dist/parts/wml/comments.mjs +30 -0
  163. package/dist/parts/wml/index.mjs +12 -0
  164. package/dist/xml/dom.d.mts +7 -0
  165. package/dist/xml/dom.mjs +18 -0
  166. package/package.json +2 -2
@@ -1,49 +1,362 @@
1
- import { Font } from './Font.mjs';
1
+ import { Font, MixedFont, readingOf, markHolderOf } from './Font.mjs';
2
2
  import { runFontSelectorOf } from '../fonts/lookup.mjs';
3
- import { typeNameOf, linkParents, runItemsOf, runsOf } from './tree.mjs';
3
+ import { typeNameOf, linkParents, runItemsOf, runsOf, revisionKindOf, WHOLE } from './tree.mjs';
4
4
  import { Docx4JException } from '../../opc/exceptions.mjs';
5
- import { ContentControl } from './ContentControl.mjs';
5
+ import { NotSupportedError } from '../../office-js/errors.mjs';
6
+ import { ContentControl, assertControlHolds, controlsOfTypes, showsPlaceholderWhenMade, BLOCK_CONTROL } from './ContentControl.mjs';
7
+ import { showPicturePlaceholder } from '../customxml/picturePlaceholder.mjs';
6
8
  import { sdt as sdtOf, nextSdtId, applyRunOptions, t as textElement } from '@docx4j/generated-objects-ts/builders/wml';
7
9
  import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
8
10
  import { Namespaces } from '../../parts/Namespaces.mjs';
9
11
  import { controlIdScope, sdtKindFor, declareSdtExtensions } from '../customxml/insert.mjs';
10
12
  import { contentOf } from './ooxml.mjs';
11
13
  import { markHolderInserted } from './tracking.mjs';
12
- import { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
14
+ import { officeMatches, wildcardMatchesAcross, refuseWildcardMarkReplacement, expandReplacement, paragraphMarkPieces, refuseMarkReplacement } from './search.mjs';
13
15
  import { commentApi } from './comments.mjs';
16
+ import { storyLayout, storyOf } from './story.mjs';
17
+ import { styleNameOf, builtInOf } from './styles.mjs';
18
+ import { GeneralException } from './generalException.mjs';
19
+ import { extentOf, assertTrackable, deleteSpan, firstCharacterOf, placeReplacement, insertParagraphAt, formatSpan } from './spanEdits.mjs';
14
20
  /**
15
- * A subset of Office JS `Word.Range`: a span of text within one paragraph, [start, end) in the
16
- * paragraph's text. Positions are recomputed from the tree on every use, so a range stays
17
- * valid across edits made through it; edits made elsewhere may shift it.
21
+ * A subset of Office JS `Word.Range`: a span of text from an offset in one paragraph to an offset in the same
22
+ * paragraph or a later one of the same story, [start, end), `start` in `paragraph`'s text and `end` in
23
+ * `endParagraph`'s. Positions are recomputed from the tree on every use, so a range stays valid across edits
24
+ * made through it; edits made elsewhere may shift it.
25
+ *
26
+ * A range within one paragraph is what it has been since CR-002: `endParagraph` is `paragraph`. A range across
27
+ * paragraphs (CR-014 phase 1) reads as Office JS's does (check 52): its `text` in `body.text`'s form between
28
+ * its ends, `paragraphs` every paragraph it touches (a table's cells' among them), `font` `null` where the runs
29
+ * differ, `search`, the tracked changes and the comments over all of it. Its writes (phase 2) are Word's too:
30
+ * `delete` and `insertText('Replace')` join the paragraphs in the last one's properties, `font` formats every
31
+ * run and mark in it, `insertText` goes at its ends, and `insertParagraph` splits the paragraph at its start or
32
+ * end; the rest of its writes throw `NotSupportedError`.
33
+ *
34
+ * A picture has a place in the text and no text of its own, so a picture at the range's start or
35
+ * end offset may be inside it or outside it. `whole` says: a range from `Paragraph.getRange('Whole')`
36
+ * or `'Content'` holds the paragraph's whole content, the pictures at its edges too, and deleting or
37
+ * replacing it deletes them; any other range holds the pictures strictly inside it, so that a search
38
+ * hit replaced does not take the picture beside it (CR-002 section 48).
18
39
  */
19
40
  export class Range {
20
- constructor(paragraph, start, end) {
21
- this.paragraph = paragraph;
41
+ constructor(paragraph, start, end, whole = false,
42
+ /**
43
+ * A paragraph's 'Whole' range: its text ends with the paragraph's mark, as Office JS's does (check 48:
44
+ * `\r`, and `\t` for a cell's last paragraph, the cell's end), where 'Content' ends with its text.
45
+ * The mark is no offset: every edit and search works on the text before it.
46
+ */
47
+ mark = false,
48
+ /** The paragraph holding `end`, where it isn't `paragraph` (CR-014 phase 1). */
49
+ endParagraph,
50
+ /**
51
+ * What the range's text ends with in place of `endParagraph`'s mark, where `mark` is set: a table's
52
+ * 'Whole' range ends with its last row's end, `\r\n` (check 52).
53
+ */
54
+ endMark) {
22
55
  this.start = start;
23
56
  this.end = end;
57
+ this.startParagraph = paragraph;
58
+ this.lastParagraph = endParagraph !== null && endParagraph !== void 0 ? endParagraph : paragraph;
59
+ this.holdsWhole = whole;
60
+ this.holdsMark = mark;
61
+ this.lastMarkText = endMark;
62
+ }
63
+ /** @internal Marks this range as its paragraph's End or Content range (`Paragraph.getRange`); returns it. */
64
+ atEndOf(kind) {
65
+ this.endOf = kind;
66
+ return this;
67
+ }
68
+ /** Whether text put at `location` is written at the paragraph's End: its End range's, at any location; its Content range's End and After. */
69
+ writesAtParagraphEnd(location) {
70
+ if (!this.endOf || this.spansParagraphs || this.end !== this.paragraph.text.length)
71
+ return false;
72
+ return this.endOf === 'End' ? this.start === this.end : location === 'End' || location === 'After';
73
+ }
74
+ /** The paragraph holding `start`. */
75
+ get paragraph() {
76
+ return this.startParagraph;
77
+ }
78
+ /** The paragraph holding `end`: `paragraph` for a range within one paragraph (CR-014 phase 1). */
79
+ get endParagraph() {
80
+ return this.lastParagraph;
81
+ }
82
+ /** The range holds its paragraphs' whole content, the pictures at its edges too (CR-002 section 48). */
83
+ get whole() {
84
+ return this.holdsWhole;
85
+ }
86
+ /** The range's text ends with its last paragraph's mark: a 'Whole' range. */
87
+ get mark() {
88
+ return this.holdsMark;
89
+ }
90
+ /** What the text ends with in place of the last paragraph's mark: a table's 'Whole' range's `\r\n`. */
91
+ get endMark() {
92
+ return this.lastMarkText;
93
+ }
94
+ /**
95
+ * True where the range holds its last paragraph's mark (a 'Whole' range ending at the paragraph's end), or a
96
+ * table's end in its place: what `delete` takes and `Replace` keeps (check 52, W1 and W2). An extension.
97
+ */
98
+ get holdsEndMark() {
99
+ return this.holdsMark && this.end >= this.lastParagraph.text.length;
100
+ }
101
+ /** The range collapsed where an edit through it closed it up, or over what the edit put there. */
102
+ /**
103
+ * This range moved onto another's span: its paragraphs and offsets (a replacement's, CR-002 section 60); `fresh`, it
104
+ * no longer holds a mark or a paragraph's whole content either, as `landAt` leaves one.
105
+ */
106
+ cover(r, fresh = false) {
107
+ this.startParagraph = r.paragraph;
108
+ this.lastParagraph = r.endParagraph;
109
+ this.start = r.start;
110
+ this.end = r.end;
111
+ if (fresh) {
112
+ this.holdsWhole = false;
113
+ this.holdsMark = false;
114
+ this.lastMarkText = undefined;
115
+ }
116
+ }
117
+ landAt(paragraph, start, end = start) {
118
+ this.startParagraph = paragraph;
119
+ this.lastParagraph = paragraph;
120
+ this.start = start;
121
+ this.end = end;
122
+ this.holdsWhole = false;
123
+ this.holdsMark = false;
124
+ this.lastMarkText = undefined;
125
+ }
126
+ /** True for a range across paragraphs: its ends are in two (extension; CR-014). */
127
+ get spansParagraphs() {
128
+ return this.endParagraph.element !== this.paragraph.element;
129
+ }
130
+ /** The edges `Paragraph.splice` is given: both for a whole range. */
131
+ get edges() {
132
+ return this.whole ? WHOLE : undefined;
133
+ }
134
+ /** The story's layout, and where this range's two paragraphs are in it. */
135
+ located() {
136
+ const layout = storyLayout(storyOf(this.paragraph.p).items, (element, container) => this.viewOf(element, container).text);
137
+ const first = layout.indexOf(this.paragraph.p);
138
+ const last = layout.indexOf(this.endParagraph.p);
139
+ if (first < 0 || last < 0)
140
+ throw new Docx4JException('This range\'s paragraphs are not in one story as a reader reads it');
141
+ return { layout, first, last };
142
+ }
143
+ /** The view of a paragraph of this range's story: this range's own where it is one of its two. */
144
+ viewOf(element, container) {
145
+ if (element === this.paragraph.element)
146
+ return this.paragraph;
147
+ if (element === this.endParagraph.element)
148
+ return this.endParagraph;
149
+ return this.paragraph.viewOf(element, container);
150
+ }
151
+ /**
152
+ * The range as one range per paragraph it touches: its part of the first, the whole of each between, its
153
+ * part of the last. A range within one paragraph is itself.
154
+ */
155
+ pieces() {
156
+ if (!this.spansParagraphs)
157
+ return [this];
158
+ const { layout, first, last } = this.located();
159
+ return layout.paragraphs.slice(first, last + 1).map((sp, i, all) => {
160
+ const p = this.viewOf(sp.element, sp.container);
161
+ if (i === 0)
162
+ return new Range(p, this.start, p.text.length, this.whole);
163
+ if (i === all.length - 1)
164
+ return new Range(p, 0, this.end, this.whole);
165
+ return new Range(p, 0, p.text.length, true);
166
+ });
167
+ }
168
+ /**
169
+ * Refuses a hyperlink set on a range across paragraphs. Word wrote a HYPERLINK field from the first paragraph into
170
+ * the last (check 54 case 34), where this package's links are `w:hyperlink` elements, one paragraph's each, which
171
+ * its getter reads (CR-014 section 12).
172
+ */
173
+ assertOneParagraph(member) {
174
+ if (this.spansParagraphs) {
175
+ throw new NotSupportedError('Range', member, "this range spans paragraphs, where Word writes a HYPERLINK field across them, which isn't "
176
+ + "written here (CR-014); link each paragraph's range");
177
+ }
178
+ }
179
+ /** Refuses, before anything changes, a write over a paragraph of the range that is read-only (a text box's, a branch's). */
180
+ assertWritableSpan() {
181
+ for (const p of this.paragraphs)
182
+ p.assertWritable();
24
183
  }
184
+ /** Every paragraph the range touches, in document order, a table's cells' among them (check 52). */
25
185
  get paragraphs() {
26
- return [this.paragraph];
186
+ if (!this.spansParagraphs)
187
+ return [this.paragraph];
188
+ const { layout, first, last } = this.located();
189
+ return layout.paragraphs.slice(first, last + 1).map((sp) => this.viewOf(sp.element, sp.container));
27
190
  }
191
+ /**
192
+ * The range's text, in `body.text`'s form across paragraphs (check 52): a mark `\r` between paragraphs, a
193
+ * table's cells a `\t` apart and each row ended `\r\n`; with the last paragraph's mark where the range
194
+ * holds it (a 'Whole' range).
195
+ */
28
196
  get text() {
29
- return this.paragraph.text.substring(this.start, this.end);
197
+ var _a, _b;
198
+ if (!this.spansParagraphs) {
199
+ const text = this.paragraph.text;
200
+ const span = text.substring(this.start, this.end);
201
+ return this.mark && this.end >= text.length ? span + ((_a = this.endMark) !== null && _a !== void 0 ? _a : this.paragraph.markText) : span;
202
+ }
203
+ const tail = this.mark && this.end >= this.endParagraph.text.length ? (_b = this.endMark) !== null && _b !== void 0 ? _b : this.endParagraph.markText : '';
204
+ const { layout, first, last } = this.located();
205
+ return layout.text.slice(layout.paragraphs[first].start + this.start, layout.paragraphs[last].start + this.end) + tail;
30
206
  }
31
207
  set text(value) {
32
208
  this.insertText(value, 'Replace');
33
209
  }
34
- /** The paragraph's style id (extension, as on Paragraph). */
210
+ /** True when the range holds no text (Office JS `isEmpty`). */
211
+ get isEmpty() {
212
+ return this.text === '';
213
+ }
214
+ /**
215
+ * The paragraph's style id (extension, as on Paragraph); a range across paragraphs gives the first one's. Set
216
+ * across paragraphs, each paragraph's (check 54 cases 32 and 33: Word gave both Heading 2).
217
+ */
35
218
  get styleId() {
36
219
  return this.paragraph.styleId;
37
220
  }
38
221
  set styleId(id) {
39
- this.paragraph.styleId = id;
222
+ for (const p of this.styledParagraphs())
223
+ p.styleId = id;
40
224
  }
41
- /** The paragraph's style name (Office JS reports the range's paragraph style). */
225
+ /**
226
+ * The range's style name, as Office JS reads it (checks 52 and 53). A range holding its paragraphs whole reads
227
+ * their paragraph style, where they share one, else `Normal`. Any other range reads its runs' character style
228
+ * where they share one (`Emphasis`), `''` where their character styles differ (some runs styled, some not: a
229
+ * footnote reference among plain text), and the paragraph style where none has one. Setting it sets the
230
+ * paragraph's style.
231
+ */
42
232
  get style() {
43
- return this.paragraph.style;
233
+ const read = this.styleRead();
234
+ return read.charStyle === undefined ? read.paragraphs((p) => p.style) : read.charStyle === '' ? '' : styleNameOf(this.paragraph.parentBody.package_, read.charStyle);
44
235
  }
45
236
  set style(id) {
46
- this.paragraph.style = id;
237
+ for (const p of this.styledParagraphs())
238
+ p.style = id;
239
+ }
240
+ /** The built-in name of `style` (Office JS `styleBuiltIn`): `Other` where it is `''`, `Normal` where paragraphs across it differ. */
241
+ get styleBuiltIn() {
242
+ const read = this.styleRead();
243
+ return read.charStyle === undefined ? read.paragraphs((p) => p.styleBuiltIn) : read.charStyle === '' ? 'Other' : builtInOf(read.charStyle);
244
+ }
245
+ /** Sets each paragraph's style by its built-in name, as `style` does. */
246
+ set styleBuiltIn(value) {
247
+ for (const p of this.styledParagraphs())
248
+ p.styleBuiltIn = value;
249
+ }
250
+ /**
251
+ * The paragraphs a style set on the range goes to: its own, or each across paragraphs (check 54 cases 32 and 33:
252
+ * Word gave a body paragraph and a list item Heading 2). Word also took away the body paragraph's centring and
253
+ * italic and the list item's numbering, which `Paragraph.style` doesn't (CR-014 section 12).
254
+ */
255
+ styledParagraphs() {
256
+ if (!this.spansParagraphs)
257
+ return [this.paragraph];
258
+ this.assertWritableSpan();
259
+ return this.paragraphs;
260
+ }
261
+ /**
262
+ * What `style` reads: the runs' shared character style id, `''` where their character styles differ, or
263
+ * undefined where the paragraph style applies, with how to read that.
264
+ */
265
+ styleRead() {
266
+ const paragraphs = (read) => {
267
+ if (!this.spansParagraphs)
268
+ return read(this.paragraph);
269
+ const values = new Set(this.paragraphs.map(read));
270
+ return values.size === 1 ? [...values][0] : 'Normal';
271
+ };
272
+ const holdsWhole = this.start === 0 && this.end >= this.endParagraph.text.length;
273
+ if (holdsWhole)
274
+ return { charStyle: undefined, paragraphs };
275
+ const ids = new Set(this.runs.map((run) => { var _a, _b, _c; return (_c = (_b = (_a = run.value.rPr) === null || _a === void 0 ? void 0 : _a.rStyle) === null || _b === void 0 ? void 0 : _b.val) !== null && _c !== void 0 ? _c : ''; }));
276
+ if (ids.size === 0)
277
+ return { charStyle: undefined, paragraphs };
278
+ if (ids.size > 1)
279
+ return { charStyle: '', paragraphs };
280
+ const only = [...ids][0];
281
+ return { charStyle: only === '' ? undefined : only, paragraphs };
282
+ }
283
+ /** The table cell holding the range's start, if it is in one (Office JS `parentTableCell`, which throws instead). */
284
+ get parentTableCell() {
285
+ return this.paragraph.parentTableCell;
286
+ }
287
+ /** The table holding the range's start, if it is in one (Office JS `parentTable`, which throws instead). */
288
+ get parentTable() {
289
+ var _a;
290
+ return (_a = this.paragraph.parentTableCell) === null || _a === void 0 ? void 0 : _a.parentTable;
291
+ }
292
+ /**
293
+ * The tables in the range (Office JS `Range.tables`): a table the range covers some of, unless the range is
294
+ * inside one of its cells, where that cell's tables are looked at instead (check 52: a range across a table,
295
+ * or into or out of one, lists it; a cell's whole range lists none).
296
+ */
297
+ get tables() {
298
+ const inRange = new Set(this.paragraphs.map((p) => p.element.value));
299
+ const pick = (tables) => tables.flatMap((table) => {
300
+ const cells = table.rows.flatMap((row) => row.cells);
301
+ if (!cells.some((cell) => cell.paragraphs.some((p) => inRange.has(p.p))))
302
+ return [];
303
+ const home = cells.find((cell) => [...inRange].every((p) => cell.paragraphs.some((cp) => cp.p === p)));
304
+ return home ? pick(home.body.tables) : [table];
305
+ });
306
+ return pick(this.storyBody().tables);
307
+ }
308
+ /**
309
+ * The content controls in the range (Office JS `Range.contentControls`): each control the range overlaps,
310
+ * except one holding the whole range, in document order (check 53: a range from a paragraph into a control
311
+ * lists it, one inside a control, its content whole, doesn't, one over an inline control lists it).
312
+ */
313
+ get contentControls() {
314
+ const layout = storyLayout(storyOf(this.paragraph.p).items, (element, container) => this.viewOf(element, container).text);
315
+ const at = (p, offset) => {
316
+ const i = layout.indexOf(p.p);
317
+ return i < 0 ? Number.NaN : layout.paragraphs[i].start + offset;
318
+ };
319
+ const from = at(this.paragraph, this.start);
320
+ const to = at(this.endParagraph, this.end);
321
+ return this.storyBody().contentControls.filter((control) => {
322
+ let start;
323
+ let end;
324
+ if (control.form === 'Run') {
325
+ let range;
326
+ try {
327
+ range = control.getRange();
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ [start, end] = [at(range.paragraph, range.start), at(range.paragraph, range.end)];
333
+ }
334
+ else {
335
+ const paragraphs = control.paragraphs;
336
+ if (paragraphs.length === 0)
337
+ return false;
338
+ const last = paragraphs[paragraphs.length - 1];
339
+ [start, end] = [at(paragraphs[0], 0), at(last, last.text.length)];
340
+ }
341
+ if (Number.isNaN(start) || Number.isNaN(end))
342
+ return false;
343
+ const overlaps = start < to && end > from || (start === end && from <= start && start <= to);
344
+ const holdsRange = start <= from && to <= end;
345
+ return overlaps && !holdsRange;
346
+ });
347
+ }
348
+ /**
349
+ * Office JS `getContentControls` (WordApi 1.5): the content controls `contentControls` lists, those of `options.types`
350
+ * where given. Word's `contentControls` lists rich-text controls only; this package's lists every kind, a recorded
351
+ * departure (CR-002 section 60 G1, check 55b), so this is how add-in code that asks for plain-text controls finds them.
352
+ */
353
+ getContentControls(options) {
354
+ return controlsOfTypes(this.contentControls, options);
355
+ }
356
+ /** A body over this range's story, for its tables and controls. */
357
+ storyBody() {
358
+ const { owner } = storyOf(this.paragraph.p);
359
+ return this.paragraph.parentBody.sub(owner);
47
360
  }
48
361
  /**
49
362
  * The text in one of the two views (extension; `text` is the accepted one). `{ view: 'original' }`
@@ -65,23 +378,52 @@ export class Range {
65
378
  }
66
379
  /** `font`, with `{ direct: true }` for the direct formatting of the first run (extension). */
67
380
  getFont(options) {
68
- const holders = () => {
381
+ // across paragraphs: each run read in its own paragraph, null where they differ, as Office JS's (check 52)
382
+ if (this.spansParagraphs) {
383
+ return new MixedFont(() => {
384
+ const resolver = this.paragraph.parentBody.propertyResolver;
385
+ const selector = runFontSelectorOf(this.paragraph.parentBody.package_);
386
+ const themeColors = (options === null || options === void 0 ? void 0 : options.direct) === true ? undefined : resolver.getThemeColors();
387
+ return this.pieces().flatMap((piece) => piece.runs.map((run) => {
388
+ const rPr = (options === null || options === void 0 ? void 0 : options.direct) === true ? run.value.rPr
389
+ : resolver.getEffectiveRPr(run.value.rPr, piece.paragraph.p.pPr, piece.paragraph.cellContext);
390
+ return readingOf(rPr, (options === null || options === void 0 ? void 0 : options.direct) !== true, (options === null || options === void 0 ? void 0 : options.direct) === true ? undefined : selector === null || selector === void 0 ? void 0 : selector.asciiFontName(rPr), themeColors);
391
+ }));
392
+ }, (opts) => {
393
+ // every run in it and every mark it holds (CR-014 phase 2, check 52)
394
+ this.assertWritableSpan();
395
+ const pieces = this.pieces();
396
+ formatSpan(pieces.map((piece, i) => ({
397
+ paragraph: piece.paragraph, start: piece.start, end: piece.end, runs: () => piece.runs,
398
+ holdsMark: i < pieces.length - 1 || this.holdsEndMark,
399
+ })), opts);
400
+ });
401
+ }
402
+ const holders = (write) => {
69
403
  if (this.start === this.end)
70
404
  return [];
71
- this.paragraph.splitAt(this.start);
72
- this.paragraph.splitAt(this.end);
405
+ // a write formats runs holding the span and nothing else: a tab, a break or text after the span in
406
+ // the same run is split off too, where splitting at the two offsets left it in the run and formatted
407
+ // it; a read splits nothing, the first run's properties being the same either way (CR-012 phase 4)
408
+ if (write)
409
+ this.paragraph.splitRunsAround(this.start, this.end);
73
410
  return this.runs.map((r) => r.value);
74
411
  };
412
+ // a range holding its paragraph's mark and no run, an empty paragraph's whole range, reads the mark (CR-002 section 59)
413
+ const mark = () => (this.holdsEndMark ? markHolderOf(this.paragraph.p) : undefined);
75
414
  if ((options === null || options === void 0 ? void 0 : options.direct) === true)
76
- return new Font(holders, () => this.paragraph.fontTracking());
77
- return new Font(holders, () => this.paragraph.fontTracking(), (rPr) => this.paragraph.parentBody.propertyResolver.getEffectiveRPr(rPr, this.paragraph.p.pPr, this.paragraph.cellContext), (rPr) => { var _a; return (_a = runFontSelectorOf(this.paragraph.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
415
+ return new Font(holders, () => this.paragraph.fontTracking(), undefined, undefined, mark);
416
+ return new Font(holders, () => this.paragraph.fontTracking(), (rPr) => this.paragraph.parentBody.propertyResolver.getEffectiveRPr(rPr, this.paragraph.p.pPr, this.paragraph.cellContext), (rPr) => { var _a; return (_a = runFontSelectorOf(this.paragraph.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); }, mark, () => this.paragraph.parentBody.propertyResolver.getThemeColors());
78
417
  }
79
418
  /** The runs the span covers (a run partly inside counts). */
80
419
  get runs() {
420
+ if (this.spansParagraphs)
421
+ return this.pieces().flatMap((piece) => piece.runs);
81
422
  const out = [];
82
423
  const seen = new Set();
83
424
  for (const seg of this.paragraph.segments()) {
84
- if (seg.end <= this.start || seg.start >= this.end)
425
+ // an equation is in no run, and its math runs' formatting is their own (CR-012 phase 4)
426
+ if (seg.end <= this.start || seg.start >= this.end || seg.math)
85
427
  continue;
86
428
  if (seen.has(seg.run))
87
429
  continue;
@@ -90,36 +432,105 @@ export class Range {
90
432
  }
91
433
  return out;
92
434
  }
435
+ /**
436
+ * Text put at the range's start ('Start', 'Before'), at its end ('End', 'After'), or in its place ('Replace').
437
+ * Across paragraphs (CR-014 phase 2, check 52) the start is in the first paragraph and the end in the last,
438
+ * before a last mark the range holds, and a replacement is `delete()` keeping a last mark, then the text where the
439
+ * paragraphs joined: untracked, in the first replaced character's look (`lookOf`); tracked, a `w:ins` before the
440
+ * deletion, as Word writes it. A range holding its paragraph's mark is replaced so too under tracking.
441
+ */
93
442
  insertText(text, location) {
443
+ var _a;
444
+ // a control's own range: into a control showing its placeholder, or an empty run-level one, its writes go inside it,
445
+ // and a Replace over a whole run-level control showing its placeholder takes it (CR-002 section 63, D3 and D4)
446
+ const through = (_a = this.ofControl) === null || _a === void 0 ? void 0 : _a.control.writeThrough(this.ofControl.part, text, location);
447
+ if (through)
448
+ return through;
449
+ // a paragraph's End range, at any location, and its Content range's End and After: after everything the paragraph
450
+ // holds, as Word writes there (CR-002 sections 63.24 to 63.27, D12 to D14; checks 69 and 70)
451
+ if (this.writesAtParagraphEnd(location)) {
452
+ const r = this.paragraph.writeAtEnd(text, location === 'After');
453
+ if (location === 'Replace')
454
+ this.cover(r);
455
+ else if (location === 'Before' || location === 'Start') {
456
+ this.startParagraph = this.lastParagraph = r.endParagraph; // a line break may have made paragraphs
457
+ this.start = this.end = r.end;
458
+ }
459
+ return r;
460
+ }
461
+ // tracked, the w:ins goes before the w:del within one paragraph too, as Word's (check 54 case 4; Jason,
462
+ // 2026-10-23: "follow Word (insertion first)"), where CR-002 wrote the w:del first
463
+ if (location === 'Replace' && (this.spansParagraphs || this.paragraph.changeTracker))
464
+ return this.replaceAcross(text);
465
+ if (this.spansParagraphs) {
466
+ this.assertWritableSpan();
467
+ if (location === 'End' || location === 'After')
468
+ return this.endParagraph.splice(this.end, this.end, text);
469
+ const r = this.paragraph.splice(this.start, this.start, text, 'after');
470
+ this.startParagraph = r.endParagraph; // the text may have broken the paragraph (CR-002 section 60 E1)
471
+ this.start = r.end;
472
+ return r;
473
+ }
94
474
  let r;
95
475
  if (location === 'Replace')
96
- r = this.paragraph.splice(this.start, this.end, text);
476
+ r = this.paragraph.splice(this.start, this.end, text, 'before', this.edges);
97
477
  // text put before the range joins the run the range starts in - inside the link the range is on,
98
478
  // as Office JS (check 32: insertText('Before') on a link's text went into the link, as 'After' did)
99
479
  else if (location === 'Before' || location === 'Start')
100
480
  r = this.paragraph.splice(this.start, this.start, text, 'after');
101
481
  else
102
482
  r = this.paragraph.splice(this.end, this.end, text);
103
- // this range keeps covering the same text: a replacement resizes it, text before it shifts it, text after it leaves it
483
+ // this range keeps covering the same text: a replacement resizes it, text before it shifts it, text after it leaves
484
+ // it; what was put in is the returned range's, which a line break may have taken into the paragraphs it made
104
485
  if (location === 'Replace')
105
- this.end = this.start + text.length;
486
+ this.cover(r);
106
487
  else if (location === 'Before' || location === 'Start') {
107
- this.start += text.length;
108
- this.end += text.length;
488
+ const length = this.end - this.start;
489
+ this.startParagraph = this.lastParagraph = r.endParagraph;
490
+ this.start = r.end;
491
+ this.end = r.end + length;
109
492
  }
110
493
  return r;
111
494
  }
495
+ /**
496
+ * A new paragraph of the text where the range starts ('Before') or ends ('After'), splitting the paragraph there,
497
+ * as Word does (`insertParagraphAt`): across paragraphs (check 52) and within one (check 54 cases 1 to 3; until
498
+ * then a range within one paragraph put it before or after the whole paragraph). At a paragraph's start or end,
499
+ * or after a range holding its mark, it goes before or after the paragraph. This range keeps covering its text.
500
+ */
112
501
  insertParagraph(text, location) {
113
- return this.paragraph.insertParagraph(text, location);
502
+ this.assertWritableSpan();
503
+ const spans = this.spansParagraphs;
504
+ if (location === 'Before') {
505
+ const { inserted, firstHalf } = insertParagraphAt(this.paragraph, this.start, text);
506
+ if (firstHalf) { // the paragraph keeps the text from the split on
507
+ if (!spans)
508
+ this.end -= this.start;
509
+ this.start = 0;
510
+ }
511
+ return inserted;
512
+ }
513
+ const at = this.holdsEndMark ? this.endParagraph.text.length : this.end;
514
+ const { inserted, firstHalf } = insertParagraphAt(this.endParagraph, at, text);
515
+ if (firstHalf) { // the text before the split moved into the first half
516
+ if (!spans)
517
+ this.startParagraph = firstHalf;
518
+ this.lastParagraph = firstHalf;
519
+ }
520
+ return inserted;
114
521
  }
115
522
  /**
116
523
  * Word's `insertOoxml` at range level: a flat OPC `pkg:package` (or a bare fragment). One
117
524
  * incoming paragraph's runs go at the start or the end of the span, or in its place; anything
118
525
  * else is inserted as blocks before or after the paragraph. Returns what was inserted, as
119
- * `insertXml` does.
526
+ * `insertXml` does. Across paragraphs (check 54 case 35) one paragraph's runs replace the range as
527
+ * `insertText('Replace')` replaces it, where the paragraphs joined; the start is the first paragraph's,
528
+ * the end the last's; several blocks go before the first paragraph or after the last, and replacing a
529
+ * range across paragraphs with several blocks is refused, as unmeasured.
120
530
  */
121
531
  async insertOoxml(ooxml, location) {
122
532
  var _a, _b, _c;
533
+ this.assertWritableSpan();
123
534
  const body = this.paragraph.parentBody;
124
535
  const preprocess = (_a = body.package_) === null || _a === void 0 ? void 0 : _a.loadOptions.preprocessor;
125
536
  const elements = await contentOf(ooxml, { preprocess: preprocess ? (doc) => preprocess(doc) : undefined, target: body.part });
@@ -128,31 +539,241 @@ export class Range {
128
539
  const only = elements.length === 1 && typeNameOf(elements[0]) === 'org_docx4j_wml.P' ? elements[0].value : undefined;
129
540
  if (only) {
130
541
  if (location === 'Replace') {
131
- this.delete();
132
- this.paragraph.insertItemsAt(this.start, ((_b = only.content) !== null && _b !== void 0 ? _b : []));
542
+ const landing = this.removeKeepingMark('insertOoxml');
543
+ landing.paragraph.insertItemsAt(landing.offset, ((_b = only.content) !== null && _b !== void 0 ? _b : []));
544
+ return [landing.paragraph];
133
545
  }
134
- else
135
- this.paragraph.insertItemsAt(location === 'Before' ? this.start : this.end, ((_c = only.content) !== null && _c !== void 0 ? _c : []));
136
- return [this.paragraph];
546
+ const [paragraph, at] = location === 'Before' ? [this.paragraph, this.start] : [this.endParagraph, this.end];
547
+ paragraph.insertItemsAt(at, ((_c = only.content) !== null && _c !== void 0 ? _c : []));
548
+ return [paragraph];
137
549
  }
138
- body.insertElement(elements, location === 'Before' ? 'Before' : 'After', this.paragraph);
550
+ if (location === 'Replace' && this.spansParagraphs) {
551
+ throw new NotSupportedError('Range', 'insertOoxml', "several blocks in place of a range across paragraphs, which Word wasn't measured doing (CR-014)");
552
+ }
553
+ const target = location === 'Before' ? this.paragraph : this.endParagraph;
554
+ body.insertElement(elements, location === 'Before' ? 'Before' : 'After', target);
139
555
  if (location === 'Replace')
140
- this.delete();
556
+ this.removeKeepingMark('insertOoxml');
141
557
  return elements.map((el) => {
142
558
  var _a;
143
559
  return typeNameOf(el) === 'org_docx4j_wml.P'
144
- ? (_a = body.paragraphFor(el.value)) !== null && _a !== void 0 ? _a : { element: el, container: this.paragraph.container }
145
- : { element: el, container: this.paragraph.container };
560
+ ? (_a = body.paragraphFor(el.value)) !== null && _a !== void 0 ? _a : { element: el, container: target.container }
561
+ : { element: el, container: target.container };
146
562
  });
147
563
  }
148
- /** Removes the span's text. */
564
+ /**
565
+ * The range's content removed as a replacement removes it, keeping a last mark the range holds (check 52):
566
+ * `delete()`'s join across paragraphs, else the paragraph's splice. Where the paragraphs closed up.
567
+ */
568
+ removeKeepingMark(member) {
569
+ if (!this.spansParagraphs && !this.holdsEndMark) {
570
+ this.paragraph.splice(this.start, this.end, '', 'before', this.edges);
571
+ this.end = this.start;
572
+ return { paragraph: this.paragraph, offset: this.start };
573
+ }
574
+ const extent = this.replaceableExtent(member);
575
+ if (this.paragraph.changeTracker)
576
+ assertTrackable(extent, member);
577
+ const landing = deleteSpan(this, extent, true);
578
+ this.landAt(landing.paragraph, landing.offset);
579
+ return landing;
580
+ }
581
+ /**
582
+ * The extent of a range a replacement may take: refused where it ends with a whole table, as Word refused it
583
+ * (`GeneralException`, check 54 case 7), and where it starts with a whole table or control (unmeasured).
584
+ */
585
+ replaceableExtent(member) {
586
+ const extent = extentOf(this, member);
587
+ if (!extent.tail) {
588
+ throw new GeneralException(`Range.${member}: this range ends with a whole table or control, which Office JS refuses to replace (check 54)`);
589
+ }
590
+ if (!extent.head) {
591
+ throw new NotSupportedError('Range', member, "this range starts with a whole table or content control, so there is no paragraph for the text to go in, and what Word does isn't measured");
592
+ }
593
+ this.assertWritableSpan();
594
+ return extent;
595
+ }
596
+ /**
597
+ * Removes the span's text, and the pictures in it (CR-002 section 48). Across paragraphs, or holding its
598
+ * paragraph's mark, it is Word's (CR-014 phase 2, check 52; `deleteSpan`): the paragraphs join in the last one's
599
+ * properties, the blocks between go, a last mark the range holds goes with it, joining the paragraph after; a
600
+ * footnote whose reference goes goes on save. Tracked, the text is deleted and every mark in the range marked
601
+ * deleted. A range starting or ending inside a table is refused with `GeneralException`, as Office JS refuses it.
602
+ */
149
603
  delete() {
150
- this.paragraph.splice(this.start, this.end, '');
604
+ // a control's own range takes a control showing its placeholder as Word does; a paragraph's 'Content' range over a
605
+ // block control's one paragraph takes the control, as Word's took one showing its placeholder (CR-002 section 63, D3)
606
+ if (this.ofControl) {
607
+ const { control, part } = this.ofControl;
608
+ if (control.deleteThrough(part)) {
609
+ this.end = this.start;
610
+ return;
611
+ }
612
+ const emptied = part === 'Content' && !this.paragraph.changeTracker ? control : undefined;
613
+ this.deleteSpan();
614
+ // a control emptied shows its entry again in Word's form (check 66 case 3), Word's filler in the bare form (D10)
615
+ emptied === null || emptied === void 0 ? void 0 : emptied.showEmptied();
616
+ return;
617
+ }
618
+ const control = this.whole && !this.mark && !this.spansParagraphs ? this.paragraph.blockControl : undefined;
619
+ if ((control === null || control === void 0 ? void 0 : control.isShowingPlaceholder) && control.paragraphs.length === 1 && this.start === 0 && this.end >= this.paragraph.text.length) {
620
+ const r = control.takeWhole();
621
+ this.landAt(r.paragraph, 0);
622
+ return;
623
+ }
624
+ this.deleteSpan();
625
+ }
626
+ /** `delete()`'s own work. */
627
+ deleteSpan() {
628
+ if (this.spansParagraphs || this.holdsEndMark) {
629
+ const extent = extentOf(this, 'delete', true);
630
+ this.assertWritableSpan();
631
+ if (this.paragraph.changeTracker)
632
+ assertTrackable(extent, 'delete');
633
+ const landing = deleteSpan(this, extent, false);
634
+ if (landing)
635
+ this.landAt(landing.paragraph, landing.offset);
636
+ return;
637
+ }
638
+ this.paragraph.splice(this.start, this.end, '', 'before', this.edges);
151
639
  this.end = this.start;
152
640
  }
641
+ /** `insertText(text, 'Replace')` across paragraphs, and any under tracking (CR-014 phases 2 and 3). */
642
+ replaceAcross(text) {
643
+ const extent = this.replaceableExtent('insertText');
644
+ if (this.paragraph.changeTracker) {
645
+ assertTrackable(extent, 'insertText');
646
+ // tracked: the insertion before the deletion (check 52 cases 39 to 48), in the first replaced run's formatting
647
+ const length = this.spansParagraphs ? undefined : this.end - this.start;
648
+ const inserted = this.paragraph.splice(this.start, this.start, text, 'after');
649
+ // the range's own text follows what was put in, in the paragraph the insertion ended in (E1)
650
+ this.startParagraph = inserted.endParagraph;
651
+ this.start = inserted.end;
652
+ if (length !== undefined) {
653
+ this.lastParagraph = inserted.endParagraph;
654
+ this.end = inserted.end + length;
655
+ }
656
+ deleteSpan(this, extentOf(this, 'insertText'), true);
657
+ this.cover(inserted, true);
658
+ return new Range(inserted.paragraph, inserted.start, inserted.end, false, false, inserted.endParagraph);
659
+ }
660
+ const first = firstCharacterOf(this);
661
+ // a line break in the text is a paragraph break (E1), refused before anything changes where none can be made
662
+ if (/[\r\n]/.test(text))
663
+ this.paragraph.assertBreakableAt(this.start, 'before');
664
+ const landing = deleteSpan(this, extent, true);
665
+ const placed = placeReplacement(landing, first, text);
666
+ const r = new Range(placed.first, placed.start, placed.end, false, false, placed.last);
667
+ this.cover(r, true);
668
+ return r;
669
+ }
670
+ /**
671
+ * The hits of a query in the range, as Office JS's `search` gives them. A plain query holding `^p`, Word's
672
+ * paragraph mark, matches across paragraphs (`searchMarks`); any other matches a paragraph at a time.
673
+ */
153
674
  search(text, options) {
154
- const base = this.start;
155
- return findAll(this.text, searchPattern(text, options)).map(([s, e]) => new Range(this.paragraph, base + s, base + e));
675
+ const marks = (options === null || options === void 0 ? void 0 : options.matchWildcards) || (options === null || options === void 0 ? void 0 : options.matchRegExp) ? undefined : paragraphMarkPieces(text);
676
+ if (marks)
677
+ return this.searchMarks(marks, options);
678
+ if ((options === null || options === void 0 ? void 0 : options.matchWildcards) && (this.spansParagraphs || this.holdsEndMark))
679
+ return this.searchWildcards(text, options);
680
+ if (this.spansParagraphs)
681
+ return this.pieces().flatMap((piece) => piece.search(text, options));
682
+ return officeMatches(this.paragraph.segments({ objects: true }), text, options, this.start, this.end)
683
+ .map(({ start, end }) => new Range(this.paragraph, start, end));
684
+ }
685
+ /**
686
+ * A query's hits where it holds `^p`, Word's paragraph mark, as Word finds them (check 52 case 2; CR-014 phase
687
+ * 3): `pieces` is the query split at its marks. A hit starts in one paragraph with the first piece matching to
688
+ * its end, takes each middle piece as a whole paragraph's text, and ends with the last piece matching from the
689
+ * next paragraph's start; a query ending with `^p` ends with that paragraph's mark, which the hit holds. A mark
690
+ * matches only a paragraph's own mark the range holds: never a cell's or a row's end, and never across the edge
691
+ * of a table, so every paragraph of a hit is in the same cell or in none (`^p` between a list item and the first
692
+ * cell after it found nothing). `^p^p` matches an empty paragraph's mark after another mark. Plain queries only:
693
+ * Word's wildcards write the mark another way.
694
+ */
695
+ searchMarks(pieces, options) {
696
+ const paragraphs = this.paragraphs;
697
+ const last = paragraphs.length - 1;
698
+ const marks = pieces.length - 1;
699
+ const endsWithMark = pieces[marks] === '';
700
+ const cell = (p) => { var _a; return (_a = p.parentTableCell) === null || _a === void 0 ? void 0 : _a.element.value; };
701
+ // a paragraph's mark the search may match: the range holds it, and it is a paragraph's, not a cell's end
702
+ const markIn = (j) => (j < last || this.holdsEndMark) && paragraphs[j].markText === '\r';
703
+ const matches = (p, query, from, to) => officeMatches(p.segments({ objects: true }), query, options, from, to);
704
+ const out = [];
705
+ for (let i = 0; i <= last; i++) {
706
+ const end = i + marks - (endsWithMark ? 1 : 0); // the paragraph the hit ends in
707
+ if (end > last)
708
+ break;
709
+ const scope = cell(paragraphs[i]);
710
+ let ok = true;
711
+ for (let j = i; j < i + marks && ok; j++)
712
+ ok = markIn(j);
713
+ for (let j = i + 1; j <= end && ok; j++)
714
+ ok = cell(paragraphs[j]) === scope;
715
+ // the middle pieces: each a whole paragraph's text (an empty one for an empty piece)
716
+ for (let j = 1; j < marks && ok; j++) {
717
+ const p = paragraphs[i + j];
718
+ ok = pieces[j] === '' ? p.text === '' : matches(p, pieces[j], 0, p.text.length).some((m) => m.start === 0 && m.end === p.text.length);
719
+ }
720
+ if (!ok)
721
+ continue;
722
+ const first = paragraphs[i];
723
+ const from = i === 0 ? this.start : 0;
724
+ const starts = pieces[0] === '' ? [first.text.length]
725
+ : matches(first, pieces[0], from, first.text.length).filter((m) => m.end === first.text.length).map((m) => m.start);
726
+ if (starts.length === 0 || starts[0] < from)
727
+ continue;
728
+ const endParagraph = paragraphs[end];
729
+ let to;
730
+ if (endsWithMark)
731
+ to = endParagraph.text.length;
732
+ else {
733
+ const limit = end === last ? this.end : endParagraph.text.length;
734
+ const hit = matches(endParagraph, pieces[marks], 0, limit).find((m) => m.start === 0);
735
+ if (!hit)
736
+ continue;
737
+ to = hit.end;
738
+ }
739
+ out.push(new Range(first, starts[0], to, false, endsWithMark, endParagraph.element === first.element ? undefined : endParagraph));
740
+ }
741
+ return out;
742
+ }
743
+ /**
744
+ * A wildcard query's hits across the paragraphs' marks, as Word's wildcards match them (check 59; CR-002 section 32,
745
+ * K7): where `^p` matches one (`searchMarks`), a paragraph's own mark the range holds, never a cell's or a row's end,
746
+ * and never across a table's edge, so the paragraphs a hit spans are in one cell or in none. `?`, `*` and a class cross
747
+ * such a mark and `^13` is one; a hit ending with one holds it.
748
+ */
749
+ searchWildcards(query, options) {
750
+ const paragraphs = this.paragraphs;
751
+ const last = paragraphs.length - 1;
752
+ const cell = (p) => { var _a; return (_a = p.parentTableCell) === null || _a === void 0 ? void 0 : _a.element.value; };
753
+ const markIn = (j) => (j < last || this.holdsEndMark) && paragraphs[j].markText === '\r';
754
+ // runs of consecutive paragraphs in one cell, or in none
755
+ const groups = [];
756
+ paragraphs.forEach((p, j) => {
757
+ const group = groups[groups.length - 1];
758
+ if (group && cell(paragraphs[group[group.length - 1]]) === cell(p))
759
+ group.push(j);
760
+ else
761
+ groups.push([j]);
762
+ });
763
+ const out = [];
764
+ for (const group of groups) {
765
+ const pieces = group.map((j) => {
766
+ const p = paragraphs[j];
767
+ const length = p.text.length;
768
+ return { segments: p.segments({ objects: true }), from: j === 0 ? this.start : 0, to: j === last ? this.end : length, length, mark: markIn(j) };
769
+ });
770
+ for (const m of wildcardMatchesAcross(pieces, query, options)) {
771
+ const first = paragraphs[group[m.first]];
772
+ const end = paragraphs[group[m.last]];
773
+ out.push(new Range(first, m.start, m.end, false, m.mark, end.element === first.element ? undefined : end));
774
+ }
775
+ }
776
+ return out;
156
777
  }
157
778
  /**
158
779
  * Replaces every match in this span, last first so the offsets stay valid; returns the count
@@ -160,30 +781,94 @@ export class Range {
160
781
  * (section 26; this one was missed there, section 31).
161
782
  */
162
783
  replaceText(find, replace, options) {
163
- const base = this.start;
164
- const matches = matchesOf(this.text, searchPattern(find, options));
784
+ refuseMarkReplacement('Range', find, options);
785
+ if (options === null || options === void 0 ? void 0 : options.matchWildcards)
786
+ refuseWildcardMarkReplacement('Range', this.search(find, options));
787
+ // across paragraphs a paragraph at a time, as `search` (CR-014 section 3), the last first
788
+ if (this.spansParagraphs) {
789
+ this.assertWritableSpan();
790
+ return this.pieces().reverse().reduce((count, piece) => count + piece.replaceText(find, replace, options), 0);
791
+ }
792
+ const matches = officeMatches(this.paragraph.segments({ objects: true }), find, options, this.start, this.end);
165
793
  for (let i = matches.length - 1; i >= 0; i--) {
166
- const match = matches[i];
167
- new Range(this.paragraph, base + match.index, base + match.index + match[0].length)
168
- .insertText(expandReplacement(replace, match, options), 'Replace');
794
+ const { start, end, match } = matches[i];
795
+ new Range(this.paragraph, start, end).insertText(expandReplacement(replace, match, options), 'Replace');
169
796
  }
170
797
  return matches.length;
171
798
  }
172
799
  /** The tracked changes this span covers, in document order (CR-002 phase F). */
173
800
  getTrackedChanges() {
801
+ if (this.spansParagraphs) {
802
+ // the story's listing, whose changes run across paragraphs as Word's do (check 52: a deletion through a
803
+ // mark is one change), each that overlaps the range; a paragraph's own listing cuts a change at its edges
804
+ const { layout, first, last } = this.located();
805
+ const at = (p, offset) => {
806
+ const i = layout.indexOf(p.p);
807
+ return i < 0 ? Number.NaN : layout.paragraphs[i].start + offset;
808
+ };
809
+ const from = layout.paragraphs[first].start + this.start;
810
+ const to = layout.paragraphs[last].start + this.end;
811
+ return this.storyBody().getTrackedChanges().filter((change) => {
812
+ const range = change.getRange();
813
+ if (!range)
814
+ return false;
815
+ const start = at(range.paragraph, range.start);
816
+ const end = at(range.endParagraph, range.end);
817
+ return !Number.isNaN(start) && !Number.isNaN(end) && start <= to && end >= from;
818
+ });
819
+ }
174
820
  return this.paragraph.getTrackedChanges().filter((change) => {
175
821
  const range = change.getRange();
176
822
  if (!range)
177
823
  return false;
178
- return range.start <= this.end && range.end >= this.start;
824
+ // a change across paragraphs (CR-014) covers this paragraph from where it starts in it to where it ends in it
825
+ const start = range.paragraph.element === this.paragraph.element ? range.start : 0;
826
+ const end = range.endParagraph.element === this.paragraph.element ? range.end : Number.POSITIVE_INFINITY;
827
+ return start <= this.end && end >= this.start;
179
828
  });
180
829
  }
830
+ /**
831
+ * A range of this one, as Office JS's `getRange` (check 52): 'Whole' is the range itself, 'Content' the same
832
+ * without a last mark, 'Start' and 'End' collapsed at its ends, and 'After' collapsed just after it - at the
833
+ * start of the next paragraph where the range holds its last paragraph's mark.
834
+ */
181
835
  getRange(location = 'Whole') {
182
836
  if (location === 'Start')
183
837
  return new Range(this.paragraph, this.start, this.start);
184
838
  if (location === 'End')
185
- return new Range(this.paragraph, this.end, this.end);
186
- return new Range(this.paragraph, this.start, this.end);
839
+ return new Range(this.endParagraph, this.end, this.end);
840
+ if (location === 'After') {
841
+ if (this.mark && this.end >= this.endParagraph.text.length) {
842
+ const layout = storyLayout(storyOf(this.endParagraph.p).items, (element, container) => this.viewOf(element, container).text);
843
+ const next = layout.paragraphs[layout.indexOf(this.endParagraph.p) + 1];
844
+ if (next)
845
+ return new Range(this.viewOf(next.element, next.container), 0, 0);
846
+ }
847
+ return new Range(this.endParagraph, this.end, this.end);
848
+ }
849
+ return new Range(this.paragraph, this.start, this.end, this.whole, this.mark && location === 'Whole', this.spansParagraphs ? this.endParagraph : undefined, this.endMark);
850
+ }
851
+ /**
852
+ * A range from this range's start or `range`'s, whichever comes first, to the end that comes last (Office JS
853
+ * `expandTo`; which is first doesn't matter, check 52). Both must be in one story.
854
+ */
855
+ expandTo(range) {
856
+ const story = storyOf(this.paragraph.p).items;
857
+ // Office JS's ItemNotFound (check 53: a body range expandTo a footnote's)
858
+ if (storyOf(range.paragraph.p).items !== story)
859
+ throw new ItemNotFound('expandTo: the two ranges are in different stories');
860
+ const layout = storyLayout(story, (element, container) => this.viewOf(element, container).text);
861
+ const at = (p, offset) => {
862
+ const i = layout.indexOf(p.p);
863
+ if (i < 0)
864
+ throw new Docx4JException('expandTo: a range is outside the story as a reader reads it');
865
+ return layout.paragraphs[i].start + offset;
866
+ };
867
+ const first = at(range.paragraph, range.start) < at(this.paragraph, this.start) ? range : this;
868
+ const thisEnd = at(this.endParagraph, this.end) + (this.mark ? 1 : 0);
869
+ const rangeEnd = at(range.endParagraph, range.end) + (range.mark ? 1 : 0);
870
+ const last = rangeEnd > thisEnd ? range : this;
871
+ return new Range(first.paragraph, first.start, last.end, first.whole && last.whole, last.mark, last.endParagraph.element === first.paragraph.element ? undefined : last.endParagraph, last.endMark);
187
872
  }
188
873
  /** The span's text as it stands (extension; Office JS getOoxml wraps it in a package). */
189
874
  toString() {
@@ -192,7 +877,18 @@ export class Range {
192
877
  // --- comments (CR-002 phase G) ---
193
878
  /** The comments this span touches, replies nested under their parent. */
194
879
  async getComments() {
195
- return commentApi().commentsOf(this);
880
+ if (!this.spansParagraphs)
881
+ return commentApi().commentsOf(this);
882
+ const seen = new Set();
883
+ const out = [];
884
+ for (const piece of this.pieces()) {
885
+ for (const comment of await commentApi().commentsOf(piece))
886
+ if (!seen.has(comment.id)) {
887
+ seen.add(comment.id);
888
+ out.push(comment);
889
+ }
890
+ }
891
+ return out;
196
892
  }
197
893
  /**
198
894
  * Comments the span: the markers and the reference run around it (runs are split at the
@@ -200,6 +896,8 @@ export class Range {
200
896
  * the comment parts the document lacks. Asynchronous because it unmarshals those parts.
201
897
  */
202
898
  async insertComment(content, options) {
899
+ // across paragraphs (CR-014 phase 3) the markers go in the first and the last paragraph, as Word's (check 52)
900
+ this.assertWritableSpan();
203
901
  return commentApi().insertComment(this, content, options);
204
902
  }
205
903
  // --- content controls (CR-002 phase E) ---
@@ -209,7 +907,10 @@ export class Range {
209
907
  * span gets an empty control at its position. Returns the control.
210
908
  */
211
909
  insertContentControl(kind) {
212
- var _a, _b;
910
+ var _a;
911
+ if (this.spansParagraphs)
912
+ return this.wrapParagraphs(kind);
913
+ this.paragraph.assertWritable();
213
914
  const paragraph = this.paragraph;
214
915
  const body = paragraph.parentBody;
215
916
  // The control is built empty first: `sdt` refuses a kind that cannot be run-level (a repeating
@@ -218,69 +919,86 @@ export class Range {
218
919
  declareSdtExtensions(body, sdt);
219
920
  if (this.start === this.end) {
220
921
  paragraph.insertItemsAt(this.start, [sdt]);
221
- return new ContentControl(sdt, containerOf(sdt, paragraph), body);
922
+ const control = new ContentControl(sdt, containerOf(sdt, paragraph), body);
923
+ // a control showing Word's default placeholder, as Word's made at a point (CR-002 section 63, D8; check 64); a
924
+ // picture control Word's form, the flag and the placeholder picture (CR-005 section 10, D2)
925
+ if (showsPlaceholderWhenMade(kind))
926
+ control.showPlaceholder();
927
+ else if (kind === 'Picture')
928
+ showPicturePlaceholder(control);
929
+ return control;
222
930
  }
223
- paragraph.splitAt(this.start);
224
- paragraph.splitAt(this.end);
225
- const segments = paragraph.segments().filter((s) => s.start >= this.start && s.end <= this.end);
226
- if (segments.length === 0)
931
+ // Every refusal before anything changes (CR-002 section 43): until then the runs were split first.
932
+ const covered = coveredSegments(paragraph, this.start, this.end);
933
+ if (covered.length === 0)
227
934
  throw new Docx4JException('This range covers no run');
228
- const owner = segments[0].runOwner;
229
- if (segments.some((s) => s.runOwner !== owner)) {
935
+ const coveredOwner = covered[0].runOwner;
936
+ if (covered.some((s) => s.runOwner !== coveredOwner)) {
230
937
  throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
231
938
  }
232
- const items = [];
233
- for (const segment of segments) {
234
- const element = owner[segment.runIndex];
235
- if (!items.includes(element))
236
- items.push(element);
237
- }
238
- const at = owner.indexOf(items[0]);
939
+ refuseTrackedDeletion(coveredOwner, covered);
239
940
  const tracker = paragraph.changeTracker;
240
- const revision = segments[0].revision;
941
+ const coveredRevision = revisionHolding(covered[0], coveredOwner);
942
+ if (tracker && coveredRevision && itemsKeyOf(coveredRevision) === undefined) {
943
+ throw new Docx4JException('This range is in a revision that cannot be split around a content control');
944
+ }
945
+ paragraph.splitAt(this.start);
946
+ paragraph.splitAt(this.end);
947
+ const segments = paragraph.segments().filter((s) => s.start >= this.start && s.end <= this.end);
948
+ const owner = segments[0].runOwner;
949
+ const { at, items } = wrappedItems(owner, segments);
950
+ const revision = revisionHolding(segments[0], owner);
241
951
  if (tracker && revision) {
242
952
  // The runs are in a revision: the control goes between its halves, not in it, and the runs go
243
953
  // into the control in a revision of the same kind and attributes with a new id - Word's form for
244
954
  // inserted text wrapped in a new control (check 30, inserted-text-wrapped: the w:ins inside the
245
955
  // control, the control's own markers outside it).
246
- const index = revision.items.indexOf(items[0]);
247
- revision.items.splice(index, items.length);
248
- const value = revision.value;
249
- const itemsKey = Object.keys(value).find((key) => value[key] === revision.items);
250
- const inner = {};
251
- for (const [key, v] of Object.entries(value))
252
- if (key !== itemsKey && key !== 'PARENT')
253
- inner[key] = v;
254
- inner.id = tracker.nextId();
255
- inner[itemsKey] = items;
256
- const wrapped = { name: revision.element.name, value: inner };
257
- linkParents(items, inner);
956
+ revision.items.splice(at, items.length);
957
+ const wrapped = revisionAround(revision, items, tracker.nextId());
258
958
  sdt.value.sdtContent.content = [wrapped];
259
959
  linkParents(wrapped, sdt.value.sdtContent);
260
- const holderOwner = revision.owner;
261
- const parent = (_a = revision.value.PARENT) !== null && _a !== void 0 ? _a : paragraph.p;
262
- if (revision.items.length === 0) {
263
- holderOwner.splice(holderOwner.indexOf(revision.element), 1, sdt); // the whole revision wrapped
264
- linkParents(sdt, parent);
265
- }
266
- else if (index === 0) {
267
- holderOwner.splice(holderOwner.indexOf(revision.element), 0, sdt); // its start wrapped
268
- linkParents(sdt, parent);
269
- }
270
- else if (!paragraph.splitHolderAround(revision, index, sdt, () => tracker.nextId())) {
960
+ if (!placeInRevision(paragraph, revision, at, sdt, () => tracker.nextId())) {
271
961
  throw new Docx4JException('This range is in a revision that cannot be split around a content control');
272
962
  }
273
- markHolderInserted(tracker, sdt, holderOwner, () => { });
274
- return new ContentControl(sdt, holderOwner, body);
963
+ markHolderInserted(tracker, sdt, revision.owner, () => { });
964
+ return new ContentControl(sdt, revision.owner, body);
275
965
  }
276
966
  sdt.value.sdtContent.content = items;
277
967
  owner.splice(at, items.length, sdt);
278
- linkParents(sdt, (_b = segments[0].run.PARENT) !== null && _b !== void 0 ? _b : paragraph.p);
968
+ linkParents(sdt, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
279
969
  // under tracking the control itself is the insertion, the runs in it what they were (check 30)
280
970
  if (tracker)
281
971
  markHolderInserted(tracker, sdt, owner, () => { });
282
972
  return new ContentControl(sdt, owner, body);
283
973
  }
974
+ /**
975
+ * A block content control around the paragraphs of a range across paragraphs, as Word puts one (check 54 cases
976
+ * 38 and 43, untracked and tracked): only a range holding its paragraphs whole, from the first's start to the
977
+ * last's end, its mark or not. Any other, starting or ending inside a paragraph or a table, is refused with
978
+ * `GeneralException`, as Word refused it (cases 36, 37 and 39 to 42). Tracked, the control is an insertion, as a
979
+ * paragraph's control is (check 30's markers).
980
+ */
981
+ wrapParagraphs(kind) {
982
+ var _a;
983
+ if (this.start !== 0 || this.end < this.endParagraph.text.length) {
984
+ throw new GeneralException('Range.insertContentControl: a range across paragraphs takes a control only where it holds its paragraphs whole, as Office JS (check 54)');
985
+ }
986
+ const extent = extentOf(this, 'insertContentControl');
987
+ this.assertWritableSpan();
988
+ const body = this.paragraph.parentBody;
989
+ const { list, first, last } = extent;
990
+ const parent = (_a = list[first].value.PARENT) !== null && _a !== void 0 ? _a : body.container;
991
+ // a bound block control holds one paragraph (CR-002 section 61): checked before sdtOf, which takes the paragraphs
992
+ assertControlHolds(body, parent, list.filter((_, i) => i < first || i > last), [BLOCK_CONTROL], undefined, ['Range', 'insertContentControl']);
993
+ const sdt = sdtOf(list.slice(first, last + 1), { kind: sdtKindFor(kind), id: nextSdtId(controlIdScope(body)), form: 'block' });
994
+ list.splice(first, last - first + 1, sdt);
995
+ linkParents(sdt, parent);
996
+ declareSdtExtensions(body, sdt);
997
+ const tracker = this.paragraph.changeTracker;
998
+ if (tracker)
999
+ markHolderInserted(tracker, sdt, list, () => { });
1000
+ return new ContentControl(sdt, list, body);
1001
+ }
284
1002
  /**
285
1003
  * Office JS `Range.hyperlink`: the address of the first hyperlink in this range, or a new
286
1004
  * hyperlink over it. `address#location` separates the address from an optional location within
@@ -306,16 +1024,20 @@ export class Range {
306
1024
  * `font` write is.
307
1025
  */
308
1026
  get hyperlink() {
309
- var _a;
1027
+ var _a, _b;
1028
+ if (this.spansParagraphs)
1029
+ return (_a = this.pieces().map((piece) => piece.hyperlink).find((link) => link !== '')) !== null && _a !== void 0 ? _a : '';
310
1030
  const holder = this.hyperlinkHolder();
311
1031
  if (holder === undefined)
312
1032
  return '';
313
- const anchor = (_a = holder.value.anchor) !== null && _a !== void 0 ? _a : '';
1033
+ const anchor = (_b = holder.value.anchor) !== null && _b !== void 0 ? _b : '';
314
1034
  const address = holder.value.id === undefined ? '' : this.relationshipTarget(holder.value.id);
315
1035
  return anchor === '' ? address : `${address}#${anchor}`;
316
1036
  }
317
1037
  set hyperlink(value) {
318
1038
  var _a, _b, _c;
1039
+ this.assertOneParagraph('hyperlink');
1040
+ this.paragraph.assertWritable();
319
1041
  // Office JS refuses a hyperlink set on an empty range - inside a link, in plain text, just after a
320
1042
  // link, and "" alike - with GeneralException, and changes nothing (test/README.md check 19, Word
321
1043
  // 16.0.20326.20158; CR-002 section 25). Before, the setter removed the link under a caret and
@@ -340,12 +1062,18 @@ export class Range {
340
1062
  start = Math.min(start, span[0]);
341
1063
  end = Math.max(end, span[1]);
342
1064
  }
343
- this.removeHyperlinks();
344
- if (value === '')
345
- return;
346
1065
  const hash = value.indexOf('#');
347
1066
  const address = hash === -1 ? value : value.slice(0, hash);
348
1067
  const location = hash === -1 ? '' : value.slice(hash + 1);
1068
+ // Every refusal comes before anything changes, judged on what the range would hold once the links
1069
+ // it touches were unwrapped (CR-002 section 43). Until then the setter unwrapped them, added the
1070
+ // relationship and split the runs before it refused, so a refused call left the links gone, an
1071
+ // orphan relationship and split runs (found by the docx4j-python session).
1072
+ if (value !== '')
1073
+ this.refuseUnlinkable(start, end, address);
1074
+ this.removeHyperlinks();
1075
+ if (value === '')
1076
+ return;
349
1077
  // w:history="1" is what Word writes on every hyperlink it makes, and what docx4j writes when
350
1078
  // it builds one (MainDocumentPart.addHyperlink): "add the target to the viewed hyperlinks"
351
1079
  // (ECMA-376 17.16.22). Readers do not depend on it, but a document holding one holder from
@@ -359,26 +1087,79 @@ export class Range {
359
1087
  paragraph.splitAt(start);
360
1088
  paragraph.splitAt(end);
361
1089
  const segments = paragraph.segments().filter((seg) => seg.start >= start && seg.end <= end);
362
- if (segments.length === 0)
363
- throw new Docx4JException('This range covers no run');
364
1090
  const owner = segments[0].runOwner;
365
- if (segments.some((seg) => seg.runOwner !== owner)) {
366
- throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
1091
+ const { at, items } = wrappedItems(owner, segments);
1092
+ const revision = revisionHolding(segments[0], owner);
1093
+ let runsParent = holder.value;
1094
+ if (revision) {
1095
+ // A w:hyperlink cannot be in a w:ins or w:moveTo (CT_RunTrackChange holds EG_ContentRunContent
1096
+ // only), so over runs in one, tracked or not, the link goes between the revision's halves and the
1097
+ // runs into it in a revision of the same kind and attributes with a new id, as a content control
1098
+ // over inserted text does (CR-002 section 43; the docx4j-python session's form). Until then the
1099
+ // link went into the revision, which marshalled with an xsi:type. Word's own form for a link set
1100
+ // on inserted text is a HYPERLINK field in the one w:ins (check 30, link-on-inserted-text), which
1101
+ // this setter's getter would not read back.
1102
+ revision.items.splice(at, items.length);
1103
+ const inner = revisionAround(revision, items, paragraph.nextAnnotationId());
1104
+ holder.value.content = [inner];
1105
+ linkParents(inner, holder.value);
1106
+ if (!placeInRevision(paragraph, revision, at, holder, () => paragraph.nextAnnotationId())) {
1107
+ throw new Docx4JException('This range is in a revision that cannot be split around a hyperlink');
1108
+ }
1109
+ runsParent = inner.value;
367
1110
  }
368
- const items = [];
369
- for (const segment of segments) {
370
- const element = owner[segment.runIndex];
371
- if (!items.includes(element))
372
- items.push(element);
1111
+ else {
1112
+ holder.value.content = items;
1113
+ owner.splice(at, items.length, holder);
1114
+ linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
373
1115
  }
374
- const at = owner.indexOf(items[0]);
375
- holder.value.content = items;
376
- owner.splice(at, items.length, holder);
377
- linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
378
1116
  this.restyle(holder.value, true);
379
- mergeTextRuns(holder.value.content, holder.value);
1117
+ mergeTextRuns(items, runsParent);
380
1118
  (_c = (_b = paragraph.parentBody.package_) === null || _b === void 0 ? void 0 : _b.requireStyles) === null || _c === void 0 ? void 0 : _c.call(_b, [HYPERLINK_STYLE]);
381
1119
  }
1120
+ /**
1121
+ * Refuses, before anything changes, a link the setter could not make over [start, end): one with an
1122
+ * address on a range with no part, over no run, over runs in more than one holder, or around a
1123
+ * tracked deletion - each judged as the range would stand once the links it touches were unwrapped,
1124
+ * their runs then in the list holding them.
1125
+ */
1126
+ refuseUnlinkable(start, end, address) {
1127
+ const paragraph = this.paragraph;
1128
+ if (address !== '' && !paragraph.parentBody.part) {
1129
+ throw new Docx4JException('This range has no part, so a hyperlink cannot be related to it');
1130
+ }
1131
+ const touched = hyperlinkHolders(paragraph).filter((h) => this.touches(h));
1132
+ const unwrapsInto = new Map();
1133
+ for (const holder of touched) {
1134
+ const inner = runItemsOf(holder.value);
1135
+ if (inner)
1136
+ unwrapsInto.set(inner, containerOf(holder, paragraph));
1137
+ }
1138
+ const ownerOnceUnwrapped = (owner) => {
1139
+ let o = owner;
1140
+ for (let guard = 0; guard < 100 && unwrapsInto.has(o); guard++)
1141
+ o = unwrapsInto.get(o);
1142
+ return o;
1143
+ };
1144
+ const covered = coveredSegments(paragraph, start, end);
1145
+ if (covered.length === 0)
1146
+ throw new Docx4JException('This range covers no run');
1147
+ const owner = ownerOnceUnwrapped(covered[0].runOwner);
1148
+ if (covered.some((seg) => ownerOnceUnwrapped(seg.runOwner) !== owner)) {
1149
+ throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
1150
+ }
1151
+ const unwrapped = new Set(touched);
1152
+ const flatten = (items) => items.flatMap((item) => {
1153
+ var _a;
1154
+ return (unwrapped.has(item)
1155
+ ? flatten(((_a = runItemsOf(item.value)) !== null && _a !== void 0 ? _a : [])) : [item]);
1156
+ });
1157
+ refuseTrackedDeletion(flatten(owner), covered);
1158
+ const revision = revisionHolding(covered[0], owner);
1159
+ if (revision && itemsKeyOf(revision) === undefined) {
1160
+ throw new Docx4JException('This range is in a revision that cannot be split around a hyperlink');
1161
+ }
1162
+ }
382
1163
  /** The first `w:hyperlink` this range touches, if any. */
383
1164
  hyperlinkHolder() {
384
1165
  return hyperlinkHolders(this.paragraph).find((holder) => this.touches(holder));
@@ -467,15 +1248,12 @@ export class Range {
467
1248
  return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
468
1249
  }
469
1250
  }
470
- /**
471
- * What Office JS throws where the host refuses an operation it has no answer for: its `code`, so that
472
- * add-in code branching on `error.code` behaves the same here (check 19).
473
- */
474
- class GeneralException extends Docx4JException {
1251
+ /** What Office JS throws where nothing matches (`ItemNotFound`): its `code`, as `GeneralException` carries its own. */
1252
+ class ItemNotFound extends Docx4JException {
475
1253
  constructor(message) {
476
1254
  super(message);
477
- this.code = 'GeneralException';
478
- this.name = 'GeneralException';
1255
+ this.code = 'ItemNotFound';
1256
+ this.name = 'ItemNotFound';
479
1257
  }
480
1258
  }
481
1259
  /** The character style Word gives the runs of a hyperlink it makes. */
@@ -541,6 +1319,84 @@ function hyperlinkHolders(paragraph) {
541
1319
  return out;
542
1320
  }
543
1321
  /** The run-level array holding an element, after `insertItemsAt` put it somewhere in the paragraph. */
1322
+ /**
1323
+ * The segments a range covers, read before its runs are split at its edges: those inside it, and those
1324
+ * a split would cut, whose inner part then is. What `segments()` gives inside the range once it is
1325
+ * split, so that a wrapper's refusals can come before anything changes.
1326
+ */
1327
+ function coveredSegments(paragraph, start, end) {
1328
+ return paragraph.segments().filter((s) => (s.start >= start && s.end <= end) || (s.start < end && s.end > start));
1329
+ }
1330
+ /**
1331
+ * The items a range's runs are wrapped in place of: everything from the first run it covers to the
1332
+ * last, in the one list holding them, so that what lies between - a bookmark, a comment's range, a
1333
+ * proofing mark - goes into the wrapper with them. Until CR-002 section 43 as many items were taken as
1334
+ * there were runs, from the first, which dropped what lay between and left a run in two places.
1335
+ */
1336
+ function wrappedItems(owner, segments) {
1337
+ const indexes = segments.map((s) => owner.indexOf(s.runOwner[s.runIndex])).filter((i) => i >= 0);
1338
+ const at = Math.min(...indexes);
1339
+ return { at, items: owner.slice(at, Math.max(...indexes) + 1) };
1340
+ }
1341
+ /**
1342
+ * Refuses a range with a tracked deletion (or text moved away) between its runs: having no text in
1343
+ * the range's, it would be wrapped unseen, and a range spanning a tracked change is refused (CR-002
1344
+ * section 43).
1345
+ */
1346
+ function refuseTrackedDeletion(items, segments) {
1347
+ const indexes = segments.map((s) => items.indexOf(s.runOwner[s.runIndex])).filter((i) => i >= 0);
1348
+ if (indexes.length === 0)
1349
+ return;
1350
+ for (const item of items.slice(Math.min(...indexes), Math.max(...indexes) + 1)) {
1351
+ const kind = revisionKindOf(item);
1352
+ if (kind === 'del' || kind === 'moveFrom') {
1353
+ throw new Docx4JException('This range spans a tracked deletion; wrap the text on either side of it');
1354
+ }
1355
+ }
1356
+ }
1357
+ /** The `w:ins` or `w:moveTo` a segment's run is directly in, when `owner` is its own item list. */
1358
+ function revisionHolding(segment, owner) {
1359
+ return segment.revision !== undefined && segment.revision.items === owner ? segment.revision : undefined;
1360
+ }
1361
+ /** The key under which a revision's value holds its items, or undefined where none does. */
1362
+ function itemsKeyOf(revision) {
1363
+ const value = revision.value;
1364
+ return Object.keys(value).find((key) => value[key] === revision.items);
1365
+ }
1366
+ /** A revision of the same kind and attributes as `revision`, with a new id, holding `items`. */
1367
+ function revisionAround(revision, items, id) {
1368
+ const value = revision.value;
1369
+ const itemsKey = itemsKeyOf(revision);
1370
+ const inner = {};
1371
+ for (const [key, v] of Object.entries(value))
1372
+ if (key !== itemsKey && key !== 'PARENT')
1373
+ inner[key] = v;
1374
+ inner.id = id;
1375
+ inner[itemsKey] = items;
1376
+ linkParents(items, inner);
1377
+ return { name: revision.element.name, value: inner };
1378
+ }
1379
+ /**
1380
+ * Puts `item` where items from `index` on were taken out of `revision`: in its place where it holds
1381
+ * nothing now, before it where they were its first, else between its halves. False where the revision
1382
+ * cannot be split.
1383
+ */
1384
+ function placeInRevision(paragraph, revision, index, item, nextId) {
1385
+ var _a;
1386
+ const owner = revision.owner;
1387
+ const parent = (_a = revision.value.PARENT) !== null && _a !== void 0 ? _a : paragraph.p;
1388
+ if (revision.items.length === 0) {
1389
+ owner.splice(owner.indexOf(revision.element), 1, item); // the whole revision wrapped
1390
+ linkParents(item, parent);
1391
+ return true;
1392
+ }
1393
+ if (index === 0) {
1394
+ owner.splice(owner.indexOf(revision.element), 0, item); // its start wrapped
1395
+ linkParents(item, parent);
1396
+ return true;
1397
+ }
1398
+ return paragraph.splitHolderAround(revision, index, item, nextId);
1399
+ }
544
1400
  function containerOf(element, paragraph) {
545
1401
  var _a, _b;
546
1402
  var _c;