@docx4j/core-ts 0.3.0 → 0.3.1

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.
@@ -1,8 +1,9 @@
1
1
  import { deepCopy, deepCopyAsSync } from '@docx4j/generated-objects-ts';
2
- import { createCTTrackChange, createCTRPrChange, createCTRPrChangeRPr, createCTPPrChange, createDelText, createPPr, createParaRPr, createTrPr, } from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
2
+ import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
3
+ import { createCTTrackChange, createCTRPrChange, createCTRPrChangeRPr, createCTPPrChange, createCTMarkup, createDelText, createPPr, createParaRPr, createTrPr, } from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
3
4
  import { rPrToElements, rPrFromElements } from '@docx4j/generated-objects-ts/builders/wml';
4
5
  import { Docx4JException } from '../../opc/exceptions.mjs';
5
- import { W_NS, linkParents, walk, typeNameOf, revisionKindOf } from './tree.mjs';
6
+ import { W_NS, linkParents, walk, typeNameOf, revisionKindOf, childrenOf, rowsOf, cellsOf } from './tree.mjs';
6
7
  /**
7
8
  * `w:id` on these is the one annotation id space (ECMA-376 17.13.5.4): revisions, bookmarks,
8
9
  * comment range marks and permissions share it. `w:comment/@w:id` (docx4j `Comments.Comment`,
@@ -294,49 +295,166 @@ export class ChangeTracker {
294
295
  }
295
296
  }
296
297
  /**
297
- * A whole paragraph inserted: the mark is marked inserted and every run of it goes into a
298
- * `w:ins`. Word, splitting a paragraph, marks the *first* paragraph's mark instead and leaves
299
- * the new one the original mark; marking the new paragraph's own mark is the same document once
298
+ * A whole paragraph inserted: the mark is marked inserted (unless it already is) and every run of it
299
+ * goes into a `w:ins` - the runs in its hyperlinks, inline content controls, fields and smart tags as
300
+ * well (`wrapNewRuns`). Word, splitting a paragraph, marks the *first* paragraph's mark instead and
301
+ * leaves the new one the original mark; marking the new paragraph's own mark is the same document once
300
302
  * accepted or rejected and keeps the edit to the element that was added (CR-002 section 13).
301
303
  */
302
304
  export function trackInsertedParagraph(tracker, p) {
303
- tracker.markParagraphInserted(p);
305
+ var _a, _b;
306
+ if (!((_b = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr) === null || _b === void 0 ? void 0 : _b.ins))
307
+ tracker.markParagraphInserted(p);
304
308
  wrapNewRuns(tracker, p);
305
309
  }
306
- /** Every top-level run of the paragraph that is not already in a revision, wrapped in one `w:ins` per group. */
310
+ /**
311
+ * Every run of the paragraph not already in a revision, wrapped in one `w:ins` per group of touching
312
+ * runs - at the top level and inside the run-level holders, where Word's form decides (`test/README.md`
313
+ * check 30, CR-002 section 37 item 7): in a hyperlink or a simple field the `w:ins` goes inside, since
314
+ * neither can be in one (`CT_RunTrackChange` holds `EG_ContentRunContent` only); a smart tag goes
315
+ * *into* the `w:ins` with the runs beside it, its own runs plain; an inline content control or custom
316
+ * XML element keeps the `w:ins` inside and is itself marked inserted (`markHolderInserted`). Before,
317
+ * only the top level was wrapped, so that rejecting an inserted paragraph left a link's text behind
318
+ * (the editor's finding on 0.3.0, CR-002 section 37 item 4).
319
+ */
307
320
  export function wrapNewRuns(tracker, p) {
308
- const content = p.content;
309
- if (!content)
321
+ wrapInsertedRuns(tracker, p.content, p);
322
+ }
323
+ /** True for what goes into a `w:ins` beside the runs: a run, or a smart tag (Word's form, check 30). */
324
+ function wrapsAsRun(item) {
325
+ const tn = typeNameOf(item);
326
+ return (tn === 'org_docx4j_wml.R' || tn === 'org_docx4j_wml.CTSmartTagRun') && revisionKindOf(item) === undefined;
327
+ }
328
+ /**
329
+ * `wrapNewRuns` over run-level items that are about to go into a paragraph (`Paragraph.insertItemsAt`):
330
+ * the runs among them, and those in their holders, wrapped in `w:ins`; a content control or custom
331
+ * XML element among them marked inserted. `parent` is what the items' array belongs to.
332
+ */
333
+ export function wrapInsertedRuns(tracker, items, parent) {
334
+ var _a, _b;
335
+ if (!items)
310
336
  return;
311
- for (let i = 0; i < content.length; i++) {
312
- if (typeNameOf(content[i]) !== 'org_docx4j_wml.R' || revisionKindOf(content[i]) !== undefined)
337
+ for (let i = 0; i < items.length; i++) {
338
+ const item = items[i];
339
+ if (revisionKindOf(item) !== undefined)
340
+ continue; // already a revision
341
+ if (wrapsAsRun(item)) {
342
+ let j = i;
343
+ while (j < items.length && wrapsAsRun(items[j]))
344
+ j++;
345
+ const runs = items.splice(i, j - i);
346
+ const ins = tracker.ins(runs);
347
+ items.splice(i, 0, ins);
348
+ linkParents(ins, parent);
349
+ continue;
350
+ }
351
+ const value = item.value;
352
+ if (typeof value !== 'object' || value === null)
313
353
  continue;
314
- let j = i;
315
- while (j < content.length && typeNameOf(content[j]) === 'org_docx4j_wml.R' && revisionKindOf(content[j]) === undefined)
316
- j++;
317
- const runs = content.splice(i, j - i);
318
- const ins = tracker.ins(runs);
319
- content.splice(i, 0, ins);
320
- linkParents(ins, p);
354
+ const holder = (_a = value.sdtContent) !== null && _a !== void 0 ? _a : value;
355
+ if (HOLDER_TYPES.has((_b = typeNameOf(item)) !== null && _b !== void 0 ? _b : '')) {
356
+ const before = items.length;
357
+ markHolderInserted(tracker, item, items, () => wrapInsertedRuns(tracker, childrenOf(value), holder));
358
+ i += items.length - before;
359
+ continue;
360
+ }
361
+ wrapInsertedRuns(tracker, childrenOf(value), holder);
321
362
  }
322
363
  }
323
- /** A whole table inserted: every row is marked inserted and every paragraph in it is an insertion. */
364
+ /** A whole table inserted: every row is marked inserted and everything in its cells is an insertion (`trackInsertedBlocks`). */
324
365
  export function trackInsertedTable(tracker, tbl) {
325
- var _a, _b, _c;
326
- for (const row of ((_a = tbl.content) !== null && _a !== void 0 ? _a : [])) {
327
- if (typeNameOf(row) !== 'org_docx4j_wml.Tr')
328
- continue;
329
- const tr = row.value;
330
- tracker.markRowInserted(tr);
331
- for (const cell of ((_b = tr.content) !== null && _b !== void 0 ? _b : [])) {
332
- if (typeNameOf(cell) !== 'org_docx4j_wml.Tc')
333
- continue;
334
- for (const block of ((_c = cell.value.content) !== null && _c !== void 0 ? _c : [])) {
335
- if (typeNameOf(block) === 'org_docx4j_wml.P')
336
- trackInsertedParagraph(tracker, block.value);
337
- }
338
- }
366
+ var _a;
367
+ for (const row of rowsOf(tbl)) {
368
+ const tr = row.element.value;
369
+ if (!((_a = tr.trPr) === null || _a === void 0 ? void 0 : _a.ins))
370
+ tracker.markRowInserted(tr);
371
+ for (const cell of cellsOf(tr))
372
+ trackInsertedBlocks(tracker, childrenOf(cell.element.value));
373
+ }
374
+ }
375
+ /**
376
+ * Block content inserted, whatever it holds: a paragraph (`trackInsertedParagraph`), a table
377
+ * (`trackInsertedTable`: rows inside row-level content controls, nested tables in its cells), and a
378
+ * block-level content control's or custom XML element's content, recursively, the element itself
379
+ * marked inserted (`markHolderInserted`; `owner` is the array holding `block`, which takes the
380
+ * markers). What `Body.insertElement` and `Table.addRows` track new content by (CR-002 section 37).
381
+ */
382
+ export function trackInsertedBlock(tracker, block, owner) {
383
+ const tn = typeNameOf(block);
384
+ if (tn === 'org_docx4j_wml.P')
385
+ trackInsertedParagraph(tracker, block.value);
386
+ else if (tn === 'org_docx4j_wml.Tbl')
387
+ trackInsertedTable(tracker, block.value);
388
+ else if (typeof block.value !== 'object' || block.value === null)
389
+ return;
390
+ else if (HOLDER_TYPES.has(tn !== null && tn !== void 0 ? tn : ''))
391
+ markHolderInserted(tracker, block, owner, () => trackInsertedBlocks(tracker, childrenOf(block.value)));
392
+ else
393
+ trackInsertedBlocks(tracker, childrenOf(block.value));
394
+ }
395
+ /** `trackInsertedBlock` over every item of a list, which is their owner. */
396
+ export function trackInsertedBlocks(tracker, items) {
397
+ if (!items)
398
+ return;
399
+ for (let i = 0; i < items.length; i++) {
400
+ const before = items.length;
401
+ trackInsertedBlock(tracker, items[i], items);
402
+ i += items.length - before; // past the markers just put in
403
+ }
404
+ }
405
+ /**
406
+ * The elements Word marks inserted as a whole with `w:customXmlInsRangeStart` / `End`: a content
407
+ * control or a custom XML element (check 30 measured the control at run and block level; a smart tag
408
+ * goes into the `w:ins` instead, and a hyperlink or field has no form of its own). Row- and cell-level
409
+ * ones are listed for the reader: `trackInsertedTable` walks through them to the rows and cells, and
410
+ * does not mark them.
411
+ */
412
+ export const HOLDER_TYPES = new Set([
413
+ 'org_docx4j_wml.SdtRun', 'org_docx4j_wml.SdtBlock', 'org_docx4j_wml.CTSdtRow', 'org_docx4j_wml.CTSdtCell',
414
+ 'org_docx4j_wml.CTCustomXmlRun', 'org_docx4j_wml.CTCustomXmlBlock', 'org_docx4j_wml.CTCustomXmlRow', 'org_docx4j_wml.CTCustomXmlCell',
415
+ ]);
416
+ /** The list a holder's content is in (`w:sdtContent`'s for a control, its own for custom XML), created when absent. */
417
+ export function holderItemsOf(value) {
418
+ var _a, _b;
419
+ var _c;
420
+ const v = value;
421
+ if (v.sdtContent)
422
+ return ((_a = (_c = v.sdtContent).content) !== null && _a !== void 0 ? _a : (_c.content = []));
423
+ if ('element' in v)
424
+ return ((_b = v.content) !== null && _b !== void 0 ? _b : (v.content = [])); // w:customXml carries w:element
425
+ return undefined;
426
+ }
427
+ /**
428
+ * A content control or custom XML element inserted under tracking, as Word writes it (check 30): a
429
+ * `w:customXmlInsRangeStart` before the element with its `w:customXmlInsRangeEnd` first inside its
430
+ * content, and a second start last inside with its end after the element. Rejecting takes the element
431
+ * away and leaves its content where it stood (`TrackedChange`'s `holder` target); without the markers
432
+ * an element holding only an insertion reads as one that was there before and was typed into, which
433
+ * rejecting keeps (check 30, existing-control-typed). `inside` tracks the content between the two
434
+ * pairs, so that the ids run in document order as Word's do.
435
+ */
436
+ export function markHolderInserted(tracker, holder, owner, inside) {
437
+ var _a;
438
+ const at = owner.indexOf(holder);
439
+ const items = holderItemsOf(holder.value);
440
+ if (at < 0 || !items) {
441
+ inside();
442
+ return;
339
443
  }
444
+ const value = holder.value;
445
+ const first = tracker.markup();
446
+ const outerStart = el.customXmlInsRangeStart(createCTTrackChange(first));
447
+ const innerEnd = el.customXmlInsRangeEnd(createCTMarkup({ id: first.id }));
448
+ owner.splice(at, 0, outerStart);
449
+ items.unshift(innerEnd);
450
+ inside();
451
+ const second = tracker.markup();
452
+ const innerStart = el.customXmlInsRangeStart(createCTTrackChange(second));
453
+ const outerEnd = el.customXmlInsRangeEnd(createCTMarkup({ id: second.id }));
454
+ items.push(innerStart);
455
+ owner.splice(owner.indexOf(holder) + 1, 0, outerEnd);
456
+ linkParents([outerStart, outerEnd], value.PARENT);
457
+ linkParents([innerEnd, innerStart], (_a = value.sdtContent) !== null && _a !== void 0 ? _a : value);
340
458
  }
341
459
  /** The paragraph mark's run properties (`w:pPr/w:rPr`), created when absent. */
342
460
  export function paraRPrOf(p) {
@@ -81,6 +81,12 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
81
81
  * `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
82
82
  */
83
83
  private requiredStyles;
84
+ /**
85
+ * Comments whose anchored text went with a rejected insertion or an accepted deletion: their
86
+ * markers are gone, their entries go from the comment parts when the comments are next read or
87
+ * the package is saved (check 32; `TrackedChange`). An extension.
88
+ */
89
+ readonly pendingCommentRemovals: Set<number>;
84
90
  /**
85
91
  * The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
86
92
  * source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
@@ -15,6 +15,7 @@ import { DefaultXPathEngine } from '../model/customxml/xpath.mjs';
15
15
  import { ChangeTracker, highestAnnotationId } from '../model/content/tracking.mjs';
16
16
  import { NumberingFacade } from '../model/content/List.mjs';
17
17
  import { StylesFacade } from '../model/content/stylesFacade.mjs';
18
+ import { flushPendingCommentRemovals } from '../model/content/Comment.mjs';
18
19
  import { PropertyResolver } from '../model/properties/PropertyResolver.mjs';
19
20
  import { PropertyResolverNotCreatedException } from '../opc/exceptions.mjs';
20
21
  import { XmlPart } from '../parts/XmlPart.mjs';
@@ -81,6 +82,12 @@ export class WordprocessingMLPackage extends OpcPackage {
81
82
  * `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
82
83
  */
83
84
  this.requiredStyles = new Set();
85
+ /**
86
+ * Comments whose anchored text went with a rejected insertion or an accepted deletion: their
87
+ * markers are gone, their entries go from the comment parts when the comments are next read or
88
+ * the package is saved (check 32; `TrackedChange`). An extension.
89
+ */
90
+ this.pendingCommentRemovals = new Set();
84
91
  /**
85
92
  * The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
86
93
  * source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
@@ -497,6 +504,8 @@ export class WordprocessingMLPackage extends OpcPackage {
497
504
  return;
498
505
  target.requireStyles(this.requiredStyles);
499
506
  target.author = { ...this.author };
507
+ for (const id of this.pendingCommentRemovals)
508
+ target.pendingCommentRemovals.add(id);
500
509
  target.trackedChangeDate = this.trackedChangeDate;
501
510
  target.fonts.defaultTheme = this.fonts.defaultTheme;
502
511
  target.xpathEngine = this.xpathEngine;
@@ -505,6 +514,8 @@ export class WordprocessingMLPackage extends OpcPackage {
505
514
  var _a;
506
515
  if (this.trackingPending)
507
516
  await this.setChangeTrackingMode((_a = this.trackingMode) !== null && _a !== void 0 ? _a : 'Off');
517
+ if (this.pendingCommentRemovals.size > 0)
518
+ await flushPendingCommentRemovals(await this.getBody());
508
519
  if (this.requiredStyles.size > 0) {
509
520
  await this.styles.ensure([...this.requiredStyles]);
510
521
  this.requiredStyles.clear();
@@ -0,0 +1,3 @@
1
+ import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
+ /** Fresh copies of the two definitions, each call. */
3
+ export declare function commentStyles(): wml.Style[];
@@ -0,0 +1,25 @@
1
+ import * as wmlFactory from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
2
+ import { COMMENT_REFERENCE_STYLE, COMMENT_TEXT_STYLE } from '../../model/content/comments.mjs';
3
+ /** Fresh copies of the two definitions, each call. */
4
+ export function commentStyles() {
5
+ return [
6
+ wmlFactory.createStyle({
7
+ type: 'paragraph', styleId: COMMENT_TEXT_STYLE,
8
+ name: wmlFactory.createStyleName({ val: 'annotation text' }),
9
+ basedOn: wmlFactory.createStyleBasedOn({ val: 'Normal' }),
10
+ uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
11
+ semiHidden: wmlFactory.createBooleanDefaultTrue(),
12
+ unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
13
+ rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 20 }), szCs: wmlFactory.createHpsMeasure({ val: 20 }) }),
14
+ }),
15
+ wmlFactory.createStyle({
16
+ type: 'character', styleId: COMMENT_REFERENCE_STYLE,
17
+ name: wmlFactory.createStyleName({ val: 'annotation reference' }),
18
+ basedOn: wmlFactory.createStyleBasedOn({ val: 'DefaultParagraphFont' }),
19
+ uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
20
+ semiHidden: wmlFactory.createBooleanDefaultTrue(),
21
+ unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
22
+ rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 16 }), szCs: wmlFactory.createHpsMeasure({ val: 16 }) }),
23
+ }),
24
+ ];
25
+ }
@@ -9,6 +9,7 @@ import { COMMENT_REFERENCE_STYLE, COMMENT_TEXT_STYLE, setCommentPartsAccess, dat
9
9
  import { dateOf } from '../../model/content/tracking.mjs';
10
10
  // Loaded for its side effect: the Comment views register themselves with the content API.
11
11
  import '../../model/content/Comment.mjs';
12
+ import { commentStyles } from './commentStyles.mjs';
12
13
  /** The comment parts of one document. */
13
14
  class DocumentCommentParts {
14
15
  constructor(main, commentsPart, comments, author) {
@@ -180,28 +181,9 @@ async function ensureCommentStyles(main) {
180
181
  linkParents(style, styles);
181
182
  (_a = pkg === null || pkg === void 0 ? void 0 : pkg.refreshPropertyResolver) === null || _a === void 0 ? void 0 : _a.call(pkg);
182
183
  };
183
- if (!list.some((s) => s.styleId === COMMENT_TEXT_STYLE)) {
184
- add(wmlFactory.createStyle({
185
- type: 'paragraph', styleId: COMMENT_TEXT_STYLE,
186
- name: wmlFactory.createStyleName({ val: 'annotation text' }),
187
- basedOn: wmlFactory.createStyleBasedOn({ val: 'Normal' }),
188
- uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
189
- semiHidden: wmlFactory.createBooleanDefaultTrue(),
190
- unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
191
- rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 20 }), szCs: wmlFactory.createHpsMeasure({ val: 20 }) }),
192
- }));
193
- }
194
- if (!list.some((s) => s.styleId === COMMENT_REFERENCE_STYLE)) {
195
- add(wmlFactory.createStyle({
196
- type: 'character', styleId: COMMENT_REFERENCE_STYLE,
197
- name: wmlFactory.createStyleName({ val: 'annotation reference' }),
198
- basedOn: wmlFactory.createStyleBasedOn({ val: 'DefaultParagraphFont' }),
199
- uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
200
- semiHidden: wmlFactory.createBooleanDefaultTrue(),
201
- unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
202
- rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 16 }), szCs: wmlFactory.createHpsMeasure({ val: 16 }) }),
203
- }));
204
- }
184
+ for (const style of commentStyles())
185
+ if (!list.some((s) => s.styleId === style.styleId))
186
+ add(style);
205
187
  }
206
188
  setCommentPartsAccess({
207
189
  async load(body) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docx4j/core-ts",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",