@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 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 (this.start === this.end) {
291
- paragraph.insertItemsAt(this.start, [holder]);
306
+ if (start === end) {
307
+ paragraph.insertItemsAt(start, [holder]);
292
308
  return;
293
309
  }
294
- paragraph.splitAt(this.start);
295
- paragraph.splitAt(this.end);
296
- const segments = paragraph.segments().filter((seg) => seg.start >= this.start && seg.end <= this.end);
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
- if (start === undefined)
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
- /** Keeps the change: a `w:ins` is unwrapped, a `w:del` removed, a deleted mark joins the paragraphs. */
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
- /** Puts back what was there: a `w:ins` is removed, a `w:del` restored, a `w:rPrChange` re-applied. */
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
- /** Keeps the change: a `w:ins` is unwrapped, a `w:del` removed, a deleted mark joins the paragraphs. */
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
- /** Puts back what was there: a `w:ins` is removed, a `w:del` restored, a `w:rPrChange` re-applied. */
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
- return out;
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.1",
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",