@docx4j/core-ts 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +8 -3
  2. package/dist/html/convert.d.mts +116 -0
  3. package/dist/html/convert.mjs +565 -0
  4. package/dist/html/elements.d.mts +19 -0
  5. package/dist/html/elements.mjs +182 -0
  6. package/dist/html/index.d.mts +2 -0
  7. package/dist/html/index.mjs +4 -0
  8. package/dist/model/content/Body.d.mts +13 -1
  9. package/dist/model/content/Body.mjs +97 -19
  10. package/dist/model/content/Comment.mjs +7 -0
  11. package/dist/model/content/InlinePicture.d.mts +89 -10
  12. package/dist/model/content/InlinePicture.mjs +245 -35
  13. package/dist/model/content/List.mjs +10 -1
  14. package/dist/model/content/Paragraph.d.mts +28 -5
  15. package/dist/model/content/Paragraph.mjs +77 -37
  16. package/dist/model/content/Range.d.mts +18 -2
  17. package/dist/model/content/Range.mjs +194 -64
  18. package/dist/model/content/Table.d.mts +38 -0
  19. package/dist/model/content/Table.mjs +80 -2
  20. package/dist/model/content/TrackedChange.d.mts +129 -3
  21. package/dist/model/content/TrackedChange.mjs +577 -12
  22. package/dist/model/content/binder.d.mts +2 -1
  23. package/dist/model/content/binder.mjs +48 -1
  24. package/dist/model/content/index.d.mts +4 -3
  25. package/dist/model/content/index.mjs +3 -2
  26. package/dist/model/content/relationships.d.mts +30 -0
  27. package/dist/model/content/relationships.mjs +133 -0
  28. package/dist/model/content/stylesFacade.d.mts +4 -2
  29. package/dist/model/content/stylesFacade.mjs +20 -6
  30. package/dist/model/content/tableColumns.d.mts +48 -0
  31. package/dist/model/content/tableColumns.mjs +484 -0
  32. package/dist/model/content/tree.d.mts +52 -2
  33. package/dist/model/content/tree.mjs +48 -3
  34. package/dist/model/customxml/CustomXmlPartCollection.d.mts +6 -1
  35. package/dist/model/customxml/CustomXmlPartCollection.mjs +19 -3
  36. package/dist/model/customxml/bindings.d.mts +5 -2
  37. package/dist/model/customxml/bindings.mjs +21 -6
  38. package/dist/model/customxml/index.d.mts +3 -0
  39. package/dist/model/customxml/index.mjs +3 -0
  40. package/dist/model/customxml/opendope.d.mts +32 -0
  41. package/dist/model/customxml/opendope.mjs +56 -0
  42. package/dist/model/customxml/pictures.d.mts +57 -0
  43. package/dist/model/customxml/pictures.mjs +192 -0
  44. package/dist/model/customxml/xhtml.d.mts +42 -0
  45. package/dist/model/customxml/xhtml.mjs +162 -0
  46. package/dist/model/fonts/families.generated.mjs +2 -2
  47. package/dist/model/fonts/registry.mjs +7 -1
  48. package/dist/model/fonts/substitutions.generated.mjs +2 -2
  49. package/dist/model/fonts/themes.generated.mjs +1 -1
  50. package/dist/model/listnumbering/definitions.mjs +18 -0
  51. package/dist/model/properties/PropertyResolver.d.mts +27 -6
  52. package/dist/model/properties/PropertyResolver.mjs +83 -16
  53. package/dist/model/properties/catalogue.d.mts +15 -1
  54. package/dist/model/properties/catalogue.mjs +80 -27
  55. package/dist/model/properties/table.d.mts +16 -2
  56. package/dist/model/properties/table.mjs +13 -3
  57. package/dist/opc/Load.d.mts +9 -2
  58. package/dist/opc/index.d.mts +2 -2
  59. package/dist/opc/index.mjs +2 -2
  60. package/dist/opc/mce/preprocessor.d.mts +62 -6
  61. package/dist/opc/mce/preprocessor.mjs +142 -35
  62. package/dist/opc/mce/understood.d.mts +10 -0
  63. package/dist/opc/mce/understood.mjs +25 -3
  64. package/dist/packages/OpcPackage.d.mts +7 -0
  65. package/dist/packages/OpcPackage.mjs +15 -3
  66. package/dist/packages/WordprocessingMLPackage.d.mts +35 -2
  67. package/dist/packages/WordprocessingMLPackage.mjs +99 -2
  68. package/dist/parts/XmlPart.d.mts +26 -5
  69. package/dist/parts/XmlPart.mjs +67 -18
  70. package/dist/parts/wml/knownStyles.d.mts +1 -0
  71. package/dist/parts/wml/knownStyles.mjs +2233 -0
  72. package/package.json +7 -2
@@ -1,6 +1,6 @@
1
1
  import { Font } from './Font.mjs';
2
2
  import { runFontSelectorOf } from '../fonts/lookup.mjs';
3
- import { typeNameOf, linkParents, runItemsOf, runsOf } from './tree.mjs';
3
+ import { typeNameOf, linkParents, runItemsOf, runsOf, revisionKindOf, WHOLE } from './tree.mjs';
4
4
  import { Docx4JException } from '../../opc/exceptions.mjs';
5
5
  import { ContentControl } from './ContentControl.mjs';
6
6
  import { sdt as sdtOf, nextSdtId, applyRunOptions, t as textElement } from '@docx4j/generated-objects-ts/builders/wml';
@@ -15,12 +15,23 @@ import { commentApi } from './comments.mjs';
15
15
  * A subset of Office JS `Word.Range`: a span of text within one paragraph, [start, end) in the
16
16
  * paragraph's text. Positions are recomputed from the tree on every use, so a range stays
17
17
  * valid across edits made through it; edits made elsewhere may shift it.
18
+ *
19
+ * A picture has a place in the text and no text of its own, so a picture at the range's start or
20
+ * end offset may be inside it or outside it. `whole` says: a range from `Paragraph.getRange('Whole')`
21
+ * or `'Content'` holds the paragraph's whole content, the pictures at its edges too, and deleting or
22
+ * replacing it deletes them; any other range holds the pictures strictly inside it, so that a search
23
+ * hit replaced does not take the picture beside it (CR-002 section 48).
18
24
  */
19
25
  export class Range {
20
- constructor(paragraph, start, end) {
26
+ constructor(paragraph, start, end, whole = false) {
21
27
  this.paragraph = paragraph;
22
28
  this.start = start;
23
29
  this.end = end;
30
+ this.whole = whole;
31
+ }
32
+ /** The edges `Paragraph.splice` is given: both for a whole range. */
33
+ get edges() {
34
+ return this.whole ? WHOLE : undefined;
24
35
  }
25
36
  get paragraphs() {
26
37
  return [this.paragraph];
@@ -93,7 +104,7 @@ export class Range {
93
104
  insertText(text, location) {
94
105
  let r;
95
106
  if (location === 'Replace')
96
- r = this.paragraph.splice(this.start, this.end, text);
107
+ r = this.paragraph.splice(this.start, this.end, text, 'before', this.edges);
97
108
  // text put before the range joins the run the range starts in - inside the link the range is on,
98
109
  // as Office JS (check 32: insertText('Before') on a link's text went into the link, as 'After' did)
99
110
  else if (location === 'Before' || location === 'Start')
@@ -145,9 +156,9 @@ export class Range {
145
156
  : { element: el, container: this.paragraph.container };
146
157
  });
147
158
  }
148
- /** Removes the span's text. */
159
+ /** Removes the span's text, and the pictures in it (CR-002 section 48). */
149
160
  delete() {
150
- this.paragraph.splice(this.start, this.end, '');
161
+ this.paragraph.splice(this.start, this.end, '', 'before', this.edges);
151
162
  this.end = this.start;
152
163
  }
153
164
  search(text, options) {
@@ -183,7 +194,7 @@ export class Range {
183
194
  return new Range(this.paragraph, this.start, this.start);
184
195
  if (location === 'End')
185
196
  return new Range(this.paragraph, this.end, this.end);
186
- return new Range(this.paragraph, this.start, this.end);
197
+ return new Range(this.paragraph, this.start, this.end, this.whole);
187
198
  }
188
199
  /** The span's text as it stands (extension; Office JS getOoxml wraps it in a package). */
189
200
  toString() {
@@ -209,7 +220,7 @@ export class Range {
209
220
  * span gets an empty control at its position. Returns the control.
210
221
  */
211
222
  insertContentControl(kind) {
212
- var _a, _b;
223
+ var _a;
213
224
  const paragraph = this.paragraph;
214
225
  const body = paragraph.parentBody;
215
226
  // The control is built empty first: `sdt` refuses a kind that cannot be run-level (a repeating
@@ -220,62 +231,44 @@ export class Range {
220
231
  paragraph.insertItemsAt(this.start, [sdt]);
221
232
  return new ContentControl(sdt, containerOf(sdt, paragraph), body);
222
233
  }
223
- paragraph.splitAt(this.start);
224
- paragraph.splitAt(this.end);
225
- const segments = paragraph.segments().filter((s) => s.start >= this.start && s.end <= this.end);
226
- if (segments.length === 0)
234
+ // Every refusal before anything changes (CR-002 section 43): until then the runs were split first.
235
+ const covered = coveredSegments(paragraph, this.start, this.end);
236
+ if (covered.length === 0)
227
237
  throw new Docx4JException('This range covers no run');
228
- const owner = segments[0].runOwner;
229
- if (segments.some((s) => s.runOwner !== owner)) {
238
+ const coveredOwner = covered[0].runOwner;
239
+ if (covered.some((s) => s.runOwner !== coveredOwner)) {
230
240
  throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
231
241
  }
232
- const items = [];
233
- for (const segment of segments) {
234
- const element = owner[segment.runIndex];
235
- if (!items.includes(element))
236
- items.push(element);
237
- }
238
- const at = owner.indexOf(items[0]);
242
+ refuseTrackedDeletion(coveredOwner, covered);
239
243
  const tracker = paragraph.changeTracker;
240
- const revision = segments[0].revision;
244
+ const coveredRevision = revisionHolding(covered[0], coveredOwner);
245
+ if (tracker && coveredRevision && itemsKeyOf(coveredRevision) === undefined) {
246
+ throw new Docx4JException('This range is in a revision that cannot be split around a content control');
247
+ }
248
+ paragraph.splitAt(this.start);
249
+ paragraph.splitAt(this.end);
250
+ const segments = paragraph.segments().filter((s) => s.start >= this.start && s.end <= this.end);
251
+ const owner = segments[0].runOwner;
252
+ const { at, items } = wrappedItems(owner, segments);
253
+ const revision = revisionHolding(segments[0], owner);
241
254
  if (tracker && revision) {
242
255
  // The runs are in a revision: the control goes between its halves, not in it, and the runs go
243
256
  // into the control in a revision of the same kind and attributes with a new id - Word's form for
244
257
  // inserted text wrapped in a new control (check 30, inserted-text-wrapped: the w:ins inside the
245
258
  // control, the control's own markers outside it).
246
- const index = revision.items.indexOf(items[0]);
247
- revision.items.splice(index, items.length);
248
- const value = revision.value;
249
- const itemsKey = Object.keys(value).find((key) => value[key] === revision.items);
250
- const inner = {};
251
- for (const [key, v] of Object.entries(value))
252
- if (key !== itemsKey && key !== 'PARENT')
253
- inner[key] = v;
254
- inner.id = tracker.nextId();
255
- inner[itemsKey] = items;
256
- const wrapped = { name: revision.element.name, value: inner };
257
- linkParents(items, inner);
259
+ revision.items.splice(at, items.length);
260
+ const wrapped = revisionAround(revision, items, tracker.nextId());
258
261
  sdt.value.sdtContent.content = [wrapped];
259
262
  linkParents(wrapped, sdt.value.sdtContent);
260
- const holderOwner = revision.owner;
261
- const parent = (_a = revision.value.PARENT) !== null && _a !== void 0 ? _a : paragraph.p;
262
- if (revision.items.length === 0) {
263
- holderOwner.splice(holderOwner.indexOf(revision.element), 1, sdt); // the whole revision wrapped
264
- linkParents(sdt, parent);
265
- }
266
- else if (index === 0) {
267
- holderOwner.splice(holderOwner.indexOf(revision.element), 0, sdt); // its start wrapped
268
- linkParents(sdt, parent);
269
- }
270
- else if (!paragraph.splitHolderAround(revision, index, sdt, () => tracker.nextId())) {
263
+ if (!placeInRevision(paragraph, revision, at, sdt, () => tracker.nextId())) {
271
264
  throw new Docx4JException('This range is in a revision that cannot be split around a content control');
272
265
  }
273
- markHolderInserted(tracker, sdt, holderOwner, () => { });
274
- return new ContentControl(sdt, holderOwner, body);
266
+ markHolderInserted(tracker, sdt, revision.owner, () => { });
267
+ return new ContentControl(sdt, revision.owner, body);
275
268
  }
276
269
  sdt.value.sdtContent.content = items;
277
270
  owner.splice(at, items.length, sdt);
278
- linkParents(sdt, (_b = segments[0].run.PARENT) !== null && _b !== void 0 ? _b : paragraph.p);
271
+ linkParents(sdt, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
279
272
  // under tracking the control itself is the insertion, the runs in it what they were (check 30)
280
273
  if (tracker)
281
274
  markHolderInserted(tracker, sdt, owner, () => { });
@@ -340,12 +333,18 @@ export class Range {
340
333
  start = Math.min(start, span[0]);
341
334
  end = Math.max(end, span[1]);
342
335
  }
343
- this.removeHyperlinks();
344
- if (value === '')
345
- return;
346
336
  const hash = value.indexOf('#');
347
337
  const address = hash === -1 ? value : value.slice(0, hash);
348
338
  const location = hash === -1 ? '' : value.slice(hash + 1);
339
+ // Every refusal comes before anything changes, judged on what the range would hold once the links
340
+ // it touches were unwrapped (CR-002 section 43). Until then the setter unwrapped them, added the
341
+ // relationship and split the runs before it refused, so a refused call left the links gone, an
342
+ // orphan relationship and split runs (found by the docx4j-python session).
343
+ if (value !== '')
344
+ this.refuseUnlinkable(start, end, address);
345
+ this.removeHyperlinks();
346
+ if (value === '')
347
+ return;
349
348
  // w:history="1" is what Word writes on every hyperlink it makes, and what docx4j writes when
350
349
  // it builds one (MainDocumentPart.addHyperlink): "add the target to the viewed hyperlinks"
351
350
  // (ECMA-376 17.16.22). Readers do not depend on it, but a document holding one holder from
@@ -359,26 +358,79 @@ export class Range {
359
358
  paragraph.splitAt(start);
360
359
  paragraph.splitAt(end);
361
360
  const segments = paragraph.segments().filter((seg) => seg.start >= start && seg.end <= end);
362
- if (segments.length === 0)
363
- throw new Docx4JException('This range covers no run');
364
361
  const owner = segments[0].runOwner;
365
- if (segments.some((seg) => seg.runOwner !== owner)) {
366
- throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
362
+ const { at, items } = wrappedItems(owner, segments);
363
+ const revision = revisionHolding(segments[0], owner);
364
+ let runsParent = holder.value;
365
+ if (revision) {
366
+ // A w:hyperlink cannot be in a w:ins or w:moveTo (CT_RunTrackChange holds EG_ContentRunContent
367
+ // only), so over runs in one, tracked or not, the link goes between the revision's halves and the
368
+ // runs into it in a revision of the same kind and attributes with a new id, as a content control
369
+ // over inserted text does (CR-002 section 43; the docx4j-python session's form). Until then the
370
+ // link went into the revision, which marshalled with an xsi:type. Word's own form for a link set
371
+ // on inserted text is a HYPERLINK field in the one w:ins (check 30, link-on-inserted-text), which
372
+ // this setter's getter would not read back.
373
+ revision.items.splice(at, items.length);
374
+ const inner = revisionAround(revision, items, paragraph.nextAnnotationId());
375
+ holder.value.content = [inner];
376
+ linkParents(inner, holder.value);
377
+ if (!placeInRevision(paragraph, revision, at, holder, () => paragraph.nextAnnotationId())) {
378
+ throw new Docx4JException('This range is in a revision that cannot be split around a hyperlink');
379
+ }
380
+ runsParent = inner.value;
367
381
  }
368
- const items = [];
369
- for (const segment of segments) {
370
- const element = owner[segment.runIndex];
371
- if (!items.includes(element))
372
- items.push(element);
382
+ else {
383
+ holder.value.content = items;
384
+ owner.splice(at, items.length, holder);
385
+ linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
373
386
  }
374
- const at = owner.indexOf(items[0]);
375
- holder.value.content = items;
376
- owner.splice(at, items.length, holder);
377
- linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
378
387
  this.restyle(holder.value, true);
379
- mergeTextRuns(holder.value.content, holder.value);
388
+ mergeTextRuns(items, runsParent);
380
389
  (_c = (_b = paragraph.parentBody.package_) === null || _b === void 0 ? void 0 : _b.requireStyles) === null || _c === void 0 ? void 0 : _c.call(_b, [HYPERLINK_STYLE]);
381
390
  }
391
+ /**
392
+ * Refuses, before anything changes, a link the setter could not make over [start, end): one with an
393
+ * address on a range with no part, over no run, over runs in more than one holder, or around a
394
+ * tracked deletion - each judged as the range would stand once the links it touches were unwrapped,
395
+ * their runs then in the list holding them.
396
+ */
397
+ refuseUnlinkable(start, end, address) {
398
+ const paragraph = this.paragraph;
399
+ if (address !== '' && !paragraph.parentBody.part) {
400
+ throw new Docx4JException('This range has no part, so a hyperlink cannot be related to it');
401
+ }
402
+ const touched = hyperlinkHolders(paragraph).filter((h) => this.touches(h));
403
+ const unwrapsInto = new Map();
404
+ for (const holder of touched) {
405
+ const inner = runItemsOf(holder.value);
406
+ if (inner)
407
+ unwrapsInto.set(inner, containerOf(holder, paragraph));
408
+ }
409
+ const ownerOnceUnwrapped = (owner) => {
410
+ let o = owner;
411
+ for (let guard = 0; guard < 100 && unwrapsInto.has(o); guard++)
412
+ o = unwrapsInto.get(o);
413
+ return o;
414
+ };
415
+ const covered = coveredSegments(paragraph, start, end);
416
+ if (covered.length === 0)
417
+ throw new Docx4JException('This range covers no run');
418
+ const owner = ownerOnceUnwrapped(covered[0].runOwner);
419
+ if (covered.some((seg) => ownerOnceUnwrapped(seg.runOwner) !== owner)) {
420
+ throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
421
+ }
422
+ const unwrapped = new Set(touched);
423
+ const flatten = (items) => items.flatMap((item) => {
424
+ var _a;
425
+ return (unwrapped.has(item)
426
+ ? flatten(((_a = runItemsOf(item.value)) !== null && _a !== void 0 ? _a : [])) : [item]);
427
+ });
428
+ refuseTrackedDeletion(flatten(owner), covered);
429
+ const revision = revisionHolding(covered[0], owner);
430
+ if (revision && itemsKeyOf(revision) === undefined) {
431
+ throw new Docx4JException('This range is in a revision that cannot be split around a hyperlink');
432
+ }
433
+ }
382
434
  /** The first `w:hyperlink` this range touches, if any. */
383
435
  hyperlinkHolder() {
384
436
  return hyperlinkHolders(this.paragraph).find((holder) => this.touches(holder));
@@ -541,6 +593,84 @@ function hyperlinkHolders(paragraph) {
541
593
  return out;
542
594
  }
543
595
  /** The run-level array holding an element, after `insertItemsAt` put it somewhere in the paragraph. */
596
+ /**
597
+ * The segments a range covers, read before its runs are split at its edges: those inside it, and those
598
+ * a split would cut, whose inner part then is. What `segments()` gives inside the range once it is
599
+ * split, so that a wrapper's refusals can come before anything changes.
600
+ */
601
+ function coveredSegments(paragraph, start, end) {
602
+ return paragraph.segments().filter((s) => (s.start >= start && s.end <= end) || (s.start < end && s.end > start));
603
+ }
604
+ /**
605
+ * The items a range's runs are wrapped in place of: everything from the first run it covers to the
606
+ * last, in the one list holding them, so that what lies between - a bookmark, a comment's range, a
607
+ * proofing mark - goes into the wrapper with them. Until CR-002 section 43 as many items were taken as
608
+ * there were runs, from the first, which dropped what lay between and left a run in two places.
609
+ */
610
+ function wrappedItems(owner, segments) {
611
+ const indexes = segments.map((s) => owner.indexOf(s.runOwner[s.runIndex])).filter((i) => i >= 0);
612
+ const at = Math.min(...indexes);
613
+ return { at, items: owner.slice(at, Math.max(...indexes) + 1) };
614
+ }
615
+ /**
616
+ * Refuses a range with a tracked deletion (or text moved away) between its runs: having no text in
617
+ * the range's, it would be wrapped unseen, and a range spanning a tracked change is refused (CR-002
618
+ * section 43).
619
+ */
620
+ function refuseTrackedDeletion(items, segments) {
621
+ const indexes = segments.map((s) => items.indexOf(s.runOwner[s.runIndex])).filter((i) => i >= 0);
622
+ if (indexes.length === 0)
623
+ return;
624
+ for (const item of items.slice(Math.min(...indexes), Math.max(...indexes) + 1)) {
625
+ const kind = revisionKindOf(item);
626
+ if (kind === 'del' || kind === 'moveFrom') {
627
+ throw new Docx4JException('This range spans a tracked deletion; wrap the text on either side of it');
628
+ }
629
+ }
630
+ }
631
+ /** The `w:ins` or `w:moveTo` a segment's run is directly in, when `owner` is its own item list. */
632
+ function revisionHolding(segment, owner) {
633
+ return segment.revision !== undefined && segment.revision.items === owner ? segment.revision : undefined;
634
+ }
635
+ /** The key under which a revision's value holds its items, or undefined where none does. */
636
+ function itemsKeyOf(revision) {
637
+ const value = revision.value;
638
+ return Object.keys(value).find((key) => value[key] === revision.items);
639
+ }
640
+ /** A revision of the same kind and attributes as `revision`, with a new id, holding `items`. */
641
+ function revisionAround(revision, items, id) {
642
+ const value = revision.value;
643
+ const itemsKey = itemsKeyOf(revision);
644
+ const inner = {};
645
+ for (const [key, v] of Object.entries(value))
646
+ if (key !== itemsKey && key !== 'PARENT')
647
+ inner[key] = v;
648
+ inner.id = id;
649
+ inner[itemsKey] = items;
650
+ linkParents(items, inner);
651
+ return { name: revision.element.name, value: inner };
652
+ }
653
+ /**
654
+ * Puts `item` where items from `index` on were taken out of `revision`: in its place where it holds
655
+ * nothing now, before it where they were its first, else between its halves. False where the revision
656
+ * cannot be split.
657
+ */
658
+ function placeInRevision(paragraph, revision, index, item, nextId) {
659
+ var _a;
660
+ const owner = revision.owner;
661
+ const parent = (_a = revision.value.PARENT) !== null && _a !== void 0 ? _a : paragraph.p;
662
+ if (revision.items.length === 0) {
663
+ owner.splice(owner.indexOf(revision.element), 1, item); // the whole revision wrapped
664
+ linkParents(item, parent);
665
+ return true;
666
+ }
667
+ if (index === 0) {
668
+ owner.splice(owner.indexOf(revision.element), 0, item); // its start wrapped
669
+ linkParents(item, parent);
670
+ return true;
671
+ }
672
+ return paragraph.splitHolderAround(revision, index, item, nextId);
673
+ }
544
674
  function containerOf(element, paragraph) {
545
675
  var _a, _b;
546
676
  var _c;
@@ -4,6 +4,7 @@ import type { Body } from './Body.mjs';
4
4
  import type { Paragraph } from './Paragraph.mjs';
5
5
  import type { Range } from './Range.mjs';
6
6
  import { type ChangeTracker } from './tracking.mjs';
7
+ import { type TableLayoutMode } from './tableColumns.mjs';
7
8
  /**
8
9
  * A subset of Office JS `Word.Table` over a `w:tbl`. Rows are read through row-level content
9
10
  * controls (an OpenDoPE repeat wraps its `w:tr` in a `w:sdt`), so `rows` is what Word shows.
@@ -51,6 +52,30 @@ export declare class Table {
51
52
  * tracks changes the rows stay and take a `w:trPr/w:del` instead (CR-002 phase F).
52
53
  */
53
54
  deleteRows(rowIndex: number, rowCount?: number): void;
55
+ /**
56
+ * Adds columns at the start or the end (Office JS `Table.addColumns`), the new cells taking the
57
+ * properties and width of the column beside them - the one to the right of the boundary when
58
+ * there is one, else the left - in the table's layout mode as Word 365 writes it (CR-002 section
59
+ * 42, checks 39 and 40): fixed copies the width and grows the table past the margin, AutoFit to
60
+ * window rescales the grid and writes the cells' percentages again, AutoFit to contents stays
61
+ * automatic. `values` is by row, then by new column. Returns the new cells per row, an extension
62
+ * (Office JS returns nothing); a row whose skipped columns grew instead has none. Tracked, the
63
+ * table change is recorded as Office JS records one and the new cells' text is an insertion.
64
+ */
65
+ addColumns(location: 'Start' | 'End', columnCount: number, values?: string[][]): TableCell[][];
66
+ /**
67
+ * Removes `columnCount` grid columns from `columnIndex` (one by default; Office JS
68
+ * `Table.deleteColumns`): a cell covering only those columns goes, one spanning further shrinks.
69
+ * Tracked, the cells still go and the table change is recorded, as Office JS's deletion leaves
70
+ * it (section 42.3; check 40c; Word's own is not tracked at all, known issues entry 2).
71
+ */
72
+ deleteColumns(columnIndex: number, columnCount?: number): void;
73
+ /** Which of Word's three layout modes the table is in: fixed, AutoFit to window, or AutoFit to contents (extension). */
74
+ get layoutMode(): TableLayoutMode;
75
+ /** The section's text width in twips, for the window mode and a percentage width. */
76
+ textWidth(): number;
77
+ /** Views of new cells, row by row. */
78
+ cellViewsOf(cells: Element<wml.Tc>[][]): TableCell[][];
54
79
  /** Removes the table from its container; tracked, every row is marked deleted instead. */
55
80
  delete(): void;
56
81
  /** The package's change tracker while `changeTrackingMode` is on (CR-002 phase F). */
@@ -118,7 +143,20 @@ export declare class TableCell {
118
143
  insertText(text: string, location: 'Start' | 'End' | 'Replace'): Range;
119
144
  /** The cell width in points (w:tcW in twips), as Office JS reports it; 0 when it is not a fixed width. */
120
145
  get width(): number;
146
+ /**
147
+ * Written in the form the cell states, else its table, else twips (CR-002 section 42.2 item 4): a
148
+ * cell in percent takes the points as a share of the section's text width; `auto` becomes twips,
149
+ * a stated width being what was asked for.
150
+ */
121
151
  set width(points: number);
152
+ /**
153
+ * New columns before or after this cell (Office JS `TableCell.insertColumns`), the new cells
154
+ * taking this cell's properties and width, as `Table.addColumns` does. Returns the new cells per
155
+ * row (extension).
156
+ */
157
+ insertColumns(location: 'Before' | 'After', columnCount: number, values?: string[][]): TableCell[][];
158
+ /** Removes the grid columns this cell covers (Office JS `TableCell.deleteColumn`); tracked as `Table.deleteColumns` is. */
159
+ deleteColumn(): void;
122
160
  /** Office JS TableCell.columnWidth, the same value. */
123
161
  get columnWidth(): number;
124
162
  set columnWidth(points: number);
@@ -4,6 +4,7 @@ import { Docx4JException } from '../../opc/exceptions.mjs';
4
4
  import { typeNameOf, childrenOf, linkParents, textOf, rowsOf, cellsOf } from './tree.mjs';
5
5
  import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
6
6
  import { trackInsertedBlocks } from './tracking.mjs';
7
+ import { insertGridColumns, deleteGridColumns, gridRangeOf, textWidthTwips, widthTypeOf, layoutModeOf } from './tableColumns.mjs';
7
8
  /** The default table width in twips (A4 with 2.54 cm margins), as the objects package's `tbl` builder uses. */
8
9
  const DEFAULT_WIDTH = 9026;
9
10
  const TWIPS_PER_POINT = 20;
@@ -128,6 +129,51 @@ export class Table {
128
129
  row.container.splice(i, 1);
129
130
  }
130
131
  }
132
+ /**
133
+ * Adds columns at the start or the end (Office JS `Table.addColumns`), the new cells taking the
134
+ * properties and width of the column beside them - the one to the right of the boundary when
135
+ * there is one, else the left - in the table's layout mode as Word 365 writes it (CR-002 section
136
+ * 42, checks 39 and 40): fixed copies the width and grows the table past the margin, AutoFit to
137
+ * window rescales the grid and writes the cells' percentages again, AutoFit to contents stays
138
+ * automatic. `values` is by row, then by new column. Returns the new cells per row, an extension
139
+ * (Office JS returns nothing); a row whose skipped columns grew instead has none. Tracked, the
140
+ * table change is recorded as Office JS records one and the new cells' text is an insertion.
141
+ */
142
+ addColumns(location, columnCount, values) {
143
+ const at = location === 'Start' ? 0 : this.columnWidths().length;
144
+ const inserted = insertGridColumns(this.tbl, this.textWidth(), at, columnCount, values, this.changeTracker);
145
+ return this.cellViewsOf(inserted.cells);
146
+ }
147
+ /**
148
+ * Removes `columnCount` grid columns from `columnIndex` (one by default; Office JS
149
+ * `Table.deleteColumns`): a cell covering only those columns goes, one spanning further shrinks.
150
+ * Tracked, the cells still go and the table change is recorded, as Office JS's deletion leaves
151
+ * it (section 42.3; check 40c; Word's own is not tracked at all, known issues entry 2).
152
+ */
153
+ deleteColumns(columnIndex, columnCount = 1) {
154
+ var _a, _b, _c;
155
+ deleteGridColumns(this.tbl, this.textWidth(), columnIndex, columnCount, this.changeTracker);
156
+ if (((_c = (_b = (_a = this.tbl.tblGrid) === null || _a === void 0 ? void 0 : _a.gridCol) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) === 0 || rowsOf(this.tbl).every((row) => cellsOf(row.element.value).length === 0))
157
+ this.delete();
158
+ }
159
+ /** Which of Word's three layout modes the table is in: fixed, AutoFit to window, or AutoFit to contents (extension). */
160
+ get layoutMode() {
161
+ return layoutModeOf(this.tbl);
162
+ }
163
+ /** The section's text width in twips, for the window mode and a percentage width. */
164
+ textWidth() {
165
+ return textWidthTwips(this.parentBody.container);
166
+ }
167
+ /** Views of new cells, row by row. */
168
+ cellViewsOf(cells) {
169
+ const rows = this.rows;
170
+ return cells.map((rowCells, i) => {
171
+ const row = rows[i];
172
+ if (!row || rowCells.length === 0)
173
+ return [];
174
+ return row.cells.filter((cell) => rowCells.includes(cell.element));
175
+ });
176
+ }
131
177
  /** Removes the table from its container; tracked, every row is marked deleted instead. */
132
178
  delete() {
133
179
  const tracker = this.changeTracker;
@@ -311,12 +357,44 @@ export class TableCell {
311
357
  const w = (_a = this.tc.tcPr) === null || _a === void 0 ? void 0 : _a.tcW;
312
358
  return (w === null || w === void 0 ? void 0 : w.type) === 'dxa' ? ((_b = w.w) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT : 0;
313
359
  }
360
+ /**
361
+ * Written in the form the cell states, else its table, else twips (CR-002 section 42.2 item 4): a
362
+ * cell in percent takes the points as a share of the section's text width; `auto` becomes twips,
363
+ * a stated width being what was asked for.
364
+ */
314
365
  set width(points) {
315
366
  var _a, _b;
316
367
  var _c, _d;
368
+ const twips = Math.round(points * TWIPS_PER_POINT);
317
369
  const tcW = ((_b = (_d = ((_a = (_c = this.tc).tcPr) !== null && _a !== void 0 ? _a : (_c.tcPr = {}))).tcW) !== null && _b !== void 0 ? _b : (_d.tcW = {}));
318
- tcW.w = Math.round(points * TWIPS_PER_POINT);
319
- tcW.type = 'dxa';
370
+ if (widthTypeOf(this.tc, this.parentTable.tbl) === 'pct') {
371
+ tcW.w = Math.round((twips * 5000) / this.parentTable.textWidth());
372
+ tcW.type = 'pct';
373
+ }
374
+ else {
375
+ tcW.w = twips;
376
+ tcW.type = 'dxa';
377
+ }
378
+ }
379
+ /**
380
+ * New columns before or after this cell (Office JS `TableCell.insertColumns`), the new cells
381
+ * taking this cell's properties and width, as `Table.addColumns` does. Returns the new cells per
382
+ * row (extension).
383
+ */
384
+ insertColumns(location, columnCount, values) {
385
+ const range = gridRangeOf(this.parentRow.tr, this.tc);
386
+ if (!range)
387
+ throw new Docx4JException('This cell is not in its row');
388
+ const table = this.parentTable;
389
+ const inserted = insertGridColumns(table.tbl, table.textWidth(), location === 'Before' ? range.start : range.end, columnCount, values, table.changeTracker);
390
+ return table.cellViewsOf(inserted.cells);
391
+ }
392
+ /** Removes the grid columns this cell covers (Office JS `TableCell.deleteColumn`); tracked as `Table.deleteColumns` is. */
393
+ deleteColumn() {
394
+ const range = gridRangeOf(this.parentRow.tr, this.tc);
395
+ if (!range)
396
+ throw new Docx4JException('This cell is not in its row');
397
+ this.parentTable.deleteColumns(range.start, range.end - range.start);
320
398
  }
321
399
  /** Office JS TableCell.columnWidth, the same value. */
322
400
  get columnWidth() {