@docx4j/core-ts 0.2.1 → 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 +3 -1
- package/dist/model/content/Range.d.mts +3 -0
- package/dist/model/content/Range.mjs +30 -8
- package/dist/model/content/TrackedChange.d.mts +9 -3
- package/dist/model/content/TrackedChange.mjs +135 -2
- 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 +12 -0
- package/dist/packages/WordprocessingMLPackage.mjs +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,9 @@ body.acceptAll(); // or rejectAll(), or accept()/reject() one at a time
|
|
|
231
231
|
```
|
|
232
232
|
|
|
233
233
|
`getTrackedChanges()` is also on `Paragraph` and `Range`. Text and searches read the accepted
|
|
234
|
-
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.
|
|
235
237
|
|
|
236
238
|
To record one caller's edits without touching the document's own setting - an agent working in a
|
|
237
239
|
document whose `w:trackRevisions` belongs to its owner - use `withTracking`. Nothing is written
|
|
@@ -118,6 +118,9 @@ export declare class Range {
|
|
|
118
118
|
* nothing.
|
|
119
119
|
*/
|
|
120
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;
|
|
121
124
|
/**
|
|
122
125
|
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
123
126
|
* and with the direct colour removed, or off again when the link goes: what Word does (CR-002
|
|
@@ -271,6 +271,22 @@ export class Range {
|
|
|
271
271
|
set hyperlink(value) {
|
|
272
272
|
var _a, _b, _c;
|
|
273
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
|
+
}
|
|
274
290
|
this.removeHyperlinks();
|
|
275
291
|
if (value === '')
|
|
276
292
|
return;
|
|
@@ -287,13 +303,13 @@ export class Range {
|
|
|
287
303
|
holder.value.anchor = location;
|
|
288
304
|
if (address !== '')
|
|
289
305
|
holder.value.id = this.addHyperlinkRelationship(address);
|
|
290
|
-
if (
|
|
291
|
-
paragraph.insertItemsAt(
|
|
306
|
+
if (start === end) {
|
|
307
|
+
paragraph.insertItemsAt(start, [holder]);
|
|
292
308
|
return;
|
|
293
309
|
}
|
|
294
|
-
paragraph.splitAt(
|
|
295
|
-
paragraph.splitAt(
|
|
296
|
-
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);
|
|
297
313
|
if (segments.length === 0)
|
|
298
314
|
throw new Docx4JException('This range covers no run');
|
|
299
315
|
const owner = segments[0].runOwner;
|
|
@@ -342,6 +358,14 @@ export class Range {
|
|
|
342
358
|
* nothing.
|
|
343
359
|
*/
|
|
344
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) {
|
|
345
369
|
let start;
|
|
346
370
|
let end = 0;
|
|
347
371
|
for (const segment of this.paragraph.segments()) {
|
|
@@ -350,9 +374,7 @@ export class Range {
|
|
|
350
374
|
start !== null && start !== void 0 ? start : (start = segment.start);
|
|
351
375
|
end = segment.end;
|
|
352
376
|
}
|
|
353
|
-
|
|
354
|
-
return false;
|
|
355
|
-
return this.start === this.end ? start <= this.start && this.start < end : start < this.end && end > this.start;
|
|
377
|
+
return start === undefined ? undefined : [start, end];
|
|
356
378
|
}
|
|
357
379
|
/**
|
|
358
380
|
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
@@ -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) {
|
|
@@ -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 `$$`
|
|
@@ -232,6 +232,18 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
|
|
|
232
232
|
* bytes cannot change; bytes that were set are read again when they are set again.
|
|
233
233
|
*/
|
|
234
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>;
|
|
235
247
|
/** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
|
|
236
248
|
markupRoots(): object[];
|
|
237
249
|
/**
|
|
@@ -448,6 +448,28 @@ export class WordprocessingMLPackage extends OpcPackage {
|
|
|
448
448
|
}
|
|
449
449
|
return this.annotationIdFloor;
|
|
450
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
|
+
}
|
|
451
473
|
/** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
|
|
452
474
|
markupRoots() {
|
|
453
475
|
const out = [];
|
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",
|