@docx4j/core-ts 0.2.0 → 0.2.2
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 +8 -1
- package/dist/model/content/Paragraph.mjs +19 -14
- package/dist/model/content/Range.d.mts +32 -3
- package/dist/model/content/Range.mjs +103 -23
- package/dist/model/content/TrackedChange.d.mts +9 -3
- package/dist/model/content/TrackedChange.mjs +135 -2
- package/dist/model/content/binder.d.mts +1 -1
- package/dist/model/content/binder.mjs +54 -21
- package/dist/model/content/index.d.mts +1 -1
- package/dist/model/content/index.mjs +1 -1
- package/dist/model/content/search.d.mts +8 -0
- package/dist/model/content/search.mjs +22 -1
- package/dist/packages/WordprocessingMLPackage.d.mts +25 -1
- package/dist/packages/WordprocessingMLPackage.mjs +43 -1
- package/dist/xml/dom.d.mts +10 -1
- package/dist/xml/dom.mjs +46 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,11 @@ body.replaceText('(\\d+) pages', '$1 pp.', { matchRegExp: true });
|
|
|
113
113
|
body.replaceText('(quick) (fox)', '\\2 \\1', { matchWildcards: true });
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
`range.hyperlink = 'https://example.com/'` (or `'#bookmark'` for a place in the document) makes the
|
|
117
|
+
range a link, styled as Word styles one: its runs take the `Hyperlink` character style, over any
|
|
118
|
+
character style they had, and lose their direct colour; `''` removes the links the range touches,
|
|
119
|
+
style and all. A document that lacks the `Hyperlink` definition gets it when it is saved.
|
|
120
|
+
|
|
116
121
|
A style the document lacks is spliced in from docx4j's defaults, with the styles it is based on and
|
|
117
122
|
linked to: `await pkg.styles.ensure(['FootnoteText', 'FootnoteReference'])` returns the ids it
|
|
118
123
|
added, and a document that already has them keeps its styles part byte for byte.
|
|
@@ -226,7 +231,9 @@ body.acceptAll(); // or rejectAll(), or accept()/reject() one at a time
|
|
|
226
231
|
```
|
|
227
232
|
|
|
228
233
|
`getTrackedChanges()` is also on `Paragraph` and `Range`. Text and searches read the accepted
|
|
229
|
-
view throughout, so an agent sees the document as it will read once the changes are taken.
|
|
234
|
+
view throughout, so an agent sees the document as it will read once the changes are taken. A moved
|
|
235
|
+
paragraph is listed as its two halves, and accepting or rejecting either resolves the whole move,
|
|
236
|
+
as Word's Review tab does.
|
|
230
237
|
|
|
231
238
|
To record one caller's edits without touching the document's own setting - an agent working in a
|
|
232
239
|
document whose `w:trackRevisions` belongs to its owner - use `withTracking`. Nothing is written
|
|
@@ -527,11 +527,8 @@ export class Paragraph {
|
|
|
527
527
|
const value = seg.text.substring(0, from) + middle + seg.text.substring(to);
|
|
528
528
|
if (value === '')
|
|
529
529
|
toRemove.push(seg);
|
|
530
|
-
else
|
|
531
|
-
t
|
|
532
|
-
if (/^\s|\s$|\s\s/.test(value))
|
|
533
|
-
t.space = 'preserve';
|
|
534
|
-
}
|
|
530
|
+
else
|
|
531
|
+
setText(t, value);
|
|
535
532
|
}
|
|
536
533
|
else
|
|
537
534
|
toRemove.push(seg);
|
|
@@ -554,9 +551,7 @@ export class Paragraph {
|
|
|
554
551
|
if (target) {
|
|
555
552
|
const t = target.item.value;
|
|
556
553
|
const at = start - target.start;
|
|
557
|
-
t
|
|
558
|
-
if (/^\s|\s$|\s\s/.test(t.value))
|
|
559
|
-
t.space = 'preserve';
|
|
554
|
+
setText(t, target.text.substring(0, at) + text + target.text.substring(at));
|
|
560
555
|
}
|
|
561
556
|
else {
|
|
562
557
|
// no w:t to extend: a new run next to the segment at `start`, with that run's formatting
|
|
@@ -613,9 +608,7 @@ export class Paragraph {
|
|
|
613
608
|
const t = seg.item.value;
|
|
614
609
|
const head = seg.text.substring(0, at);
|
|
615
610
|
const tail = seg.text.substring(at);
|
|
616
|
-
t
|
|
617
|
-
if (/^\s|\s$|\s\s/.test(head))
|
|
618
|
-
t.space = 'preserve';
|
|
611
|
+
setText(t, head);
|
|
619
612
|
const rest = seg.owner.splice(seg.index + 1);
|
|
620
613
|
const second = runOf([textItem(tail), ...rest], seg.run.rPr ? deepCopy(seg.run.rPr) : undefined);
|
|
621
614
|
seg.runOwner.splice(seg.runIndex + 1, 0, second);
|
|
@@ -807,9 +800,7 @@ export class Paragraph {
|
|
|
807
800
|
continue;
|
|
808
801
|
if (seg.editable) {
|
|
809
802
|
const t = seg.item.value;
|
|
810
|
-
t
|
|
811
|
-
if (/^\s|\s$|\s\s/.test(t.value))
|
|
812
|
-
t.space = 'preserve';
|
|
803
|
+
setText(t, side === 'after' ? seg.text + text : text + seg.text);
|
|
813
804
|
}
|
|
814
805
|
else {
|
|
815
806
|
const run = runOf([textItem(text)], copyRPr(seg.run.rPr));
|
|
@@ -927,3 +918,17 @@ function outlineLevelOf(pPr) {
|
|
|
927
918
|
const level = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.outlineLvl) === null || _a === void 0 ? void 0 : _a.val;
|
|
928
919
|
return level === undefined ? 10 : level + 1;
|
|
929
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Sets a `w:t`'s text with `xml:space="preserve"` exactly when the text needs it - whitespace at
|
|
923
|
+
* either end, or two whitespace characters together - which is how the builders' `t()` and Word
|
|
924
|
+
* write it. Setting the attribute and never taking it off left a text shortened from "Font styles "
|
|
925
|
+
* to "Font X" still preserving, the same text but not the same bytes as a `w:t` made afresh
|
|
926
|
+
* (CR-002 section 32 item 2).
|
|
927
|
+
*/
|
|
928
|
+
function setText(t, value) {
|
|
929
|
+
t.value = value;
|
|
930
|
+
if (/^\s|\s$|\s\s/.test(value))
|
|
931
|
+
t.space = 'preserve';
|
|
932
|
+
else
|
|
933
|
+
delete t.space;
|
|
934
|
+
}
|
|
@@ -90,14 +90,43 @@ export declare class Range {
|
|
|
90
90
|
* Setting removes the hyperlinks the range already carries and wraps its runs in one
|
|
91
91
|
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
92
92
|
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
93
|
-
* this range's part, which is why the part must exist (CR-002 section 20).
|
|
93
|
+
* this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
|
|
94
|
+
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
|
|
95
|
+
*
|
|
96
|
+
* The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
|
|
97
|
+
* 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
|
|
98
|
+
* character style it had, and loses its direct colour, keeping the rest of its direct formatting;
|
|
99
|
+
* a link removed takes the style off its runs again. The `Hyperlink` definition is added to the
|
|
100
|
+
* styles part when the document lacks it, at the latest when the package is saved - a setter
|
|
101
|
+
* cannot await it - so `await pkg.styles.ensure('Hyperlink')` first where an effective read
|
|
102
|
+
* before the save must see it. Under change tracking the restyle is a formatting revision, as a
|
|
103
|
+
* `font` write is.
|
|
94
104
|
*/
|
|
95
105
|
get hyperlink(): string;
|
|
96
106
|
set hyperlink(value: string);
|
|
97
|
-
/** The `w:hyperlink` this range
|
|
107
|
+
/** The first `w:hyperlink` this range touches, if any. */
|
|
98
108
|
private hyperlinkHolder;
|
|
99
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Unwraps every `w:hyperlink` the range touches, keeping its runs where they are and taking the
|
|
111
|
+
* `Hyperlink` style off them. Until 0.2.1 this unwrapped every hyperlink of the paragraph, so
|
|
112
|
+
* linking one word unlinked the others (CR-002 section 25).
|
|
113
|
+
*/
|
|
100
114
|
private removeHyperlinks;
|
|
115
|
+
/**
|
|
116
|
+
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
117
|
+
* (from its first character up to, not including, its end). A hyperlink holding no text touches
|
|
118
|
+
* nothing.
|
|
119
|
+
*/
|
|
120
|
+
private touches;
|
|
121
|
+
private touchesSpan;
|
|
122
|
+
/** The text a hyperlink holds, as [start, end) in the paragraph, or undefined when it holds none. */
|
|
123
|
+
private spanOf;
|
|
124
|
+
/**
|
|
125
|
+
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
126
|
+
* and with the direct colour removed, or off again when the link goes: what Word does (CR-002
|
|
127
|
+
* section 25). A run whose properties are then empty loses its `w:rPr`, as in Word's output.
|
|
128
|
+
*/
|
|
129
|
+
private restyle;
|
|
101
130
|
/** The target of a relationship of this range's part, for the getter. */
|
|
102
131
|
private relationshipTarget;
|
|
103
132
|
/** An external relationship of this range's part for the address, and its id. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Font } from './Font.mjs';
|
|
2
2
|
import { runFontSelectorOf } from '../fonts/lookup.mjs';
|
|
3
|
-
import { typeNameOf, linkParents, runItemsOf } from './tree.mjs';
|
|
3
|
+
import { typeNameOf, linkParents, runItemsOf, runsOf } from './tree.mjs';
|
|
4
4
|
import { Docx4JException } from '../../opc/exceptions.mjs';
|
|
5
5
|
import { ContentControl } from './ContentControl.mjs';
|
|
6
|
-
import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
|
|
6
|
+
import { sdt as sdtOf, nextSdtId, applyRunOptions } from '@docx4j/generated-objects-ts/builders/wml';
|
|
7
7
|
import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
|
|
8
8
|
import { Namespaces } from '../../parts/Namespaces.mjs';
|
|
9
9
|
import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
|
|
@@ -247,7 +247,17 @@ export class Range {
|
|
|
247
247
|
* Setting removes the hyperlinks the range already carries and wraps its runs in one
|
|
248
248
|
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
249
249
|
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
250
|
-
* this range's part, which is why the part must exist (CR-002 section 20).
|
|
250
|
+
* this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
|
|
251
|
+
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
|
|
252
|
+
*
|
|
253
|
+
* The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
|
|
254
|
+
* 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
|
|
255
|
+
* character style it had, and loses its direct colour, keeping the rest of its direct formatting;
|
|
256
|
+
* a link removed takes the style off its runs again. The `Hyperlink` definition is added to the
|
|
257
|
+
* styles part when the document lacks it, at the latest when the package is saved - a setter
|
|
258
|
+
* cannot await it - so `await pkg.styles.ensure('Hyperlink')` first where an effective read
|
|
259
|
+
* before the save must see it. Under change tracking the restyle is a formatting revision, as a
|
|
260
|
+
* `font` write is.
|
|
251
261
|
*/
|
|
252
262
|
get hyperlink() {
|
|
253
263
|
var _a;
|
|
@@ -259,8 +269,24 @@ export class Range {
|
|
|
259
269
|
return anchor === '' ? address : `${address}#${anchor}`;
|
|
260
270
|
}
|
|
261
271
|
set hyperlink(value) {
|
|
262
|
-
var _a;
|
|
272
|
+
var _a, _b, _c;
|
|
263
273
|
const paragraph = this.paragraph;
|
|
274
|
+
// A range that crosses the edge of a link takes the whole of that link into the new one; a range
|
|
275
|
+
// inside a link replaces it with a link over the range alone. Measured in Word through Office JS
|
|
276
|
+
// (test/README.md check 16, cases F and G; CR-002 section 25): "alpha beta" linked and then
|
|
277
|
+
// "beta gamma" is one link over "alpha beta gamma", where "alpha beta gamma" linked and then
|
|
278
|
+
// "beta" leaves "alpha " and " gamma" unlinked.
|
|
279
|
+
let start = this.start;
|
|
280
|
+
let end = this.end;
|
|
281
|
+
for (const holder of hyperlinkHolders(paragraph)) {
|
|
282
|
+
const span = this.spanOf(holder);
|
|
283
|
+
if (span === undefined || !this.touchesSpan(span))
|
|
284
|
+
continue;
|
|
285
|
+
if (span[0] <= this.start && this.end <= span[1])
|
|
286
|
+
continue;
|
|
287
|
+
start = Math.min(start, span[0]);
|
|
288
|
+
end = Math.max(end, span[1]);
|
|
289
|
+
}
|
|
264
290
|
this.removeHyperlinks();
|
|
265
291
|
if (value === '')
|
|
266
292
|
return;
|
|
@@ -277,13 +303,13 @@ export class Range {
|
|
|
277
303
|
holder.value.anchor = location;
|
|
278
304
|
if (address !== '')
|
|
279
305
|
holder.value.id = this.addHyperlinkRelationship(address);
|
|
280
|
-
if (
|
|
281
|
-
paragraph.insertItemsAt(
|
|
306
|
+
if (start === end) {
|
|
307
|
+
paragraph.insertItemsAt(start, [holder]);
|
|
282
308
|
return;
|
|
283
309
|
}
|
|
284
|
-
paragraph.splitAt(
|
|
285
|
-
paragraph.splitAt(
|
|
286
|
-
const segments = paragraph.segments().filter((seg) => seg.start >=
|
|
310
|
+
paragraph.splitAt(start);
|
|
311
|
+
paragraph.splitAt(end);
|
|
312
|
+
const segments = paragraph.segments().filter((seg) => seg.start >= start && seg.end <= end);
|
|
287
313
|
if (segments.length === 0)
|
|
288
314
|
throw new Docx4JException('This range covers no run');
|
|
289
315
|
const owner = segments[0].runOwner;
|
|
@@ -300,35 +326,79 @@ export class Range {
|
|
|
300
326
|
holder.value.content = items;
|
|
301
327
|
owner.splice(at, items.length, holder);
|
|
302
328
|
linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
|
|
329
|
+
this.restyle(holder.value, true);
|
|
330
|
+
(_c = (_b = paragraph.parentBody.package_) === null || _b === void 0 ? void 0 : _b.requireStyles) === null || _c === void 0 ? void 0 : _c.call(_b, [HYPERLINK_STYLE]);
|
|
303
331
|
}
|
|
304
|
-
/** The `w:hyperlink` this range
|
|
332
|
+
/** The first `w:hyperlink` this range touches, if any. */
|
|
305
333
|
hyperlinkHolder() {
|
|
306
|
-
|
|
307
|
-
if (segment.end <= this.start && this.start !== this.end)
|
|
308
|
-
continue;
|
|
309
|
-
if (segment.start >= this.end && this.start !== this.end)
|
|
310
|
-
break;
|
|
311
|
-
const parent = segment.run.PARENT;
|
|
312
|
-
const found = hyperlinkHolders(this.paragraph).find((h) => h.value === parent);
|
|
313
|
-
if (found)
|
|
314
|
-
return found;
|
|
315
|
-
}
|
|
316
|
-
return undefined;
|
|
334
|
+
return hyperlinkHolders(this.paragraph).find((holder) => this.touches(holder));
|
|
317
335
|
}
|
|
318
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* Unwraps every `w:hyperlink` the range touches, keeping its runs where they are and taking the
|
|
338
|
+
* `Hyperlink` style off them. Until 0.2.1 this unwrapped every hyperlink of the paragraph, so
|
|
339
|
+
* linking one word unlinked the others (CR-002 section 25).
|
|
340
|
+
*/
|
|
319
341
|
removeHyperlinks() {
|
|
320
342
|
var _a, _b, _c;
|
|
321
|
-
for (const holder of hyperlinkHolders(this.paragraph)) {
|
|
343
|
+
for (const holder of hyperlinkHolders(this.paragraph).filter((h) => this.touches(h))) {
|
|
322
344
|
const owner = containerOf(holder, this.paragraph);
|
|
323
345
|
const at = owner.indexOf(holder);
|
|
324
346
|
if (at === -1)
|
|
325
347
|
continue;
|
|
348
|
+
this.restyle(holder.value, false);
|
|
326
349
|
owner.splice(at, 1, ...((_a = holder.value.content) !== null && _a !== void 0 ? _a : []));
|
|
327
350
|
for (const item of ((_b = holder.value.content) !== null && _b !== void 0 ? _b : [])) {
|
|
328
351
|
linkParents(item, (_c = holder.PARENT) !== null && _c !== void 0 ? _c : this.paragraph.p);
|
|
329
352
|
}
|
|
330
353
|
}
|
|
331
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
357
|
+
* (from its first character up to, not including, its end). A hyperlink holding no text touches
|
|
358
|
+
* nothing.
|
|
359
|
+
*/
|
|
360
|
+
touches(holder) {
|
|
361
|
+
const span = this.spanOf(holder);
|
|
362
|
+
return span !== undefined && this.touchesSpan(span);
|
|
363
|
+
}
|
|
364
|
+
touchesSpan([start, end]) {
|
|
365
|
+
return this.start === this.end ? start <= this.start && this.start < end : start < this.end && end > this.start;
|
|
366
|
+
}
|
|
367
|
+
/** The text a hyperlink holds, as [start, end) in the paragraph, or undefined when it holds none. */
|
|
368
|
+
spanOf(holder) {
|
|
369
|
+
let start;
|
|
370
|
+
let end = 0;
|
|
371
|
+
for (const segment of this.paragraph.segments()) {
|
|
372
|
+
if (!within(segment.run, holder.value, this.paragraph.p))
|
|
373
|
+
continue;
|
|
374
|
+
start !== null && start !== void 0 ? start : (start = segment.start);
|
|
375
|
+
end = segment.end;
|
|
376
|
+
}
|
|
377
|
+
return start === undefined ? undefined : [start, end];
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
381
|
+
* and with the direct colour removed, or off again when the link goes: what Word does (CR-002
|
|
382
|
+
* section 25). A run whose properties are then empty loses its `w:rPr`, as in Word's output.
|
|
383
|
+
*/
|
|
384
|
+
restyle(holder, linked) {
|
|
385
|
+
var _a, _b, _c;
|
|
386
|
+
const tracking = this.paragraph.fontTracking();
|
|
387
|
+
const runs = new Set([...runsOf(holder), ...runsOf(holder, { view: 'original' })].map((r) => r.value));
|
|
388
|
+
for (const run of runs) {
|
|
389
|
+
if (!linked && ((_b = (_a = run.rPr) === null || _a === void 0 ? void 0 : _a.rStyle) === null || _b === void 0 ? void 0 : _b.val) !== HYPERLINK_STYLE)
|
|
390
|
+
continue;
|
|
391
|
+
(_c = run.rPr) !== null && _c !== void 0 ? _c : (run.rPr = { TYPE_NAME: 'org_docx4j_wml.RPr' });
|
|
392
|
+
// the properties as they are now become w:rPrChange, unless this run is our own insertion
|
|
393
|
+
if (tracking && !tracking.ownInsertions.has(run))
|
|
394
|
+
tracking.tracker.recordRPrChange(run.rPr);
|
|
395
|
+
applyRunOptions(run.rPr, linked ? { style: HYPERLINK_STYLE, color: '' } : { style: '' });
|
|
396
|
+
if (Object.keys(run.rPr).every((key) => key === 'TYPE_NAME'))
|
|
397
|
+
delete run.rPr;
|
|
398
|
+
else
|
|
399
|
+
linkParents(run.rPr, run);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
332
402
|
/** The target of a relationship of this range's part, for the getter. */
|
|
333
403
|
relationshipTarget(id) {
|
|
334
404
|
var _a, _b, _c;
|
|
@@ -343,6 +413,16 @@ export class Range {
|
|
|
343
413
|
return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
|
|
344
414
|
}
|
|
345
415
|
}
|
|
416
|
+
/** The character style Word gives the runs of a hyperlink it makes. */
|
|
417
|
+
const HYPERLINK_STYLE = 'Hyperlink';
|
|
418
|
+
/** Whether `object` is `ancestor` or below it, following `PARENT` no further than `stop`. */
|
|
419
|
+
function within(object, ancestor, stop) {
|
|
420
|
+
for (let o = object; o !== undefined && o !== stop; o = o.PARENT) {
|
|
421
|
+
if (o === ancestor)
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
346
426
|
/** Every `w:hyperlink` of a paragraph, outermost first (they do not nest in practice). */
|
|
347
427
|
function hyperlinkHolders(paragraph) {
|
|
348
428
|
const out = [];
|
|
@@ -70,9 +70,15 @@ export declare class TrackedChange {
|
|
|
70
70
|
get text(): string;
|
|
71
71
|
/** The range the change covers; a deletion is a collapsed range where it sits in the accepted text. */
|
|
72
72
|
getRange(): Range | undefined;
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Keeps the change: a `w:ins` is unwrapped, a `w:del` removed, a deleted mark joins the paragraphs.
|
|
75
|
+
* Either half of a move keeps the whole move, as Word's Review tab does (CR-002 section 29).
|
|
76
|
+
*/
|
|
74
77
|
accept(): void;
|
|
75
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Puts back what was there: a `w:ins` is removed, a `w:del` restored, a `w:rPrChange` re-applied.
|
|
80
|
+
* Either half of a move rejects the whole move, as Word's Review tab does (CR-002 section 29).
|
|
81
|
+
*/
|
|
76
82
|
reject(): void;
|
|
77
83
|
private requireParagraph;
|
|
78
84
|
}
|
|
@@ -86,7 +92,7 @@ export declare class TrackedChange {
|
|
|
86
92
|
* with: the paragraph then goes, its content joining the one before. With neither neighbour the
|
|
87
93
|
* mark is simply dropped.
|
|
88
94
|
*/
|
|
89
|
-
export declare function joinWithNext(paragraph: Paragraph, fallbackToPrevious?: boolean): void;
|
|
95
|
+
export declare function joinWithNext(paragraph: Pick<Paragraph, 'container' | 'element' | 'p'>, fallbackToPrevious?: boolean): void;
|
|
90
96
|
/** The tracked changes of one paragraph, in document order. */
|
|
91
97
|
export declare function trackedChangesOfParagraph(paragraph: Paragraph): TrackedChange[];
|
|
92
98
|
/**
|
|
@@ -73,12 +73,17 @@ export class TrackedChange {
|
|
|
73
73
|
}
|
|
74
74
|
return p.getRange('Whole');
|
|
75
75
|
}
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Keeps the change: a `w:ins` is unwrapped, a `w:del` removed, a deleted mark joins the paragraphs.
|
|
78
|
+
* Either half of a move keeps the whole move, as Word's Review tab does (CR-002 section 29).
|
|
79
|
+
*/
|
|
77
80
|
accept() {
|
|
78
81
|
var _a, _b, _c;
|
|
79
82
|
const t = this.target;
|
|
80
83
|
switch (t.kind) {
|
|
81
84
|
case 'run':
|
|
85
|
+
if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && resolveMove(this, 'accept'))
|
|
86
|
+
return;
|
|
82
87
|
if (t.revision === 'ins' || t.revision === 'moveTo')
|
|
83
88
|
unwrap(t);
|
|
84
89
|
else
|
|
@@ -111,12 +116,17 @@ export class TrackedChange {
|
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Puts back what was there: a `w:ins` is removed, a `w:del` restored, a `w:rPrChange` re-applied.
|
|
121
|
+
* Either half of a move rejects the whole move, as Word's Review tab does (CR-002 section 29).
|
|
122
|
+
*/
|
|
115
123
|
reject() {
|
|
116
124
|
var _a;
|
|
117
125
|
const t = this.target;
|
|
118
126
|
switch (t.kind) {
|
|
119
127
|
case 'run':
|
|
128
|
+
if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && resolveMove(this, 'reject'))
|
|
129
|
+
return;
|
|
120
130
|
if (t.revision === 'ins' || t.revision === 'moveTo')
|
|
121
131
|
remove(t.owner, t.element);
|
|
122
132
|
else
|
|
@@ -298,6 +308,129 @@ function acceptedOffsetOf(p, target) {
|
|
|
298
308
|
visit(runItemsOf(p));
|
|
299
309
|
return found >= 0 ? found : pos;
|
|
300
310
|
}
|
|
311
|
+
const RANGE_MARKERS = {
|
|
312
|
+
moveFromRangeStart: { kind: 'start', side: 'from' }, moveFromRangeEnd: { kind: 'end', side: 'from' },
|
|
313
|
+
moveToRangeStart: { kind: 'start', side: 'to' }, moveToRangeEnd: { kind: 'end', side: 'to' },
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* Accepts or rejects the whole move a run-level half belongs to, as Word's Review tab does from
|
|
317
|
+
* either half (`test/README.md` check 18, Word 16.0.20326.20158; Office JS cannot list a move at
|
|
318
|
+
* all there, check 17). Word pairs the halves by the `w:name` on their range starts
|
|
319
|
+
* (`w:moveFromRangeStart`, `w:moveToRangeStart`); `w:moveFrom` and `w:moveTo` carry none, so a half
|
|
320
|
+
* belongs to the move whose range of its side is open where it stands. Accepting keeps the
|
|
321
|
+
* destination and removes the source, paragraph mark and all; rejecting the other way round; the
|
|
322
|
+
* four range markers go either way. Returns false for a half in no named range - markup without
|
|
323
|
+
* range markers, or a half already resolved - which the caller then treats on its own, as before.
|
|
324
|
+
*/
|
|
325
|
+
function resolveMove(change, action) {
|
|
326
|
+
var _a, _b, _c;
|
|
327
|
+
const t = change.target;
|
|
328
|
+
if (t.kind !== 'run' || !change.paragraph)
|
|
329
|
+
return false;
|
|
330
|
+
const pieces = movePiecesOf(storyOf(change.paragraph.p));
|
|
331
|
+
const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
|
|
332
|
+
if (!half || half.kind !== 'half' || half.names.length === 0)
|
|
333
|
+
return false;
|
|
334
|
+
const name = half.names[0];
|
|
335
|
+
const ids = new Set(pieces.filter((piece) => piece.kind === 'start' && piece.name === name).map((piece) => piece.id));
|
|
336
|
+
const mine = pieces.filter((piece) => (piece.kind === 'start' && piece.name === name) || (piece.kind === 'end' && ids.has(piece.id))
|
|
337
|
+
|| ((piece.kind === 'half' || piece.kind === 'mark') && piece.names.includes(name)));
|
|
338
|
+
const kept = action === 'accept' ? 'to' : 'from';
|
|
339
|
+
// the markers first: a range end between two paragraphs would stop the paragraph join below
|
|
340
|
+
for (const piece of mine)
|
|
341
|
+
if (piece.kind === 'start' || piece.kind === 'end')
|
|
342
|
+
remove(piece.owner, piece.element);
|
|
343
|
+
for (const piece of mine) {
|
|
344
|
+
if (piece.kind !== 'half')
|
|
345
|
+
continue;
|
|
346
|
+
const target = { kind: 'run', revision: piece.side === 'from' ? 'moveFrom' : 'moveTo', element: piece.element, owner: piece.owner, value: piece.element.value };
|
|
347
|
+
if (piece.side === kept) {
|
|
348
|
+
if (piece.side === 'from')
|
|
349
|
+
restoreDeleted(target);
|
|
350
|
+
else
|
|
351
|
+
unwrap(target);
|
|
352
|
+
}
|
|
353
|
+
else
|
|
354
|
+
remove(piece.owner, piece.element);
|
|
355
|
+
}
|
|
356
|
+
// the paragraph marks last, last first, so that a join never disturbs one still to do
|
|
357
|
+
const marks = mine.filter((piece) => piece.kind === 'mark');
|
|
358
|
+
for (let i = marks.length - 1; i >= 0; i--) {
|
|
359
|
+
const mark = marks[i];
|
|
360
|
+
const p = mark.paragraph.value;
|
|
361
|
+
if (mark.side === kept) {
|
|
362
|
+
const rPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
|
|
363
|
+
if (rPr)
|
|
364
|
+
delete rPr[mark.side === 'from' ? 'moveFrom' : 'moveTo'];
|
|
365
|
+
pruneParagraphProperties(p);
|
|
366
|
+
}
|
|
367
|
+
else if (((_b = p.content) !== null && _b !== void 0 ? _b : []).length === 0) {
|
|
368
|
+
remove(mark.container, mark.paragraph); // the moved paragraph, emptied: gone, as in Word
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const rPr = (_c = p.pPr) === null || _c === void 0 ? void 0 : _c.rPr;
|
|
372
|
+
if (rPr)
|
|
373
|
+
delete rPr[mark.side === 'from' ? 'moveFrom' : 'moveTo'];
|
|
374
|
+
joinWithNext({ container: mark.container, element: mark.paragraph, p }, mark.side === 'to');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
/** The story a paragraph is in: the outermost array of block content above it (a body, a header, a note). */
|
|
380
|
+
function storyOf(p) {
|
|
381
|
+
let story;
|
|
382
|
+
for (let o = p; o !== undefined; o = o.PARENT) {
|
|
383
|
+
const content = o.content;
|
|
384
|
+
if (Array.isArray(content) && o.TYPE_NAME !== 'org_docx4j_wml.P')
|
|
385
|
+
story = content;
|
|
386
|
+
}
|
|
387
|
+
return story !== null && story !== void 0 ? story : [];
|
|
388
|
+
}
|
|
389
|
+
/** Every piece of move markup in a story, in document order, each half and mark with the names of the ranges open over it. */
|
|
390
|
+
function movePiecesOf(story) {
|
|
391
|
+
const out = [];
|
|
392
|
+
const open = { from: new Map(), to: new Map() };
|
|
393
|
+
const visit = (items) => {
|
|
394
|
+
var _a, _b, _c, _d, _e, _f;
|
|
395
|
+
for (const el of items) {
|
|
396
|
+
const marker = RANGE_MARKERS[(_b = (_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) !== null && _b !== void 0 ? _b : ''];
|
|
397
|
+
const value = el.value;
|
|
398
|
+
if (marker && value) {
|
|
399
|
+
const id = Number(value.id);
|
|
400
|
+
if (marker.kind === 'start') {
|
|
401
|
+
open[marker.side].set(id, (_c = value.name) !== null && _c !== void 0 ? _c : '');
|
|
402
|
+
out.push({ kind: 'start', side: marker.side, id, name: (_d = value.name) !== null && _d !== void 0 ? _d : '', element: el, owner: items });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
open[marker.side].delete(id);
|
|
406
|
+
out.push({ kind: 'end', side: marker.side, id, element: el, owner: items });
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const revision = revisionKindOf(el);
|
|
411
|
+
if (revision === 'moveFrom' || revision === 'moveTo') {
|
|
412
|
+
const side = revision === 'moveFrom' ? 'from' : 'to';
|
|
413
|
+
out.push({ kind: 'half', side, element: el, owner: items, names: [...open[side].values()] });
|
|
414
|
+
}
|
|
415
|
+
const v = el.value;
|
|
416
|
+
if (typeof v !== 'object' || v === null)
|
|
417
|
+
continue;
|
|
418
|
+
const children = (_e = childrenOf(v)) !== null && _e !== void 0 ? _e : runItemsOf(v);
|
|
419
|
+
if (children)
|
|
420
|
+
visit(children);
|
|
421
|
+
if (typeNameOf(el) === 'org_docx4j_wml.P') {
|
|
422
|
+
const rPr = (_f = v.pPr) === null || _f === void 0 ? void 0 : _f.rPr;
|
|
423
|
+
for (const side of ['from', 'to']) {
|
|
424
|
+
if (rPr === null || rPr === void 0 ? void 0 : rPr[side === 'from' ? 'moveFrom' : 'moveTo']) {
|
|
425
|
+
out.push({ kind: 'mark', side, paragraph: el, container: items, names: [...open[side].values()] });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
visit(story);
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
301
434
|
// --- collecting ----------------------------------------------------------------------------
|
|
302
435
|
/** The tracked changes of one paragraph, in document order. */
|
|
303
436
|
export function trackedChangesOfParagraph(paragraph) {
|
|
@@ -32,7 +32,7 @@ export interface BoundSnapshot {
|
|
|
32
32
|
* on Enter - so no cache is needed, which is what CR-006 section 2 was uncertain about.
|
|
33
33
|
*/
|
|
34
34
|
export declare function bindSnapshot<T>(element: Jsonix.TypedNamedValue<T>): Promise<BoundSnapshot>;
|
|
35
|
-
/** Turns selected nodes into `Bound`s against a snapshot. */
|
|
35
|
+
/** Turns selected nodes into `Bound`s against a snapshot, with one path cache for them all. */
|
|
36
36
|
export declare function boundsOf(nodes: readonly Node[], snapshot: BoundSnapshot): Bound[];
|
|
37
37
|
/**
|
|
38
38
|
* The node's path from the document element, in the prefixed form the part itself uses, with a
|
|
@@ -30,14 +30,15 @@ export async function bindSnapshot(element) {
|
|
|
30
30
|
});
|
|
31
31
|
return { root, objects };
|
|
32
32
|
}
|
|
33
|
-
/** Turns selected nodes into `Bound`s against a snapshot. */
|
|
33
|
+
/** Turns selected nodes into `Bound`s against a snapshot, with one path cache for them all. */
|
|
34
34
|
export function boundsOf(nodes, snapshot) {
|
|
35
|
+
const paths = new Paths();
|
|
35
36
|
return nodes.map((node) => {
|
|
36
37
|
const owner = owningElement(node);
|
|
37
38
|
const bound = {
|
|
38
39
|
node,
|
|
39
40
|
object: owner === undefined ? undefined : snapshot.objects.get(owner),
|
|
40
|
-
path:
|
|
41
|
+
path: paths.of(node),
|
|
41
42
|
};
|
|
42
43
|
if (node.nodeType === 2)
|
|
43
44
|
bound.attribute = node.name;
|
|
@@ -66,29 +67,61 @@ function owningElement(node) {
|
|
|
66
67
|
* than a short one, and docx4j's harness records the same form.
|
|
67
68
|
*/
|
|
68
69
|
export function pathOf(node) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
return new Paths().of(node);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Paths for the nodes of one snapshot. Each parent's children are counted once, on first need,
|
|
74
|
+
* and each element's path is kept for its descendants, so a hit list costs one pass over the
|
|
75
|
+
* parents it touches. Counting a node's preceding siblings afresh for every node and every
|
|
76
|
+
* ancestor was quadratic in a parent's children: the 6,972 hits of `//w:p[w:pPr/w:pStyle]` over a
|
|
77
|
+
* 7,088-paragraph body took 21 s in Chromium, where the marshal took 0.5 s (CR-006 section 10).
|
|
78
|
+
*/
|
|
79
|
+
class Paths {
|
|
80
|
+
constructor() {
|
|
81
|
+
/** For a parent, the position of each element child among its siblings of the same name. */
|
|
82
|
+
this.positions = new Map();
|
|
83
|
+
this.elementPaths = new Map();
|
|
72
84
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
of(node) {
|
|
86
|
+
if (node.nodeType === 2) {
|
|
87
|
+
const owner = node.ownerElement;
|
|
88
|
+
return `${owner === null ? '' : this.of(owner)}/@${node.name}`;
|
|
89
|
+
}
|
|
90
|
+
if (node.nodeType === 3 || node.nodeType === 4) {
|
|
91
|
+
const parent = node.parentNode;
|
|
92
|
+
return `${parent === null ? '' : this.of(parent)}/text()`;
|
|
93
|
+
}
|
|
94
|
+
if (node.nodeType !== 1)
|
|
95
|
+
return '';
|
|
96
|
+
const known = this.elementPaths.get(node);
|
|
97
|
+
if (known !== undefined)
|
|
98
|
+
return known;
|
|
99
|
+
const element = node;
|
|
100
|
+
const parent = element.parentNode;
|
|
101
|
+
const path = parent === null || parent.nodeType !== 1
|
|
102
|
+
? `/${element.nodeName}`
|
|
103
|
+
: `${this.of(parent)}/${element.nodeName}[${this.position(parent, element)}]`;
|
|
104
|
+
this.elementPaths.set(node, path);
|
|
105
|
+
return path;
|
|
76
106
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
position(parent, element) {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
let positions = this.positions.get(parent);
|
|
110
|
+
if (positions === undefined) {
|
|
111
|
+
positions = new Map();
|
|
112
|
+
const counts = new Map();
|
|
113
|
+
for (let sibling = parent.firstChild; sibling; sibling = sibling.nextSibling) {
|
|
114
|
+
if (sibling.nodeType !== 1)
|
|
115
|
+
continue;
|
|
116
|
+
const name = sibling.nodeName;
|
|
117
|
+
const n = ((_a = counts.get(name)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
118
|
+
counts.set(name, n);
|
|
119
|
+
positions.set(sibling, n);
|
|
120
|
+
}
|
|
121
|
+
this.positions.set(parent, positions);
|
|
89
122
|
}
|
|
123
|
+
return (_b = positions.get(element)) !== null && _b !== void 0 ? _b : 0;
|
|
90
124
|
}
|
|
91
|
-
return `${pathOf(parent)}/${element.nodeName}[${index}]`;
|
|
92
125
|
}
|
|
93
126
|
function isElement(node) {
|
|
94
127
|
return typeof node === 'object' && node !== null && node.nodeType === 1;
|
|
@@ -13,7 +13,7 @@ export { contentOf, isFlatOpc, rewriteRelationshipIds, type OoxmlOptions } from
|
|
|
13
13
|
export { type SearchOptions, searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
|
|
14
14
|
export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf, type TextSegment, type Located, type TextView, type TextViewOptions, type RevisionKind, type RevisionHolder } from './tree.mjs';
|
|
15
15
|
export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext, type TrackedChangeType, type TrackedChangeTarget } from './TrackedChange.mjs';
|
|
16
|
-
export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, type TrackingScope, } from './tracking.mjs';
|
|
16
|
+
export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, highestAnnotationId, MARKUP_TYPES, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, type TrackingScope, } from './tracking.mjs';
|
|
17
17
|
export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS, type Element, type Wrapper, type WmlOptions, type Raw, type Interpolated, type RunOptions, type RunFormatting, type ParagraphOptions, type TableOptions, type CellOptions, type SdtKind, type SdtForm, type SdtOptions } from '@docx4j/generated-objects-ts/builders/wml';
|
|
18
18
|
export { StylesFacade, type StylePackageLike } from './stylesFacade.mjs';
|
|
19
19
|
export { bindSnapshot, boundsOf, pathOf, type Bound, type BoundSnapshot } from './binder.mjs';
|
|
@@ -16,7 +16,7 @@ export { searchPattern, findAll, matchesOf, expandReplacement } from './search.m
|
|
|
16
16
|
export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf } from './tree.mjs';
|
|
17
17
|
// change tracking (CR-002 phase F)
|
|
18
18
|
export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext } from './TrackedChange.mjs';
|
|
19
|
-
export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, } from './tracking.mjs';
|
|
19
|
+
export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, highestAnnotationId, MARKUP_TYPES, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, } from './tracking.mjs';
|
|
20
20
|
// The objects package's builders, re-exported so that one import serves the content API and the tree it works on.
|
|
21
21
|
export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS } from '@docx4j/generated-objects-ts/builders/wml';
|
|
22
22
|
export { StylesFacade } from './stylesFacade.mjs';
|
|
@@ -26,6 +26,14 @@ export declare function findAll(text: string, pattern: RegExp): [number, number]
|
|
|
26
26
|
* Every match of a pattern in a text, with its capture groups: what `replaceText` needs to expand
|
|
27
27
|
* `$1` and Word's `\1`. An empty match advances by one rather than looping forever, as `findAll`
|
|
28
28
|
* has always done.
|
|
29
|
+
*
|
|
30
|
+
* A match that starts or ends inside a grapheme cluster is dropped: half of an emoji's surrogate
|
|
31
|
+
* pair (a wildcard `?` or class matches one UTF-16 unit), or an Arabic letter without the
|
|
32
|
+
* combining mark after it (`\p{L}+` stops before the mark). Such a range is not text a person can
|
|
33
|
+
* see, and replacing it splits a character - half a surrogate pair is saved as U+FFFD (CR-002
|
|
34
|
+
* section 32 item 1, found by the editor's agreement suite). The clusters are `Intl.Segmenter`'s,
|
|
35
|
+
* computed only for a text with a code unit at or above U+0300, below which every code unit is a
|
|
36
|
+
* cluster of its own, so Latin text pays nothing; a runtime without `Intl.Segmenter` keeps every match.
|
|
29
37
|
*/
|
|
30
38
|
export declare function matchesOf(text: string, pattern: RegExp): RegExpExecArray[];
|
|
31
39
|
/**
|
|
@@ -94,6 +94,14 @@ export function findAll(text, pattern) {
|
|
|
94
94
|
* Every match of a pattern in a text, with its capture groups: what `replaceText` needs to expand
|
|
95
95
|
* `$1` and Word's `\1`. An empty match advances by one rather than looping forever, as `findAll`
|
|
96
96
|
* has always done.
|
|
97
|
+
*
|
|
98
|
+
* A match that starts or ends inside a grapheme cluster is dropped: half of an emoji's surrogate
|
|
99
|
+
* pair (a wildcard `?` or class matches one UTF-16 unit), or an Arabic letter without the
|
|
100
|
+
* combining mark after it (`\p{L}+` stops before the mark). Such a range is not text a person can
|
|
101
|
+
* see, and replacing it splits a character - half a surrogate pair is saved as U+FFFD (CR-002
|
|
102
|
+
* section 32 item 1, found by the editor's agreement suite). The clusters are `Intl.Segmenter`'s,
|
|
103
|
+
* computed only for a text with a code unit at or above U+0300, below which every code unit is a
|
|
104
|
+
* cluster of its own, so Latin text pays nothing; a runtime without `Intl.Segmenter` keeps every match.
|
|
97
105
|
*/
|
|
98
106
|
export function matchesOf(text, pattern) {
|
|
99
107
|
const out = [];
|
|
@@ -105,7 +113,20 @@ export function matchesOf(text, pattern) {
|
|
|
105
113
|
}
|
|
106
114
|
out.push(m);
|
|
107
115
|
}
|
|
108
|
-
|
|
116
|
+
const boundaries = out.length > 0 ? graphemeBoundaries(text) : undefined;
|
|
117
|
+
return boundaries === undefined ? out : out.filter((m) => boundaries.has(m.index) && boundaries.has(m.index + m[0].length));
|
|
118
|
+
}
|
|
119
|
+
/** The code-unit offsets where a grapheme cluster starts, and the text's end; undefined where every unit is its own cluster. */
|
|
120
|
+
function graphemeBoundaries(text) {
|
|
121
|
+
if (!/[\u0300-\uffff]/.test(text))
|
|
122
|
+
return undefined;
|
|
123
|
+
const Segmenter = Intl.Segmenter;
|
|
124
|
+
if (Segmenter === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
const boundaries = new Set([text.length]);
|
|
127
|
+
for (const segment of new Segmenter(undefined, { granularity: 'grapheme' }).segment(text))
|
|
128
|
+
boundaries.add(segment.index);
|
|
129
|
+
return boundaries;
|
|
109
130
|
}
|
|
110
131
|
/**
|
|
111
132
|
* A replacement with its group references expanded against one match: `$1` to `$9`, `$&` and `$$`
|
|
@@ -75,6 +75,12 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
|
|
|
75
75
|
*/
|
|
76
76
|
private trackingScopes;
|
|
77
77
|
private beforeScopes;
|
|
78
|
+
/**
|
|
79
|
+
* Style ids a synchronous setter has given runs, to be defined in the styles part by the time the
|
|
80
|
+
* package is saved: `Range.hyperlink` writes `w:rStyle w:val="Hyperlink"` and cannot await
|
|
81
|
+
* `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
|
|
82
|
+
*/
|
|
83
|
+
private requiredStyles;
|
|
78
84
|
/**
|
|
79
85
|
* The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
|
|
80
86
|
* source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
|
|
@@ -226,9 +232,27 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
|
|
|
226
232
|
* bytes cannot change; bytes that were set are read again when they are set again.
|
|
227
233
|
*/
|
|
228
234
|
seedAnnotationIds(): Promise<number>;
|
|
235
|
+
/**
|
|
236
|
+
* Records the highest annotation id of a part the caller has read itself, so that
|
|
237
|
+
* `seedAnnotationIds()` does not read it again. An editor that imports the main document part into
|
|
238
|
+
* a model of its own knows that part's ids already, and seeding would parse it a second time -
|
|
239
|
+
* 1.06 s for a 255-page document (CR-002 section 34). The floor rises to `highest` at once, and
|
|
240
|
+
* the part counts as read for the bytes it holds now: set its bytes again and the next seed reads
|
|
241
|
+
* it afresh; unmarshal it and the counter's own scan of it covers it.
|
|
242
|
+
*
|
|
243
|
+
* The caller vouches for the number: one lower than the part's highest id lets a new revision
|
|
244
|
+
* reuse an id. Asynchronous only because a part whose bytes were set is keyed on those bytes.
|
|
245
|
+
*/
|
|
246
|
+
noteAnnotationIds(part: XmlPart<unknown>, highest: number): Promise<void>;
|
|
229
247
|
/** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
|
|
230
248
|
markupRoots(): object[];
|
|
231
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Asks for style definitions a synchronous setter has referenced; they are ensured on save, as
|
|
251
|
+
* `pkg.styles.ensure(ids)` would, and a document that already has them keeps its styles part
|
|
252
|
+
* byte for byte (CR-002 section 25). Await `styles.ensure` instead to have them at once.
|
|
253
|
+
*/
|
|
254
|
+
requireStyles(ids: Iterable<string>): void;
|
|
255
|
+
/** A clone carries the identity, the tracking date, the theme setting, the XPath engine and the styles still to ensure. */
|
|
232
256
|
protected copyPackageSettingsTo(target: OpcPackage): void;
|
|
233
257
|
saveTo<R>(sink: PartSink<R>): Promise<R>;
|
|
234
258
|
/** The paragraph at an address anywhere in the document: 'body/3', 'header:rId5/0', a paraId, or a text match. */
|
|
@@ -75,6 +75,12 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
75
75
|
* applies again, and when none is left the package is as it was (CR-002 section 31).
|
|
76
76
|
*/
|
|
77
77
|
this.trackingScopes = [];
|
|
78
|
+
/**
|
|
79
|
+
* Style ids a synchronous setter has given runs, to be defined in the styles part by the time the
|
|
80
|
+
* package is saved: `Range.hyperlink` writes `w:rStyle w:val="Hyperlink"` and cannot await
|
|
81
|
+
* `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
|
|
82
|
+
*/
|
|
83
|
+
this.requiredStyles = new Set();
|
|
78
84
|
/**
|
|
79
85
|
* The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
|
|
80
86
|
* source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
|
|
@@ -442,6 +448,28 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
442
448
|
}
|
|
443
449
|
return this.annotationIdFloor;
|
|
444
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Records the highest annotation id of a part the caller has read itself, so that
|
|
453
|
+
* `seedAnnotationIds()` does not read it again. An editor that imports the main document part into
|
|
454
|
+
* a model of its own knows that part's ids already, and seeding would parse it a second time -
|
|
455
|
+
* 1.06 s for a 255-page document (CR-002 section 34). The floor rises to `highest` at once, and
|
|
456
|
+
* the part counts as read for the bytes it holds now: set its bytes again and the next seed reads
|
|
457
|
+
* it afresh; unmarshal it and the counter's own scan of it covers it.
|
|
458
|
+
*
|
|
459
|
+
* The caller vouches for the number: one lower than the part's highest id lets a new revision
|
|
460
|
+
* reuse an id. Asynchronous only because a part whose bytes were set is keyed on those bytes.
|
|
461
|
+
*/
|
|
462
|
+
async noteAnnotationIds(part, highest) {
|
|
463
|
+
if (!Number.isInteger(highest) || highest < 0) {
|
|
464
|
+
throw new Docx4JException(`The highest annotation id must be a non-negative integer, not ${String(highest)}`);
|
|
465
|
+
}
|
|
466
|
+
if (this.parts.get(part.partName.name) !== part) {
|
|
467
|
+
throw new Docx4JException(`${part.partName.name} is not a part of this package`);
|
|
468
|
+
}
|
|
469
|
+
this.annotationIdFloor = Math.max(this.annotationIdFloor, highest);
|
|
470
|
+
if (!part.isUnmarshalled)
|
|
471
|
+
this.seededParts.set(part, part.isUntouched ? true : await part.getBytes());
|
|
472
|
+
}
|
|
445
473
|
/** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
|
|
446
474
|
markupRoots() {
|
|
447
475
|
const out = [];
|
|
@@ -454,10 +482,20 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
454
482
|
}
|
|
455
483
|
return out;
|
|
456
484
|
}
|
|
457
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* Asks for style definitions a synchronous setter has referenced; they are ensured on save, as
|
|
487
|
+
* `pkg.styles.ensure(ids)` would, and a document that already has them keeps its styles part
|
|
488
|
+
* byte for byte (CR-002 section 25). Await `styles.ensure` instead to have them at once.
|
|
489
|
+
*/
|
|
490
|
+
requireStyles(ids) {
|
|
491
|
+
for (const id of ids)
|
|
492
|
+
this.requiredStyles.add(id);
|
|
493
|
+
}
|
|
494
|
+
/** A clone carries the identity, the tracking date, the theme setting, the XPath engine and the styles still to ensure. */
|
|
458
495
|
copyPackageSettingsTo(target) {
|
|
459
496
|
if (!(target instanceof WordprocessingMLPackage))
|
|
460
497
|
return;
|
|
498
|
+
target.requireStyles(this.requiredStyles);
|
|
461
499
|
target.author = { ...this.author };
|
|
462
500
|
target.trackedChangeDate = this.trackedChangeDate;
|
|
463
501
|
target.fonts.defaultTheme = this.fonts.defaultTheme;
|
|
@@ -467,6 +505,10 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
467
505
|
var _a;
|
|
468
506
|
if (this.trackingPending)
|
|
469
507
|
await this.setChangeTrackingMode((_a = this.trackingMode) !== null && _a !== void 0 ? _a : 'Off');
|
|
508
|
+
if (this.requiredStyles.size > 0) {
|
|
509
|
+
await this.styles.ensure([...this.requiredStyles]);
|
|
510
|
+
this.requiredStyles.clear();
|
|
511
|
+
}
|
|
470
512
|
return super.saveTo(sink);
|
|
471
513
|
}
|
|
472
514
|
/** The paragraph at an address anywhere in the document: 'body/3', 'header:rId5/0', a paraId, or a text match. */
|
package/dist/xml/dom.d.mts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/** The declaration docx4j writes on every XML part. */
|
|
2
2
|
export declare const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Parses XML text to a DOM document; throws on malformed input, in a browser as in Node.
|
|
5
|
+
*
|
|
6
|
+
* xmldom throws. A browser's `DOMParser` does not: Chromium returns the document up to the error
|
|
7
|
+
* with an XHTML `parsererror` element first in the root, and Firefox a `parsererror` root in its
|
|
8
|
+
* own namespace, and handing that on unmarshalled a truncated part without complaint - a settings
|
|
9
|
+
* part with a stray `<` kept its first child and lost the rest, which a save would have written
|
|
10
|
+
* (CR-002 section 33). Such a document is refused here, with the parser's own message, which
|
|
11
|
+
* carries the line and column in both browsers.
|
|
12
|
+
*/
|
|
4
13
|
export declare function parseXml(text: string): Document;
|
|
5
14
|
/** Serialises a node (document or element) without an XML declaration. */
|
|
6
15
|
export declare function serializeXml(node: Node): string;
|
package/dist/xml/dom.mjs
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
1
|
// DOM, text and base64 helpers over the runtime's DOM implementation. In Node the runtime brings
|
|
2
2
|
// @xmldom/xmldom; in browsers it uses the native DOM. Jsonix.DOM is typed since @docx4j/jsonix 3.2.1.
|
|
3
3
|
import { Jsonix } from '@docx4j/generated-objects-ts';
|
|
4
|
+
import { Docx4JException } from '../opc/exceptions.mjs';
|
|
4
5
|
const DOM = Jsonix.DOM;
|
|
5
6
|
/** The declaration docx4j writes on every XML part. */
|
|
6
7
|
export const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Parses XML text to a DOM document; throws on malformed input, in a browser as in Node.
|
|
10
|
+
*
|
|
11
|
+
* xmldom throws. A browser's `DOMParser` does not: Chromium returns the document up to the error
|
|
12
|
+
* with an XHTML `parsererror` element first in the root, and Firefox a `parsererror` root in its
|
|
13
|
+
* own namespace, and handing that on unmarshalled a truncated part without complaint - a settings
|
|
14
|
+
* part with a stray `<` kept its first child and lost the rest, which a save would have written
|
|
15
|
+
* (CR-002 section 33). Such a document is refused here, with the parser's own message, which
|
|
16
|
+
* carries the line and column in both browsers.
|
|
17
|
+
*/
|
|
8
18
|
export function parseXml(text) {
|
|
9
|
-
|
|
19
|
+
const doc = DOM.parse(text);
|
|
20
|
+
const error = parserErrorOf(doc);
|
|
21
|
+
if (error !== undefined)
|
|
22
|
+
throw new Docx4JException(`Malformed XML: ${error}`);
|
|
23
|
+
return doc;
|
|
24
|
+
}
|
|
25
|
+
/** The namespaces a browser puts its `parsererror` element in: Chromium and WebKit's, Firefox's. */
|
|
26
|
+
const PARSER_ERROR_NAMESPACES = new Set([
|
|
27
|
+
'http://www.w3.org/1999/xhtml',
|
|
28
|
+
'http://www.mozilla.org/newlayout/xml/parsererror.xml',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* The message of a browser's `parsererror` document - the root, or the root's first element child,
|
|
32
|
+
* named `parsererror` in one of the parser-error namespaces - or undefined for a document that
|
|
33
|
+
* parsed. Chromium's message is the `div` in its element; its `h3`s are boilerplate.
|
|
34
|
+
*/
|
|
35
|
+
function parserErrorOf(doc) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
const root = doc.documentElement;
|
|
38
|
+
if (!root)
|
|
39
|
+
return undefined;
|
|
40
|
+
let first = null;
|
|
41
|
+
for (let child = root.firstChild; child; child = child.nextSibling) {
|
|
42
|
+
if (child.nodeType === 1) {
|
|
43
|
+
first = child;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const element of [root, first]) {
|
|
48
|
+
if (element === null || element.localName !== 'parsererror' || !PARSER_ERROR_NAMESPACES.has((_a = element.namespaceURI) !== null && _a !== void 0 ? _a : ''))
|
|
49
|
+
continue;
|
|
50
|
+
const div = element.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', 'div')[0];
|
|
51
|
+
return ((_b = (div !== null && div !== void 0 ? div : element).textContent) !== null && _b !== void 0 ? _b : '').replace(/\s+/g, ' ').trim() || 'the parser reported an error';
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
10
54
|
}
|
|
11
55
|
/** Serialises a node (document or element) without an XML declaration. */
|
|
12
56
|
export function serializeXml(node) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docx4j/core-ts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "docx4j-core for TypeScript: Open Packaging (docx/pptx/xlsx zip and flat OPC), typed parts and relationships, style, numbering and font resolution over @docx4j/generated-objects-ts. Node, browsers, Word add-ins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|