@docx4j/core-ts 0.1.3 → 0.1.5

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,5 +1,5 @@
1
1
  import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
- import type { ListDefinition, LevelDefinition } from '../listnumbering/definitions.mjs';
2
+ import type { NumberingDefinitions, ListDefinition, LevelDefinition } from '../listnumbering/definitions.mjs';
3
3
  import type { Emulator } from '../listnumbering/Emulator.mjs';
4
4
  import { Font } from './Font.mjs';
5
5
  import type { Body } from './Body.mjs';
@@ -31,6 +31,38 @@ export interface ListLabel {
31
31
  /** Whether the level's `w:numFmt` is `bullet`. */
32
32
  isBullet: boolean;
33
33
  }
34
+ /**
35
+ * The content API must not import `WordprocessingMLPackage` or `parts/wml` at runtime (that
36
+ * would be a cycle, since `parts/wml` builds the `Body` views), so the numbering part is
37
+ * reached by shape, as `Body.propertyResolver` and `fonts/lookup` are.
38
+ */
39
+ interface NumberingPartLike {
40
+ readonly isUnmarshalled: boolean;
41
+ readonly contents: wml.Numbering;
42
+ readonly definitions: NumberingDefinitions;
43
+ getEmulator(reset?: boolean): Emulator;
44
+ makeLive(): wml.Numbering;
45
+ getContents(): Promise<wml.Numbering>;
46
+ }
47
+ /** As much of an `XmlPart` as the story walk reads. */
48
+ interface StoryPartLike {
49
+ readonly contentType: string;
50
+ readonly isUnmarshalled: boolean;
51
+ readonly contents: unknown;
52
+ }
53
+ /** As much of a `MainDocumentPart` as the list views use. */
54
+ interface MainPartLike {
55
+ numberingDefinitionsPart?: NumberingPartLike;
56
+ readonly headerParts: StoryPartLike[];
57
+ readonly footerParts: StoryPartLike[];
58
+ addTargetPart(part: unknown): unknown;
59
+ }
60
+ /** As much of a `WordprocessingMLPackage` as the list views use. */
61
+ interface ListPackageLike {
62
+ getMainDocumentPart(): MainPartLike;
63
+ refreshPropertyResolver(): void;
64
+ refresh(): Promise<unknown>;
65
+ }
34
66
  /**
35
67
  * The package's numbering emulator, or undefined where the document has no lists (or nothing
36
68
  * has read the numbering part: `await pkg.getBody()` builds the resolver, which reads it).
@@ -251,3 +283,39 @@ export declare function detachFromList(paragraph: Paragraph): void;
251
283
  * be unmarshalled, and a document with no numbering part gets one here (CR-002 section 17).
252
284
  */
253
285
  export declare function startNewList(paragraph: Paragraph, options?: StartListOptions): Promise<List>;
286
+ /**
287
+ * The document's list definitions, as verbs that need no paragraph (CR-002 section 19, at the
288
+ * editor's request): `pkg.numbering`.
289
+ *
290
+ * `Paragraph.startNewList()` and `List.restart()` are Office JS's shapes and stay as they are,
291
+ * but both build a definition first and touch a paragraph only at the end. An editor that keeps
292
+ * its own document model has the numbering part and a `w:numId` to write, and nothing else - and
293
+ * going through a paragraph would mean projecting a tree, running the verb and projecting back,
294
+ * a whole-body round trip for what is otherwise one attribute. So the definition half is here.
295
+ */
296
+ export declare class NumberingFacade {
297
+ private readonly pkg;
298
+ constructor(pkg: ListPackageLike);
299
+ /**
300
+ * A new list definition, and its `w:numId` as a string for a `w:numPr` to name. The numbering
301
+ * part is created with docx4j's default definitions where the document has none, and an
302
+ * existing one is unmarshalled so that what is written to it is saved.
303
+ *
304
+ * Asynchronous for the same reasons `startNewList` is: the default definitions are XML to be
305
+ * unmarshalled, and the part may have to be created. Nothing is attached to anything; the
306
+ * caller writes `w:numPr` itself.
307
+ */
308
+ newList(options?: {
309
+ bullet?: boolean;
310
+ }): Promise<string>;
311
+ /**
312
+ * A `w:num` that numbers as `numId` does but starts again, and its own `w:numId`:
313
+ * `List.restart()` without needing a `Body`. The counters are shared by abstract definition,
314
+ * so restarting means a new `w:num` with a `w:startOverride`, spent on its first use in a
315
+ * story (CR-001 section 15.2).
316
+ *
317
+ * Synchronous, so the numbering part must have been read: `await pkg.getPropertyResolver()`,
318
+ * `newList()` or any list read does it.
319
+ */
320
+ restart(numId: string | number): string;
321
+ }
@@ -398,22 +398,10 @@ export class List {
398
398
  * Extension name (Office JS has no restart verb); `Paragraph.restartList()` is the sugar.
399
399
  */
400
400
  restart() {
401
- var _a, _b, _c;
402
- const numbering = this.live();
403
- const num = this.element;
404
- const start = (_b = (_a = this.level(0)) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 1;
405
- const created = f.createNumberingNum({
406
- numId: nextNumId(numbering),
407
- abstractNumId: f.createNumberingNumAbstractNumId({ val: num.abstractNumId.val }),
408
- lvlOverride: [f.createNumberingNumLvlOverride({
409
- ilvl: 0,
410
- startOverride: f.createNumberingNumLvlOverrideStartOverride({ val: start }),
411
- })],
412
- });
413
- ((_c = numbering.num) !== null && _c !== void 0 ? _c : (numbering.num = [])).push(created);
414
- linkParents(created, numbering);
401
+ var _a, _b;
402
+ const numId = addRestartedNum(this.live(), this.element.abstractNumId.val, (_b = (_a = this.level(0)) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 1);
415
403
  this.written();
416
- return new List(String(created.numId), this.body);
404
+ return new List(String(numId), this.body);
417
405
  }
418
406
  /**
419
407
  * Gives this list a `w:abstractNum` of its own where another `w:num` shares the one it names
@@ -668,12 +656,24 @@ export function detachFromList(paragraph) {
668
656
  * be unmarshalled, and a document with no numbering part gets one here (CR-002 section 17).
669
657
  */
670
658
  export async function startNewList(paragraph, options) {
671
- var _a, _b, _c, _d, _e;
659
+ var _a, _b;
672
660
  const body = paragraph.parentBody;
673
661
  const part = await ensureNumberingPart(body);
662
+ const numId = await addListDefinition(part, (options === null || options === void 0 ? void 0 : options.bullet) === true);
663
+ (_a = packageOf(body)) === null || _a === void 0 ? void 0 : _a.refreshPropertyResolver();
664
+ return attachToList(paragraph, numId, (_b = options === null || options === void 0 ? void 0 : options.level) !== null && _b !== void 0 ? _b : 0);
665
+ }
666
+ /**
667
+ * A `w:abstractNum` copied from docx4j's default definitions (its `numbering.xml` resource:
668
+ * `w:abstractNum` 1 is Word's decimal set and 0 its bullet set) and a `w:num` naming it; the new
669
+ * `w:numId`. The paragraph half of `startNewList` is separate because it is separable: nothing
670
+ * about building a list definition needs one.
671
+ */
672
+ async function addListDefinition(part, bullet) {
673
+ var _a, _b, _c;
674
674
  const numbering = await part.getContents();
675
675
  const defaults = await defaultNumbering();
676
- const template = ((_a = defaults.abstractNum) !== null && _a !== void 0 ? _a : [])[(options === null || options === void 0 ? void 0 : options.bullet) === true ? 0 : 1];
676
+ const template = ((_a = defaults.abstractNum) !== null && _a !== void 0 ? _a : [])[bullet ? 0 : 1];
677
677
  if (template === undefined)
678
678
  throw new Docx4JException('The default numbering definitions are missing an abstractNum');
679
679
  const abstract = deepCopy(template);
@@ -689,8 +689,27 @@ export async function startNewList(paragraph, options) {
689
689
  });
690
690
  ((_c = numbering.num) !== null && _c !== void 0 ? _c : (numbering.num = [])).push(num);
691
691
  linkParents(num, numbering);
692
- (_d = packageOf(body)) === null || _d === void 0 ? void 0 : _d.refreshPropertyResolver();
693
- return attachToList(paragraph, num.numId, (_e = options === null || options === void 0 ? void 0 : options.level) !== null && _e !== void 0 ? _e : 0);
692
+ return num.numId;
693
+ }
694
+ /**
695
+ * A `w:num` naming the same `w:abstractNum` as another, with a `w:startOverride` at level 0; the
696
+ * new `w:numId`. The counters are shared by abstract definition, so this is how Word restarts a
697
+ * list (CR-001 section 15.2), and it is what {@link List.restart} and
698
+ * {@link NumberingFacade.restart} both do.
699
+ */
700
+ function addRestartedNum(numbering, abstractNumId, start) {
701
+ var _a;
702
+ const created = f.createNumberingNum({
703
+ numId: nextNumId(numbering),
704
+ abstractNumId: f.createNumberingNumAbstractNumId({ val: abstractNumId }),
705
+ lvlOverride: [f.createNumberingNumLvlOverride({
706
+ ilvl: 0,
707
+ startOverride: f.createNumberingNumLvlOverrideStartOverride({ val: start }),
708
+ })],
709
+ });
710
+ ((_a = numbering.num) !== null && _a !== void 0 ? _a : (numbering.num = [])).push(created);
711
+ linkParents(created, numbering);
712
+ return created.numId;
694
713
  }
695
714
  /**
696
715
  * The numbering part, created with docx4j's default numbering where the document has none
@@ -703,6 +722,14 @@ async function ensureNumberingPart(body) {
703
722
  if (pkg === undefined || main === undefined) {
704
723
  throw new Docx4JException('This body has no package, so a list cannot be added to it');
705
724
  }
725
+ return ensureNumberingPartOf(pkg, main);
726
+ }
727
+ /**
728
+ * The same, reached from the package rather than from a body, which is what
729
+ * {@link NumberingFacade} uses: creating a list definition needs no paragraph and no body, and
730
+ * making one go through either costs a caller that holds neither (CR-002 section 19).
731
+ */
732
+ async function ensureNumberingPartOf(pkg, main) {
706
733
  const existing = main.numberingDefinitionsPart;
707
734
  if (existing !== undefined) {
708
735
  // the resolver read the part privately; unmarshalling it gives a second tree, so the
@@ -725,6 +752,58 @@ async function ensureNumberingPart(body) {
725
752
  await pkg.refresh();
726
753
  return created;
727
754
  }
755
+ /**
756
+ * The document's list definitions, as verbs that need no paragraph (CR-002 section 19, at the
757
+ * editor's request): `pkg.numbering`.
758
+ *
759
+ * `Paragraph.startNewList()` and `List.restart()` are Office JS's shapes and stay as they are,
760
+ * but both build a definition first and touch a paragraph only at the end. An editor that keeps
761
+ * its own document model has the numbering part and a `w:numId` to write, and nothing else - and
762
+ * going through a paragraph would mean projecting a tree, running the verb and projecting back,
763
+ * a whole-body round trip for what is otherwise one attribute. So the definition half is here.
764
+ */
765
+ export class NumberingFacade {
766
+ constructor(pkg) {
767
+ this.pkg = pkg;
768
+ }
769
+ /**
770
+ * A new list definition, and its `w:numId` as a string for a `w:numPr` to name. The numbering
771
+ * part is created with docx4j's default definitions where the document has none, and an
772
+ * existing one is unmarshalled so that what is written to it is saved.
773
+ *
774
+ * Asynchronous for the same reasons `startNewList` is: the default definitions are XML to be
775
+ * unmarshalled, and the part may have to be created. Nothing is attached to anything; the
776
+ * caller writes `w:numPr` itself.
777
+ */
778
+ async newList(options) {
779
+ const main = this.pkg.getMainDocumentPart();
780
+ const part = await ensureNumberingPartOf(this.pkg, main);
781
+ const numId = await addListDefinition(part, (options === null || options === void 0 ? void 0 : options.bullet) === true);
782
+ this.pkg.refreshPropertyResolver();
783
+ return String(numId);
784
+ }
785
+ /**
786
+ * A `w:num` that numbers as `numId` does but starts again, and its own `w:numId`:
787
+ * `List.restart()` without needing a `Body`. The counters are shared by abstract definition,
788
+ * so restarting means a new `w:num` with a `w:startOverride`, spent on its first use in a
789
+ * story (CR-001 section 15.2).
790
+ *
791
+ * Synchronous, so the numbering part must have been read: `await pkg.getPropertyResolver()`,
792
+ * `newList()` or any list read does it.
793
+ */
794
+ restart(numId) {
795
+ var _a, _b;
796
+ const part = this.pkg.getMainDocumentPart().numberingDefinitionsPart;
797
+ if (part === undefined)
798
+ throw new Docx4JException('This document has no numbering part, so there is no list to restart');
799
+ const definition = part.definitions.list(numId);
800
+ if (definition === undefined)
801
+ throw new ItemNotFound(`No w:num for numId ${numId}`);
802
+ const created = addRestartedNum(part.makeLive(), definition.num.abstractNumId.val, (_b = (_a = definition.level('0')) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 1);
803
+ this.pkg.refreshPropertyResolver();
804
+ return String(created);
805
+ }
806
+ }
728
807
  // ----------------------------------------------------------------------------- the mechanics
729
808
  const TWIPS_PER_POINT = 20;
730
809
  /** Office JS `Word.ListNumbering` as `w:numFmt`. */
@@ -75,4 +75,26 @@ export declare class Range {
75
75
  * span gets an empty control at its position. Returns the control.
76
76
  */
77
77
  insertContentControl(kind?: ContentControlType): ContentControl;
78
+ /**
79
+ * Office JS `Range.hyperlink`: the address of the first hyperlink in this range, or a new
80
+ * hyperlink over it. `address#location` separates the address from an optional location within
81
+ * it, so `"#heading"` is a link inside this document (a `w:anchor`) and
82
+ * `"https://example.com"` an external one (a relationship). Reading gives `""` where the range
83
+ * is in no hyperlink.
84
+ *
85
+ * Setting removes the hyperlinks the range already carries and wraps its runs in one
86
+ * `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
87
+ * setting `""` removes them and wraps nothing. An external address becomes a relationship of
88
+ * this range's part, which is why the part must exist (CR-002 section 20).
89
+ */
90
+ get hyperlink(): string;
91
+ set hyperlink(value: string);
92
+ /** The `w:hyperlink` this range's first run sits in, if any. */
93
+ private hyperlinkHolder;
94
+ /** Unwraps every `w:hyperlink` the range touches, keeping its runs where they are. */
95
+ private removeHyperlinks;
96
+ /** The target of a relationship of this range's part, for the getter. */
97
+ private relationshipTarget;
98
+ /** An external relationship of this range's part for the address, and its id. */
99
+ private addHyperlinkRelationship;
78
100
  }
@@ -4,6 +4,8 @@ import { typeNameOf, linkParents, runItemsOf } 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 } from '@docx4j/generated-objects-ts/builders/wml';
7
+ import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
8
+ import { Namespaces } from '../../parts/Namespaces.mjs';
7
9
  import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
8
10
  import { contentOf } from './ooxml.mjs';
9
11
  import { searchPattern, findAll } from './search.mjs';
@@ -227,6 +229,121 @@ export class Range {
227
229
  linkParents(sdt, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
228
230
  return new ContentControl(sdt, owner, body);
229
231
  }
232
+ /**
233
+ * Office JS `Range.hyperlink`: the address of the first hyperlink in this range, or a new
234
+ * hyperlink over it. `address#location` separates the address from an optional location within
235
+ * it, so `"#heading"` is a link inside this document (a `w:anchor`) and
236
+ * `"https://example.com"` an external one (a relationship). Reading gives `""` where the range
237
+ * is in no hyperlink.
238
+ *
239
+ * Setting removes the hyperlinks the range already carries and wraps its runs in one
240
+ * `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
241
+ * setting `""` removes them and wraps nothing. An external address becomes a relationship of
242
+ * this range's part, which is why the part must exist (CR-002 section 20).
243
+ */
244
+ get hyperlink() {
245
+ var _a;
246
+ const holder = this.hyperlinkHolder();
247
+ if (holder === undefined)
248
+ return '';
249
+ const anchor = (_a = holder.value.anchor) !== null && _a !== void 0 ? _a : '';
250
+ const address = holder.value.id === undefined ? '' : this.relationshipTarget(holder.value.id);
251
+ return anchor === '' ? address : `${address}#${anchor}`;
252
+ }
253
+ set hyperlink(value) {
254
+ var _a;
255
+ const paragraph = this.paragraph;
256
+ this.removeHyperlinks();
257
+ if (value === '')
258
+ return;
259
+ const hash = value.indexOf('#');
260
+ const address = hash === -1 ? value : value.slice(0, hash);
261
+ const location = hash === -1 ? '' : value.slice(hash + 1);
262
+ const holder = hyperlinkOf({ content: [] });
263
+ if (location !== '')
264
+ holder.value.anchor = location;
265
+ if (address !== '')
266
+ holder.value.id = this.addHyperlinkRelationship(address);
267
+ if (this.start === this.end) {
268
+ paragraph.insertItemsAt(this.start, [holder]);
269
+ return;
270
+ }
271
+ paragraph.splitAt(this.start);
272
+ paragraph.splitAt(this.end);
273
+ const segments = paragraph.segments().filter((seg) => seg.start >= this.start && seg.end <= this.end);
274
+ if (segments.length === 0)
275
+ throw new Docx4JException('This range covers no run');
276
+ const owner = segments[0].runOwner;
277
+ if (segments.some((seg) => seg.runOwner !== owner)) {
278
+ throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
279
+ }
280
+ const items = [];
281
+ for (const segment of segments) {
282
+ const element = owner[segment.runIndex];
283
+ if (!items.includes(element))
284
+ items.push(element);
285
+ }
286
+ const at = owner.indexOf(items[0]);
287
+ holder.value.content = items;
288
+ owner.splice(at, items.length, holder);
289
+ linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
290
+ }
291
+ /** The `w:hyperlink` this range's first run sits in, if any. */
292
+ hyperlinkHolder() {
293
+ for (const segment of this.paragraph.segments()) {
294
+ if (segment.end <= this.start && this.start !== this.end)
295
+ continue;
296
+ if (segment.start >= this.end && this.start !== this.end)
297
+ break;
298
+ const parent = segment.run.PARENT;
299
+ const found = hyperlinkHolders(this.paragraph).find((h) => h.value === parent);
300
+ if (found)
301
+ return found;
302
+ }
303
+ return undefined;
304
+ }
305
+ /** Unwraps every `w:hyperlink` the range touches, keeping its runs where they are. */
306
+ removeHyperlinks() {
307
+ var _a, _b, _c;
308
+ for (const holder of hyperlinkHolders(this.paragraph)) {
309
+ const owner = containerOf(holder, this.paragraph);
310
+ const at = owner.indexOf(holder);
311
+ if (at === -1)
312
+ continue;
313
+ owner.splice(at, 1, ...((_a = holder.value.content) !== null && _a !== void 0 ? _a : []));
314
+ for (const item of ((_b = holder.value.content) !== null && _b !== void 0 ? _b : [])) {
315
+ linkParents(item, (_c = holder.PARENT) !== null && _c !== void 0 ? _c : this.paragraph.p);
316
+ }
317
+ }
318
+ }
319
+ /** The target of a relationship of this range's part, for the getter. */
320
+ relationshipTarget(id) {
321
+ var _a, _b, _c;
322
+ const part = this.paragraph.parentBody.part;
323
+ return (_c = (_b = (_a = part === null || part === void 0 ? void 0 : part.relationshipsPart) === null || _a === void 0 ? void 0 : _a.getRelationshipById(id)) === null || _b === void 0 ? void 0 : _b.target) !== null && _c !== void 0 ? _c : '';
324
+ }
325
+ /** An external relationship of this range's part for the address, and its id. */
326
+ addHyperlinkRelationship(address) {
327
+ const part = this.paragraph.parentBody.part;
328
+ if (!part)
329
+ throw new Docx4JException('This range has no part, so a hyperlink cannot be related to it');
330
+ return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
331
+ }
332
+ }
333
+ /** Every `w:hyperlink` of a paragraph, outermost first (they do not nest in practice). */
334
+ function hyperlinkHolders(paragraph) {
335
+ const out = [];
336
+ const visit = (items) => {
337
+ for (const item of items !== null && items !== void 0 ? items : []) {
338
+ if (typeNameOf(item) === 'org_docx4j_wml.P.Hyperlink')
339
+ out.push(item);
340
+ const value = item.value;
341
+ if (typeof value === 'object' && value !== null)
342
+ visit(runItemsOf(value));
343
+ }
344
+ };
345
+ visit(runItemsOf(paragraph.p));
346
+ return out;
230
347
  }
231
348
  /** The run-level array holding an element, after `insertItemsAt` put it somewhere in the paragraph. */
232
349
  function containerOf(element, paragraph) {
@@ -3,7 +3,7 @@ export { Paragraph, type Alignment } from './Paragraph.mjs';
3
3
  export { Range } from './Range.mjs';
4
4
  export { BUILT_IN_STYLES, builtInOf, idOfBuiltIn, displayNameOf, styleNameOf, styleIdOf, type BuiltInStyle } from './styles.mjs';
5
5
  export { Comment } from './Comment.mjs';
6
- export { List, ListItem, ListItemNotFoundError, listsOf, listLabelsOf, numberingEmulatorOf, levelOf, type ListLevelType, type ListNumbering, type ListBullet, type StartListOptions, type ListLabel, } from './List.mjs';
6
+ export { List, ListItem, ListItemNotFoundError, NumberingFacade, listsOf, listLabelsOf, numberingEmulatorOf, levelOf, type ListLevelType, type ListNumbering, type ListBullet, type StartListOptions, type ListLabel, } from './List.mjs';
7
7
  export { type Author, type CommentParts, type CommentPartsAccess, type CommentsExtensible, type CommentMarker, COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
8
8
  export { Font, type UnderlineType, type RPrHolder, type FontTracking } from './Font.mjs';
9
9
  export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
@@ -5,7 +5,7 @@ export { Range } from './Range.mjs';
5
5
  export { BUILT_IN_STYLES, builtInOf, idOfBuiltIn, displayNameOf, styleNameOf, styleIdOf } from './styles.mjs';
6
6
  export { Comment } from './Comment.mjs';
7
7
  // lists (CR-002 phase H)
8
- export { List, ListItem, ListItemNotFoundError, listsOf, listLabelsOf, numberingEmulatorOf, levelOf, } from './List.mjs';
8
+ export { List, ListItem, ListItemNotFoundError, NumberingFacade, listsOf, listLabelsOf, numberingEmulatorOf, levelOf, } from './List.mjs';
9
9
  export { COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
10
10
  export { Font } from './Font.mjs';
11
11
  export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
@@ -3,10 +3,32 @@ import type { BooleanConversionMode, XPathEngine, XPathValue } from './xpath.mjs
3
3
  * XPath 3.1 through FontoXPath, for the `xpath2` boolean conversion mode and for expressions
4
4
  * written in XPath 2.0 syntax. `select`, `selectValue` and the `java` and `xpath1` modes answer as
5
5
  * `DefaultXPathEngine` does; `xpath2` is the mode this engine exists for.
6
+ *
7
+ * **In a browser or any bundled application, pass the module in.** The no-argument form loads
8
+ * `fontoxpath` by a dynamic `import()` of the bare specifier, which Node resolves and a browser
9
+ * does not: there is no import map, and the `@vite-ignore` that keeps a bundler from rewriting the
10
+ * specifier (needed so that a bundle never carries the optional peer against the consumer's will)
11
+ * also keeps it from resolving it. So the application imports the module itself, where its own
12
+ * bundler resolves it statically, and hands it over:
13
+ *
14
+ * import * as fontoxpath from 'fontoxpath';
15
+ * import { FontoXPathEngine } from '@docx4j/core-ts/xpath-fonto';
16
+ * pkg.xpathEngine = new FontoXPathEngine(fontoxpath); // ready at once
17
+ *
18
+ * In Node, `new FontoXPathEngine()` and `await ready()` still work and still say what to install
19
+ * if the peer is absent. Which module is loaded, and when, is the consumer's business either way -
20
+ * the same principle as `pkg.xpathEngine` being settable at all (CR-005 phase A, 2026-09-26).
6
21
  */
7
22
  export declare class FontoXPathEngine implements XPathEngine {
8
23
  private module;
9
24
  private loading;
25
+ /**
26
+ * @param fontoxpath the `fontoxpath` module, imported by the caller. Omit it in Node to have
27
+ * `ready()` import it; supply it in a browser or a bundle, where a bare dynamic specifier
28
+ * cannot be resolved. Either the module's namespace object or its default export is accepted,
29
+ * the published ESM build having no named exports under Node's interop.
30
+ */
31
+ constructor(fontoxpath?: unknown);
10
32
  get isReady(): boolean;
11
33
  ready(): Promise<void>;
12
34
  select(expression: string, context: Node, namespaces?: Record<string, string>): Node[];
@@ -21,8 +21,33 @@ const ANY_TYPE = 0;
21
21
  * XPath 3.1 through FontoXPath, for the `xpath2` boolean conversion mode and for expressions
22
22
  * written in XPath 2.0 syntax. `select`, `selectValue` and the `java` and `xpath1` modes answer as
23
23
  * `DefaultXPathEngine` does; `xpath2` is the mode this engine exists for.
24
+ *
25
+ * **In a browser or any bundled application, pass the module in.** The no-argument form loads
26
+ * `fontoxpath` by a dynamic `import()` of the bare specifier, which Node resolves and a browser
27
+ * does not: there is no import map, and the `@vite-ignore` that keeps a bundler from rewriting the
28
+ * specifier (needed so that a bundle never carries the optional peer against the consumer's will)
29
+ * also keeps it from resolving it. So the application imports the module itself, where its own
30
+ * bundler resolves it statically, and hands it over:
31
+ *
32
+ * import * as fontoxpath from 'fontoxpath';
33
+ * import { FontoXPathEngine } from '@docx4j/core-ts/xpath-fonto';
34
+ * pkg.xpathEngine = new FontoXPathEngine(fontoxpath); // ready at once
35
+ *
36
+ * In Node, `new FontoXPathEngine()` and `await ready()` still work and still say what to install
37
+ * if the peer is absent. Which module is loaded, and when, is the consumer's business either way -
38
+ * the same principle as `pkg.xpathEngine` being settable at all (CR-005 phase A, 2026-09-26).
24
39
  */
25
40
  export class FontoXPathEngine {
41
+ /**
42
+ * @param fontoxpath the `fontoxpath` module, imported by the caller. Omit it in Node to have
43
+ * `ready()` import it; supply it in a browser or a bundle, where a bare dynamic specifier
44
+ * cannot be resolved. Either the module's namespace object or its default export is accepted,
45
+ * the published ESM build having no named exports under Node's interop.
46
+ */
47
+ constructor(fontoxpath) {
48
+ if (fontoxpath !== undefined)
49
+ this.module = moduleOf(fontoxpath);
50
+ }
26
51
  get isReady() {
27
52
  return this.module !== undefined;
28
53
  }
@@ -31,15 +56,14 @@ export class FontoXPathEngine {
31
56
  if (this.isReady)
32
57
  return;
33
58
  (_a = this.loading) !== null && _a !== void 0 ? _a : (this.loading = (async () => {
34
- var _a;
35
59
  const specifier = 'fontoxpath';
36
60
  try {
37
- // The published ESM build has no named exports under Node's interop, only a default.
38
61
  const loaded = await import(/* @vite-ignore */ specifier);
39
- this.module = (_a = loaded.default) !== null && _a !== void 0 ? _a : loaded;
62
+ this.module = moduleOf(loaded);
40
63
  }
41
64
  catch (e) {
42
- throw new Docx4JException("FontoXPathEngine needs the optional peer dependency 'fontoxpath' (npm install fontoxpath)", { cause: e });
65
+ throw new Docx4JException("FontoXPathEngine needs the optional peer dependency 'fontoxpath' (npm install fontoxpath); "
66
+ + 'in a browser or a bundle, import it yourself and pass it: new FontoXPathEngine(fontoxpath)', { cause: e });
43
67
  }
44
68
  })());
45
69
  await this.loading;
@@ -107,6 +131,20 @@ export class FontoXPathEngine {
107
131
  return this.module;
108
132
  }
109
133
  }
134
+ /**
135
+ * The module, whether given as a namespace object or as a default export, checked for the one
136
+ * function every call goes through so that the wrong module is refused where it is passed rather
137
+ * than at the first evaluation.
138
+ */
139
+ function moduleOf(loaded) {
140
+ const candidate = loaded;
141
+ const module = typeof (candidate === null || candidate === void 0 ? void 0 : candidate.evaluateXPath) === 'function' ? candidate : candidate === null || candidate === void 0 ? void 0 : candidate.default;
142
+ if (!module || typeof module.evaluateXPath !== 'function') {
143
+ throw new Docx4JException('FontoXPathEngine was given something that is not the fontoxpath module: '
144
+ + "expected its evaluateXPath, as in new FontoXPathEngine(await import('fontoxpath'))");
145
+ }
146
+ return module;
147
+ }
110
148
  /** A FontoXPath result in the four-type union `selectValue` reports. */
111
149
  function valueOf(value) {
112
150
  var _a;
@@ -179,6 +179,7 @@ export const SUBSET_MEMBERS = [
179
179
  'Range.getComments',
180
180
  'Range.getRange',
181
181
  'Range.getTrackedChanges',
182
+ 'Range.hyperlink',
182
183
  'Range.insertComment',
183
184
  'Range.insertParagraph',
184
185
  'Range.insertText',
@@ -9,6 +9,7 @@ import type { Author } from '../model/content/comments.mjs';
9
9
  import { CustomXmlPartCollection } from '../model/customxml/CustomXmlPartCollection.mjs';
10
10
  import { type XPathEngine } from '../model/customxml/xpath.mjs';
11
11
  import { ChangeTracker, type ChangeTrackingMode, type TrackingHost } from '../model/content/tracking.mjs';
12
+ import { NumberingFacade } from '../model/content/List.mjs';
12
13
  import { PropertyResolver } from '../model/properties/PropertyResolver.mjs';
13
14
  import type { Emulator } from '../model/listnumbering/Emulator.mjs';
14
15
  import type { TrackedChange } from '../model/content/TrackedChange.mjs';
@@ -161,6 +162,14 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
161
162
  * `customXmlParts.load()`; every node call is synchronous afterwards.
162
163
  */
163
164
  xpathEngine: XPathEngine;
165
+ /**
166
+ * The document's list definitions as verbs that need no paragraph: `newList()` and
167
+ * `restart(numId)` (CR-002 section 19). `Paragraph.startNewList()` and `List.restart()` are
168
+ * Office JS's shapes and remain; this is for a caller that holds a `w:numId` rather than a
169
+ * paragraph - an editor with its own document model, writing `w:numPr` itself.
170
+ */
171
+ get numbering(): NumberingFacade;
172
+ private numberingFacade;
164
173
  /**
165
174
  * Office JS `document.customXmlParts`: the custom XML data storage parts as views, with
166
175
  * `add`, and docx4j's `applyBindings()` / `updateFromContentControls()` (CR-002 section 3.5).
@@ -13,6 +13,7 @@ import { HeaderPart, FooterPart } from '../parts/wml/index.mjs';
13
13
  import { CustomXmlPartCollection } from '../model/customxml/CustomXmlPartCollection.mjs';
14
14
  import { DefaultXPathEngine } from '../model/customxml/xpath.mjs';
15
15
  import { ChangeTracker } from '../model/content/tracking.mjs';
16
+ import { NumberingFacade } from '../model/content/List.mjs';
16
17
  import { PropertyResolver } from '../model/properties/PropertyResolver.mjs';
17
18
  import { PropertyResolverNotCreatedException } from '../opc/exceptions.mjs';
18
19
  import { XmlPart } from '../parts/XmlPart.mjs';
@@ -385,6 +386,16 @@ export class WordprocessingMLPackage extends OpcPackage {
385
386
  all.push(await part.getBody());
386
387
  return all;
387
388
  }
389
+ /**
390
+ * The document's list definitions as verbs that need no paragraph: `newList()` and
391
+ * `restart(numId)` (CR-002 section 19). `Paragraph.startNewList()` and `List.restart()` are
392
+ * Office JS's shapes and remain; this is for a caller that holds a `w:numId` rather than a
393
+ * paragraph - an editor with its own document model, writing `w:numPr` itself.
394
+ */
395
+ get numbering() {
396
+ var _a;
397
+ return ((_a = this.numberingFacade) !== null && _a !== void 0 ? _a : (this.numberingFacade = new NumberingFacade(this)));
398
+ }
388
399
  /**
389
400
  * Office JS `document.customXmlParts`: the custom XML data storage parts as views, with
390
401
  * `add`, and docx4j's `applyBindings()` / `updateFromContentControls()` (CR-002 section 3.5).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docx4j/core-ts",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",