@docx4j/core-ts 0.2.2 → 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.
@@ -7,8 +7,8 @@ import { marshalString, deepCopy } from '@docx4j/generated-objects-ts';
7
7
  import { XmlMapping } from '../customxml/XmlMapping.mjs';
8
8
  import { CheckboxContentControl, DatePickerContentControl, ListContentControl, PictureContentControl, RepeatingSectionContentControl, GroupContentControl, checkboxRun, CHECKBOX_FONT, } from '../customxml/kinds.mjs';
9
9
  import { runsForValue, updateFromControl, PLACEHOLDER_TEXT } from '../customxml/bindings.mjs';
10
- import { sdtProperty, sdtKindOf } from '@docx4j/generated-objects-ts/builders/wml';
11
- import { typeNameOf, childrenOf, textOf, runItemsOf, segmentsOf, linkParents, SDT_TYPES } from './tree.mjs';
10
+ import { sdtProperty, sdtKindOf, t as textItem } from '@docx4j/generated-objects-ts/builders/wml';
11
+ import { typeNameOf, childrenOf, textOf, runItemsOf, segmentsOf, linkParents, runOf, SDT_TYPES } from './tree.mjs';
12
12
  import { Range } from './Range.mjs';
13
13
  import { Table } from './Table.mjs';
14
14
  /** The namespaces of the Word 2010 and 2012 content-control properties. */
@@ -128,6 +128,8 @@ export class ContentControl {
128
128
  if (!span)
129
129
  throw new Docx4JException('This content control is not in a paragraph');
130
130
  const { paragraph, start, end } = span;
131
+ if (span.empty)
132
+ return this.insertIntoEmpty(paragraph, text);
131
133
  if (location === 'Start')
132
134
  return paragraph.splice(start, start, text);
133
135
  if (location === 'End')
@@ -251,7 +253,8 @@ export class ContentControl {
251
253
  };
252
254
  let start;
253
255
  let end = 0;
254
- for (const seg of segmentsOf(paragraph.p)) {
256
+ const segs = segmentsOf(paragraph.p);
257
+ for (const seg of segs) {
255
258
  if (!inside(seg.run))
256
259
  continue;
257
260
  if (start === undefined)
@@ -259,11 +262,54 @@ export class ContentControl {
259
262
  end = seg.end;
260
263
  }
261
264
  if (start === undefined) {
262
- // an empty control: its position is where its runs would be
263
- return { paragraph, start: 0, end: 0 };
265
+ // a control holding no text: its position is the end of the last text before it in the
266
+ // paragraph (before, it was reported at 0, and text put into it went to the paragraph's start)
267
+ let pos = 0;
268
+ let found = false;
269
+ const visit = (items) => {
270
+ for (const el of items !== null && items !== void 0 ? items : []) {
271
+ if (found)
272
+ return;
273
+ if (el === this.element) {
274
+ found = true;
275
+ return;
276
+ }
277
+ const v = el.value;
278
+ if (typeof v !== 'object' || v === null)
279
+ continue;
280
+ if (typeNameOf(el) === 'org_docx4j_wml.R') {
281
+ const last = segs.filter((seg) => seg.run === v).pop();
282
+ if (last)
283
+ pos = last.end;
284
+ }
285
+ else
286
+ visit(runItemsOf(v));
287
+ }
288
+ };
289
+ visit(paragraph.p.content);
290
+ return { paragraph, start: pos, end: pos, empty: true };
264
291
  }
265
292
  return { paragraph, start, end };
266
293
  }
294
+ /**
295
+ * Text into a run control that holds no run: a new run in its content, in a `w:ins` under
296
+ * tracking (Word: check 30, new-control-typed), placed before the control's trailing insertion
297
+ * marker when it is itself an insertion.
298
+ */
299
+ insertIntoEmpty(paragraph, text) {
300
+ var _a, _b;
301
+ var _c;
302
+ const sdt = this.sdt;
303
+ const items = ((_b = (_c = ((_a = sdt.sdtContent) !== null && _a !== void 0 ? _a : (sdt.sdtContent = {}))).content) !== null && _b !== void 0 ? _b : (_c.content = []));
304
+ const run = runOf([textItem(text)]);
305
+ const tracker = paragraph.changeTracker;
306
+ const item = tracker ? tracker.ins([run]) : run;
307
+ const at = items.findIndex((el) => { var _a; return ((_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'customXmlInsRangeStart'; });
308
+ items.splice(at < 0 ? items.length : at, 0, item);
309
+ linkParents(item, sdt.sdtContent);
310
+ const span = this.runSpan();
311
+ return new Range(paragraph, span.start, span.end);
312
+ }
267
313
  // --- CR-002 phase E: the XML mapping, the typed kinds and the w:sdtPr properties ---
268
314
  /** The control's data binding (Office JS `xmlMapping`), backed by `w:dataBinding`. */
269
315
  get xmlMapping() {
@@ -49,6 +49,8 @@ export class Font {
49
49
  if (tracking && !tracking.ownInsertions.has(h))
50
50
  tracking.tracker.recordRPrChange(h.rPr);
51
51
  applyRunOptions(h.rPr, opts);
52
+ if (tracking)
53
+ tracking.tracker.settleRPrChange(h);
52
54
  }
53
55
  }
54
56
  get bold() { return this.read().bold; }
@@ -511,6 +511,7 @@ export class ListItem {
511
511
  }
512
512
  numPr.ilvl = f.createPPrBaseNumPrIlvl({ val: value });
513
513
  linkParents(numPr.ilvl, numPr);
514
+ this.paragraph.settleFormatting();
514
515
  }
515
516
  /**
516
517
  * Office JS `listItem.listString`: the label Word paints in front of the paragraph.
@@ -628,6 +629,7 @@ export function attachToList(paragraph, listId, level = 0) {
628
629
  numPr.numId = f.createPPrBaseNumPrNumId({ val: Number(listId) });
629
630
  numPr.ilvl = f.createPPrBaseNumPrIlvl({ val: level });
630
631
  linkParents(numPr, paragraph.p.pPr);
632
+ paragraph.settleFormatting();
631
633
  return new List(String(listId), paragraph.parentBody);
632
634
  }
633
635
  /**
@@ -640,12 +642,13 @@ export function detachFromList(paragraph) {
640
642
  if (((_a = paragraph.p.pPr) === null || _a === void 0 ? void 0 : _a.numPr) !== undefined) {
641
643
  delete paragraph.pPr().numPr;
642
644
  }
643
- if (!isListItem(paragraph))
644
- return;
645
- const numPr = paragraph.numPr();
646
- numPr.numId = f.createPPrBaseNumPrNumId({ val: 0 });
647
- linkParents(numPr.numId, numPr);
648
- delete numPr.ilvl;
645
+ if (isListItem(paragraph)) {
646
+ const numPr = paragraph.numPr();
647
+ numPr.numId = f.createPPrBaseNumPrNumId({ val: 0 });
648
+ linkParents(numPr.numId, numPr);
649
+ delete numPr.ilvl;
650
+ }
651
+ paragraph.settleFormatting();
649
652
  }
650
653
  /**
651
654
  * Office JS `paragraph.startNewList()`: a `w:abstractNum` copied from docx4j's default
@@ -9,7 +9,7 @@ import { ContentControl, type ContentControlType } from './ContentControl.mjs';
9
9
  import { InlinePicture, type InlinePictureOptions } from './InlinePicture.mjs';
10
10
  import { type CommentContent, type CommentOptions } from './comments.mjs';
11
11
  import type { Comment } from './Comment.mjs';
12
- import { type ChangeTracker } from './tracking.mjs';
12
+ import { ChangeTracker } from './tracking.mjs';
13
13
  import { TrackedChange } from './TrackedChange.mjs';
14
14
  import { type List, type ListItem, type StartListOptions } from './List.mjs';
15
15
  /**
@@ -106,6 +106,15 @@ export declare class Paragraph {
106
106
  /** The effective alignment; an absent `w:jc` after resolution is `'Left'`, as Office JS. */
107
107
  get alignment(): Alignment;
108
108
  set alignment(v: AlignmentOrUnknown);
109
+ /**
110
+ * The alignment the paragraph resolves to with no `w:jc` of its own: its style's, else the
111
+ * defaults'. Word's Ctrl+L on a centred paragraph whose style is left-aligned removes the `w:jc`
112
+ * rather than writing `w:jc="left"`, with tracking on (check 26) and off (the editor's
113
+ * `measure-word2010.docx` and `measure-word15.docx`, 2026-09-30; CR-002 section 38), so a value the
114
+ * paragraph already inherits is not written. Measured for left over a left-aligned style; taken to
115
+ * hold for any value. Undefined without a resolver, where the value is written as asked.
116
+ */
117
+ private inheritedAlignment;
109
118
  /** Indents and spacing in points, as Office JS; the effective values (CR-001 Phase B step 2). */
110
119
  get leftIndent(): number;
111
120
  set leftIndent(pt: number);
@@ -186,6 +195,17 @@ export declare class Paragraph {
186
195
  */
187
196
  delete(): void;
188
197
  private trackedDelete;
198
+ /**
199
+ * A paragraph mark deleted under tracking gives the next paragraph this paragraph's properties,
200
+ * as a `w:pPrChange` on it recording its own: Word, at Delete at the end of a Heading 2 with a
201
+ * Normal paragraph after it, wrote the next paragraph `w:pStyle Heading2` with
202
+ * `<w:pPrChange><w:pPr/></w:pPrChange>`, and No Markup reads the joined line as a heading
203
+ * throughout (the editor's `measure-word15.docx`, 2026-09-30; CR-002 section 38). The Delete
204
+ * counterpart of check 29's Enter. Nothing where the two paragraphs' properties are the same
205
+ * (check 22's deleted marks). Accepting the mark then joins into a paragraph with these
206
+ * properties, and rejecting puts the next paragraph's back.
207
+ */
208
+ private giveMarkPropertiesToNext;
189
209
  /**
190
210
  * The paragraph's content and its mark marked deleted, the paragraph element itself kept:
191
211
  * what the cells of a deleted table row need (a `w:tc` must keep a block-level child, and Word
@@ -206,6 +226,20 @@ export declare class Paragraph {
206
226
  * @internal public so that the list views (CR-002 phase H) write `w:numPr` the same way.
207
227
  */
208
228
  pPr(): wml.PPr;
229
+ /**
230
+ * After a tracked write of the paragraph's properties: when the paragraph is as it was, its
231
+ * recorded properties go back as they were and the `w:pPrChange` goes, since Word keeps no
232
+ * formatting change that changes nothing, whoever changed it back (`test/README.md` check 26:
233
+ * centred then left again, by the same author and by another; Increase Indent then Decrease
234
+ * Indent; each left no `w:pPr`; CR-002 section 29). "As it was" is Office JS's paragraph
235
+ * formatting (alignment, indents, spacing, outline level) read resolved from both, the rest of the
236
+ * properties - the style, the list - written the same: Word's Ctrl+L and Decrease Indent removed
237
+ * the direct values where this package's setters write `w:jc="left"` and `w:ind w:left="0"`.
238
+ * Without the package's `PropertyResolver` only properties written the same count.
239
+ *
240
+ * @internal public so that the list views (CR-002 phase H) settle their `w:numPr` writes.
241
+ */
242
+ settleFormatting(): void;
209
243
  /**
210
244
  * The paragraph's `w:numPr`, created when absent, through {@link pPr} so that a tracked
211
245
  * write records `w:pPrChange` first.
@@ -227,7 +261,7 @@ export declare class Paragraph {
227
261
  * is removed, a run left empty is removed. Text is inserted into the w:t at `start`,
228
262
  * else into a new run with the formatting of the nearest run.
229
263
  */
230
- splice(start: number, end: number, text: string): Range;
264
+ splice(start: number, end: number, text: string, prefer?: 'before' | 'after'): Range;
231
265
  /**
232
266
  * Inserts run-level items (runs, hyperlinks, content controls) at a text offset, splitting the
233
267
  * run there when the offset falls inside one. PARENT is linked on what is inserted.
@@ -286,12 +320,47 @@ export declare class Paragraph {
286
320
  * Returns the `w:del` elements it made, in document order.
287
321
  */
288
322
  private deleteText;
323
+ /**
324
+ * Splits the revision that holds the text at `at` into two - the second half a copy of its
325
+ * attributes under a new `w:id` - and puts `item` between them. Word splits a revision so when
326
+ * text is typed inside it: around the typist's `w:ins` with tracking on, around plain text with it
327
+ * off (`test/README.md` checks 20 and 21). False, with nothing changed but a run split at `at`,
328
+ * where the text there is not directly in the revision (in a hyperlink inside it, say): the caller
329
+ * then does what it did before.
330
+ */
331
+ private insertBetweenHalves;
332
+ /**
333
+ * Moves a revision's items from `index` on into a copy of it - its attributes, a new `w:id` - and
334
+ * puts `item` between the two; with nothing from `index` on, `item` just goes after the revision.
335
+ * False, with nothing changed, where the revision's items cannot be told apart from its attributes.
336
+ * Internal: `Range.insertContentControl` uses it to put a new control between the halves of the
337
+ * insertion it wraps part of (check 30).
338
+ */
339
+ splitHolderAround(holder: RevisionHolderOf, index: number, item: Element | Element[], nextId: () => number): boolean;
340
+ /**
341
+ * Puts `item` beside a revision rather than in it: after it, or before it - and then before the
342
+ * range start of a move whose range opens just there, so that text typed at the start of moved
343
+ * text is not inside the move (`test/README.md` check 20 case 03).
344
+ */
345
+ private insertBesideRevision;
346
+ /**
347
+ * A fresh annotation id for markup this paragraph writes itself: the package's counter when there
348
+ * is a package that keeps one, since a document has one id space; else one above every id in the
349
+ * story.
350
+ */
351
+ /** @internal `Comment` uses it to split an insertion at a comment's end. */
352
+ nextAnnotationId(): number;
289
353
  /**
290
354
  * Text inserted at `at` as a `w:ins`. A run of this author's own that is already inside a
291
355
  * `w:ins` is extended rather than nested in another one, as Word does. `anchor` is the `w:del`
292
356
  * of a replacement, which the insertion must follow.
293
357
  */
294
358
  private insertTracked;
359
+ /**
360
+ * An insertion put at `at`, `before` and `after` being the segments either side of it once the runs
361
+ * are split there: never inside another's revision.
362
+ */
363
+ private placeInsertion;
295
364
  /**
296
365
  * Splits runs so that every run holding text in [start, end) holds nothing else, and returns
297
366
  * the segments inside the span. `splitAt` does the `w:t` boundaries; this also moves the items
@@ -303,4 +372,5 @@ export declare class Paragraph {
303
372
  /** Moves the items after `seg` into a run of their own, behind `seg`'s run. */
304
373
  private splitRunAfter;
305
374
  }
375
+ type RevisionHolderOf = NonNullable<TextSegment['revision']>;
306
376
  export { W_NS };