@docx4j/core-ts 0.1.5 → 0.2.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 (47) hide show
  1. package/README.md +76 -0
  2. package/dist/model/content/Body.d.mts +36 -2
  3. package/dist/model/content/Body.mjs +79 -4
  4. package/dist/model/content/Comment.d.mts +15 -5
  5. package/dist/model/content/Comment.mjs +70 -22
  6. package/dist/model/content/List.d.mts +13 -2
  7. package/dist/model/content/List.mjs +48 -9
  8. package/dist/model/content/Paragraph.d.mts +11 -1
  9. package/dist/model/content/Paragraph.mjs +18 -7
  10. package/dist/model/content/Range.d.mts +7 -2
  11. package/dist/model/content/Range.mjs +21 -8
  12. package/dist/model/content/binder.d.mts +44 -0
  13. package/dist/model/content/binder.mjs +95 -0
  14. package/dist/model/content/comments.d.mts +19 -1
  15. package/dist/model/content/index.d.mts +5 -3
  16. package/dist/model/content/index.mjs +3 -1
  17. package/dist/model/content/search.d.mts +27 -2
  18. package/dist/model/content/search.mjs +59 -3
  19. package/dist/model/content/stylesFacade.d.mts +37 -0
  20. package/dist/model/content/stylesFacade.mjs +114 -0
  21. package/dist/model/content/tracking.d.mts +57 -2
  22. package/dist/model/content/tracking.mjs +47 -15
  23. package/dist/model/customxml/CustomXmlPartCollection.d.mts +7 -0
  24. package/dist/model/customxml/CustomXmlPartCollection.mjs +40 -6
  25. package/dist/model/customxml/xpath.d.mts +18 -3
  26. package/dist/model/customxml/xpath.mjs +18 -3
  27. package/dist/office-js/index.d.mts +3 -1
  28. package/dist/office-js/run.d.mts +12 -2
  29. package/dist/office-js/run.mjs +8 -1
  30. package/dist/office-js/shimmed.d.mts +46 -0
  31. package/dist/office-js/shimmed.mjs +1 -0
  32. package/dist/opc/exceptions.d.mts +25 -0
  33. package/dist/opc/exceptions.mjs +20 -0
  34. package/dist/opc/index.d.mts +1 -1
  35. package/dist/opc/index.mjs +1 -1
  36. package/dist/opc/mce/understood.mjs +37 -8
  37. package/dist/packages/WordprocessingMLPackage.d.mts +92 -2
  38. package/dist/packages/WordprocessingMLPackage.mjs +144 -1
  39. package/dist/parts/XmlPart.d.mts +44 -0
  40. package/dist/parts/XmlPart.mjs +86 -3
  41. package/dist/parts/sml/index.d.mts +9 -8
  42. package/dist/parts/sml/index.mjs +23 -19
  43. package/dist/parts/wml/index.d.mts +10 -2
  44. package/dist/parts/wml/index.mjs +11 -3
  45. package/dist/parts/wml/spliceableStyles.d.mts +1 -0
  46. package/dist/parts/wml/spliceableStyles.mjs +122 -0
  47. package/package.json +2 -2
@@ -1,18 +1,21 @@
1
1
  import { OpcPackage, type PackageSource } from './OpcPackage.mjs';
2
2
  import type { LoadOptions } from '../opc/Load.mjs';
3
3
  import type { Part } from '../parts/Part.mjs';
4
- import { MainDocumentPart, type NumberingDefinitionsPart } from '../parts/wml/index.mjs';
4
+ import { MainDocumentPart, StyleDefinitionsPart, type NumberingDefinitionsPart } from '../parts/wml/index.mjs';
5
5
  import { type FontSettings, type DefaultThemeValue } from '../model/fonts/defaultTheme.mjs';
6
6
  import type { Body, Address, Outline, OutlineParagraph, OutlineTable } from '../model/content/Body.mjs';
7
7
  import type { Paragraph } from '../model/content/Paragraph.mjs';
8
8
  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
- import { ChangeTracker, type ChangeTrackingMode, type TrackingHost } from '../model/content/tracking.mjs';
11
+ import { ChangeTracker, type ChangeTrackingMode, type TrackingHost, type TrackingScope } from '../model/content/tracking.mjs';
12
12
  import { NumberingFacade } from '../model/content/List.mjs';
13
+ import { StylesFacade } from '../model/content/stylesFacade.mjs';
14
+ import type { Bound } from '../model/content/binder.mjs';
13
15
  import { PropertyResolver } from '../model/properties/PropertyResolver.mjs';
14
16
  import type { Emulator } from '../model/listnumbering/Emulator.mjs';
15
17
  import type { TrackedChange } from '../model/content/TrackedChange.mjs';
18
+ import { XmlPart } from '../parts/XmlPart.mjs';
16
19
  import type { PartSink } from '../opc/PartStore.mjs';
17
20
  import '../parts/wml/comments.mjs';
18
21
  /** The outline of a document: the body's paragraphs and tables, then each header's and footer's (loaded ones only). */
@@ -59,6 +62,25 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
59
62
  private tracker;
60
63
  /** Set when `changeTrackingMode` was written but the settings part was not unmarshalled yet. */
61
64
  private trackingPending;
65
+ /**
66
+ * `withTracking`'s mode for the duration of one call: read by the getter before everything else
67
+ * and **never** written by the setter, so it touches neither the settings part, `trackingPending`
68
+ * nor the cached `trackingMode` (CR-002 section 28.1).
69
+ */
70
+ private trackingOverride;
71
+ /**
72
+ * The `withTracking` calls still running, oldest first, and what the package said before the
73
+ * first of them. The latest call's markup applies; when it ends, the latest one still running
74
+ * applies again, and when none is left the package is as it was (CR-002 section 31).
75
+ */
76
+ private trackingScopes;
77
+ private beforeScopes;
78
+ /**
79
+ * The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
80
+ * source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
81
+ * array until they are set again.
82
+ */
83
+ private seededParts;
62
84
  /**
63
85
  * The package's font settings (CR-001 Phase B step 4), docx4j's `docx4j.fonts.*` properties
64
86
  * scoped to one package: `defaultTheme` says which Office theme a font reference resolves
@@ -131,6 +153,30 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
131
153
  */
132
154
  get changeTrackingMode(): ChangeTrackingMode;
133
155
  set changeTrackingMode(mode: ChangeTrackingMode);
156
+ /**
157
+ * Runs `fn` with revisions recorded as `markup` says, and the document's own setting untouched:
158
+ * nothing is written to `w:trackRevisions`, no settings part is created, and the mode a later read
159
+ * reports is the one that was there before (CR-002 section 28.1, for an editor whose agent must
160
+ * record its edits as a named author in a room whose tracking setting belongs to the document).
161
+ *
162
+ * The author, the date and the mode are restored in a `finally`, so a throw from `fn` cannot leave
163
+ * the package tracking. It is asynchronous for one reason: it awaits `seedAnnotationIds()` before
164
+ * running `fn`, so a revision written inside it cannot reuse an id from a part nobody has read
165
+ * (section 28.2). `fn` itself may be synchronous.
166
+ *
167
+ * Calls nest, and calls that overlap in time end in whatever order they end without leaving the
168
+ * package tracking. But the markup is the package's for as long as a call runs, not the call's:
169
+ * while two are running, **the later one's author and mode apply to every edit**, the earlier
170
+ * one's included, because nothing here can tell which asynchronous call an edit came from. Await
171
+ * one before starting another when they must be recorded as different authors (section 31).
172
+ */
173
+ withTracking<T>(markup: TrackingScope, fn: () => T | Promise<T>): Promise<T>;
174
+ /**
175
+ * One `withTracking` call's markup made the package's. The tracker is left alone: the getter makes
176
+ * another when the mode differs, and the id counter it reads is the package's, so a tracker kept
177
+ * across the call cannot reissue an id given out inside it (section 31).
178
+ */
179
+ private applyTrackingScope;
134
180
  /** The mode as the settings part has it, unmarshalling the part (docx4j's async counterpart of the getter). */
135
181
  getChangeTrackingMode(): Promise<ChangeTrackingMode>;
136
182
  /** Writes `w:trackRevisions`, unmarshalling the settings part, or creating it when there is none. */
@@ -145,6 +191,41 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
145
191
  getTrackedChanges(): TrackedChange[];
146
192
  /** The tracker every content-API mutation asks for; undefined while the mode is `Off`. */
147
193
  get changeTracker(): ChangeTracker | undefined;
194
+ /**
195
+ * The objects an XPath selects in a part of this document, through the package's engine
196
+ * (CR-006; docx4j `MainDocumentPart.getJAXBNodesViaXPath`). The part defaults to the main
197
+ * document part, which is what a `//w:p[...]` expression means.
198
+ */
199
+ selectObjects(xpath: string, options?: {
200
+ part?: XmlPart<unknown>;
201
+ namespaces?: Record<string, string>;
202
+ }): Promise<Bound[]>;
203
+ /**
204
+ * TrackingHost: a floor for the annotation id, from the WordprocessingML parts that were **not**
205
+ * unmarshalled; `seedAnnotationIds()` fills it. 0 until then (CR-002 section 28.2).
206
+ */
207
+ annotationIdFloor: number;
208
+ /**
209
+ * TrackingHost: the last annotation id issued, shared by every tracker this package makes (CR-002
210
+ * section 31). Forgotten when the mode is set, as the tracker used to be, so that the next id is
211
+ * found by a fresh scan of whatever has arrived since by other routes (`insertOoxml`, `setContents`).
212
+ */
213
+ annotationIdCounter: number | undefined;
214
+ /**
215
+ * Raises `annotationIdFloor` above every annotation id in the WordprocessingML parts that are not
216
+ * unmarshalled, reading each **privately** (`readContents`, CR-001 Phase D's device), so that a
217
+ * part nobody has touched keeps its byte-for-byte round trip and still cannot have its ids reused.
218
+ *
219
+ * Awaited by `setChangeTrackingMode()` and by `withTracking()`, which is where tracking is turned
220
+ * on; a caller that uses the synchronous `changeTrackingMode` setter on a document with unread
221
+ * parts should await this itself, since `nextId()` cannot (the content API's setters are
222
+ * synchronous, which is why the floor exists at all rather than the scan being made async).
223
+ *
224
+ * A part is read once for the bytes it holds: an agent that wraps every edit in `withTracking`
225
+ * would otherwise re-read every header, footer and note part each time (section 31). Its source
226
+ * bytes cannot change; bytes that were set are read again when they are set again.
227
+ */
228
+ seedAnnotationIds(): Promise<number>;
148
229
  /** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
149
230
  markupRoots(): object[];
150
231
  /** A clone carries the identity, the tracking date, the theme setting and the XPath engine. */
@@ -162,6 +243,15 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
162
243
  * `customXmlParts.load()`; every node call is synchronous afterwards.
163
244
  */
164
245
  xpathEngine: XPathEngine;
246
+ /**
247
+ * The document's style definitions as a verb that needs no `Body`: `ensure(ids)` splices in a
248
+ * style the document lacks, out of the same defaults `createPackage()` writes, with what it is
249
+ * based on and linked to (CR-002 section 22.2).
250
+ */
251
+ get styles(): StylesFacade;
252
+ private stylesFacade;
253
+ /** The styles part, created with the defaults when the document has none (CR-002 section 22.2). */
254
+ ensureStyleDefinitionsPart(): StyleDefinitionsPart;
165
255
  /**
166
256
  * The document's list definitions as verbs that need no paragraph: `newList()` and
167
257
  * `restart(numId)` (CR-002 section 19). `Paragraph.startNewList()` and `List.restart()` are
@@ -12,8 +12,9 @@ import { newFontSettings, themeOfSettings, } from '../model/fonts/defaultTheme.m
12
12
  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
- import { ChangeTracker } from '../model/content/tracking.mjs';
15
+ import { ChangeTracker, highestAnnotationId } from '../model/content/tracking.mjs';
16
16
  import { NumberingFacade } from '../model/content/List.mjs';
17
+ import { StylesFacade } from '../model/content/stylesFacade.mjs';
17
18
  import { PropertyResolver } from '../model/properties/PropertyResolver.mjs';
18
19
  import { PropertyResolverNotCreatedException } from '../opc/exceptions.mjs';
19
20
  import { XmlPart } from '../parts/XmlPart.mjs';
@@ -68,6 +69,18 @@ export class WordprocessingMLPackage extends OpcPackage {
68
69
  this.author = { name: 'docx4j' };
69
70
  /** Set when `changeTrackingMode` was written but the settings part was not unmarshalled yet. */
70
71
  this.trackingPending = false;
72
+ /**
73
+ * The `withTracking` calls still running, oldest first, and what the package said before the
74
+ * first of them. The latest call's markup applies; when it ends, the latest one still running
75
+ * applies again, and when none is left the package is as it was (CR-002 section 31).
76
+ */
77
+ this.trackingScopes = [];
78
+ /**
79
+ * The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
80
+ * source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
81
+ * array until they are set again.
82
+ */
83
+ this.seededParts = new WeakMap();
71
84
  /**
72
85
  * The package's font settings (CR-001 Phase B step 4), docx4j's `docx4j.fonts.*` properties
73
86
  * scoped to one package: `defaultTheme` says which Office theme a font reference resolves
@@ -79,6 +92,11 @@ export class WordprocessingMLPackage extends OpcPackage {
79
92
  * `mainDocumentPart.refreshFonts()`.
80
93
  */
81
94
  this.fonts = newFontSettings();
95
+ /**
96
+ * TrackingHost: a floor for the annotation id, from the WordprocessingML parts that were **not**
97
+ * unmarshalled; `seedAnnotationIds()` fills it. 0 until then (CR-002 section 28.2).
98
+ */
99
+ this.annotationIdFloor = 0;
82
100
  // --- custom XML (CR-002 phase E) ---
83
101
  /**
84
102
  * The XPath engine the custom XML views use (CR-002 section 3.6): `document.evaluate` in a
@@ -258,6 +276,8 @@ export class WordprocessingMLPackage extends OpcPackage {
258
276
  */
259
277
  get changeTrackingMode() {
260
278
  var _a;
279
+ if (this.trackingOverride !== undefined)
280
+ return this.trackingOverride;
261
281
  if (this.trackingMode !== undefined)
262
282
  return this.trackingMode;
263
283
  const settings = (_a = this.mainDocumentPart) === null || _a === void 0 ? void 0 : _a.documentSettingsPart;
@@ -268,6 +288,7 @@ export class WordprocessingMLPackage extends OpcPackage {
268
288
  set changeTrackingMode(mode) {
269
289
  this.trackingMode = mode;
270
290
  this.tracker = undefined;
291
+ this.annotationIdCounter = undefined;
271
292
  const settings = this.mainDocumentPart ? this.settingsPart() : undefined;
272
293
  if (settings === null || settings === void 0 ? void 0 : settings.isUnmarshalled) {
273
294
  writeTrackRevisions(settings.contents, mode);
@@ -276,6 +297,58 @@ export class WordprocessingMLPackage extends OpcPackage {
276
297
  else
277
298
  this.trackingPending = true;
278
299
  }
300
+ /**
301
+ * Runs `fn` with revisions recorded as `markup` says, and the document's own setting untouched:
302
+ * nothing is written to `w:trackRevisions`, no settings part is created, and the mode a later read
303
+ * reports is the one that was there before (CR-002 section 28.1, for an editor whose agent must
304
+ * record its edits as a named author in a room whose tracking setting belongs to the document).
305
+ *
306
+ * The author, the date and the mode are restored in a `finally`, so a throw from `fn` cannot leave
307
+ * the package tracking. It is asynchronous for one reason: it awaits `seedAnnotationIds()` before
308
+ * running `fn`, so a revision written inside it cannot reuse an id from a part nobody has read
309
+ * (section 28.2). `fn` itself may be synchronous.
310
+ *
311
+ * Calls nest, and calls that overlap in time end in whatever order they end without leaving the
312
+ * package tracking. But the markup is the package's for as long as a call runs, not the call's:
313
+ * while two are running, **the later one's author and mode apply to every edit**, the earlier
314
+ * one's included, because nothing here can tell which asynchronous call an edit came from. Await
315
+ * one before starting another when they must be recorded as different authors (section 31).
316
+ */
317
+ async withTracking(markup, fn) {
318
+ await this.seedAnnotationIds();
319
+ const scope = { ...markup };
320
+ if (this.trackingScopes.length === 0) {
321
+ this.beforeScopes = { author: this.author, date: this.trackedChangeDate, override: this.trackingOverride };
322
+ }
323
+ this.trackingScopes.push(scope);
324
+ this.applyTrackingScope(scope);
325
+ try {
326
+ return await fn();
327
+ }
328
+ finally {
329
+ this.trackingScopes.splice(this.trackingScopes.indexOf(scope), 1);
330
+ const latest = this.trackingScopes[this.trackingScopes.length - 1];
331
+ if (latest !== undefined)
332
+ this.applyTrackingScope(latest);
333
+ else if (this.beforeScopes !== undefined) {
334
+ this.author = this.beforeScopes.author;
335
+ this.trackedChangeDate = this.beforeScopes.date;
336
+ this.trackingOverride = this.beforeScopes.override;
337
+ this.beforeScopes = undefined;
338
+ }
339
+ }
340
+ }
341
+ /**
342
+ * One `withTracking` call's markup made the package's. The tracker is left alone: the getter makes
343
+ * another when the mode differs, and the id counter it reads is the package's, so a tracker kept
344
+ * across the call cannot reissue an id given out inside it (section 31).
345
+ */
346
+ applyTrackingScope(scope) {
347
+ var _a;
348
+ this.author = scope.author;
349
+ this.trackedChangeDate = scope.date;
350
+ this.trackingOverride = (_a = scope.mode) !== null && _a !== void 0 ? _a : 'TrackAll';
351
+ }
279
352
  /** The mode as the settings part has it, unmarshalling the part (docx4j's async counterpart of the getter). */
280
353
  async getChangeTrackingMode() {
281
354
  var _a, _b;
@@ -288,8 +361,11 @@ export class WordprocessingMLPackage extends OpcPackage {
288
361
  async setChangeTrackingMode(mode) {
289
362
  this.trackingMode = mode;
290
363
  this.tracker = undefined;
364
+ this.annotationIdCounter = undefined;
291
365
  this.trackingPending = false;
292
366
  writeTrackRevisions(await this.settingsPart().getContents(), mode);
367
+ if (mode !== 'Off')
368
+ await this.seedAnnotationIds();
293
369
  }
294
370
  /** The settings part, created (with its relationship and content type) when the document has none. */
295
371
  settingsPart() {
@@ -319,6 +395,53 @@ export class WordprocessingMLPackage extends OpcPackage {
319
395
  this.tracker = new ChangeTracker(this, mode);
320
396
  return this.tracker;
321
397
  }
398
+ /**
399
+ * The objects an XPath selects in a part of this document, through the package's engine
400
+ * (CR-006; docx4j `MainDocumentPart.getJAXBNodesViaXPath`). The part defaults to the main
401
+ * document part, which is what a `//w:p[...]` expression means.
402
+ */
403
+ async selectObjects(xpath, options = {}) {
404
+ var _a;
405
+ const part = (_a = options.part) !== null && _a !== void 0 ? _a : this.getMainDocumentPart();
406
+ return part.selectObjects(xpath, { engine: this.xpathEngine, namespaces: options.namespaces });
407
+ }
408
+ /**
409
+ * Raises `annotationIdFloor` above every annotation id in the WordprocessingML parts that are not
410
+ * unmarshalled, reading each **privately** (`readContents`, CR-001 Phase D's device), so that a
411
+ * part nobody has touched keeps its byte-for-byte round trip and still cannot have its ids reused.
412
+ *
413
+ * Awaited by `setChangeTrackingMode()` and by `withTracking()`, which is where tracking is turned
414
+ * on; a caller that uses the synchronous `changeTrackingMode` setter on a document with unread
415
+ * parts should await this itself, since `nextId()` cannot (the content API's setters are
416
+ * synchronous, which is why the floor exists at all rather than the scan being made async).
417
+ *
418
+ * A part is read once for the bytes it holds: an agent that wraps every edit in `withTracking`
419
+ * would otherwise re-read every header, footer and note part each time (section 31). Its source
420
+ * bytes cannot change; bytes that were set are read again when they are set again.
421
+ */
422
+ async seedAnnotationIds() {
423
+ for (const part of this.parts) {
424
+ if (!(part instanceof XmlPart) || part.isUnmarshalled)
425
+ continue;
426
+ if (!part.partName.name.startsWith('/word/'))
427
+ continue;
428
+ const read = part.isUntouched ? true : await part.getBytes();
429
+ if (this.seededParts.get(part) === read)
430
+ continue;
431
+ let tree;
432
+ try {
433
+ tree = await part.readContents();
434
+ }
435
+ catch {
436
+ tree = undefined;
437
+ } // an unreadable part cannot hold ids we must avoid
438
+ if (typeof tree === 'object' && tree !== null) {
439
+ this.annotationIdFloor = highestAnnotationId(tree, this.annotationIdFloor);
440
+ }
441
+ this.seededParts.set(part, read);
442
+ }
443
+ return this.annotationIdFloor;
444
+ }
322
445
  /** TrackingHost: the trees a new revision id must be above, which is every part already unmarshalled. */
323
446
  markupRoots() {
324
447
  const out = [];
@@ -386,6 +509,26 @@ export class WordprocessingMLPackage extends OpcPackage {
386
509
  all.push(await part.getBody());
387
510
  return all;
388
511
  }
512
+ /**
513
+ * The document's style definitions as a verb that needs no `Body`: `ensure(ids)` splices in a
514
+ * style the document lacks, out of the same defaults `createPackage()` writes, with what it is
515
+ * based on and linked to (CR-002 section 22.2).
516
+ */
517
+ get styles() {
518
+ var _a;
519
+ return ((_a = this.stylesFacade) !== null && _a !== void 0 ? _a : (this.stylesFacade = new StylesFacade(this)));
520
+ }
521
+ /** The styles part, created with the defaults when the document has none (CR-002 section 22.2). */
522
+ ensureStyleDefinitionsPart() {
523
+ const main = this.getMainDocumentPart();
524
+ let part = main.styleDefinitionsPart;
525
+ if (!part) {
526
+ part = new StyleDefinitionsPart();
527
+ part.setXml(DEFAULT_STYLES_XML);
528
+ main.addTargetPart(part);
529
+ }
530
+ return part;
531
+ }
389
532
  /**
390
533
  * The document's list definitions as verbs that need no paragraph: `newList()` and
391
534
  * `restart(numId)` (CR-002 section 19). `Paragraph.startNewList()` and `List.restart()` are
@@ -1,6 +1,17 @@
1
1
  import { type Jsonix } from '@docx4j/generated-objects-ts';
2
2
  import { Part } from './Part.mjs';
3
3
  import { PartName } from '../opc/PartName.mjs';
4
+ import { type Bound } from '../model/content/binder.mjs';
5
+ /** What `selectObjects` needs: the engine to evaluate with, and any prefixes beyond the defaults. */
6
+ export interface SelectOptions {
7
+ /** The engine; `pkg.selectObjects` and `Body.select` pass the package's. It is readied on the snapshot. */
8
+ engine?: {
9
+ ready(context?: Node): Promise<void>;
10
+ select(expression: string, context: Node, namespaces?: Record<string, string>): Node[];
11
+ } | undefined;
12
+ /** Prefixes added to (or overriding) the objects package's own table. */
13
+ namespaces?: Record<string, string> | undefined;
14
+ }
4
15
  /** A qualified name for a part's root element. */
5
16
  export interface RootName {
6
17
  namespaceURI: string;
@@ -31,6 +42,12 @@ export declare class XmlPart<T = unknown> extends Part {
31
42
  /** True once contents were unmarshalled or set; the part is then re-marshalled on save. */
32
43
  get isUnmarshalled(): boolean;
33
44
  get isLoaded(): boolean;
45
+ /**
46
+ * True while the part holds nothing but its source: never unmarshalled, no contents or bytes set.
47
+ * Such a part is written back byte for byte, and what a private read finds in it cannot change,
48
+ * which is what lets a reader remember it (CR-002 section 31).
49
+ */
50
+ get isUntouched(): boolean;
34
51
  /** The unmarshalled contents (docx4j getJaxbElement()); throws when not yet unmarshalled. */
35
52
  get contents(): T;
36
53
  /** The root element as [name, value]; undefined until unmarshalled. */
@@ -57,8 +74,35 @@ export declare class XmlPart<T = unknown> extends Part {
57
74
  getXml(): Promise<string>;
58
75
  /** Marshalled bytes when unmarshalled or set, else the source bytes untouched. */
59
76
  getBytes(): Promise<Uint8Array>;
77
+ /**
78
+ * The objects an XPath selects in this part, through a DOM marshalled for the purpose
79
+ * (CR-006; docx4j `JaxbXmlPart.getJAXBNodesViaXPath`, which does it with a JAXB `Binder`).
80
+ *
81
+ * Each result carries the node the expression selected, the tree object behind it, the attribute
82
+ * name when an attribute was selected, and the node's path from the root. The DOM is a **snapshot**:
83
+ * changing it changes nothing, and a select leaves the part's own bytes alone - the marshal is
84
+ * thrown away. Every call marshals afresh, since the tree has no change log a cached snapshot could
85
+ * be checked against (CR-006 section 2).
86
+ *
87
+ * The prefixes default to the ones the objects package writes, so `//w:p[w:pPr/w:pStyle/@w:val='Heading1']`
88
+ * needs no mapping; `options.namespaces` adds to or overrides them. The engine is the caller's
89
+ * (`pkg.xpathEngine`: XPath 1.0 by default, 3.1 with `FontoXPathEngine`), and is readied here on
90
+ * the snapshot itself: whether an engine can evaluate a tree is a property of the document that
91
+ * tree is in (CR-002 section 21), and readying it on a marshal of its own would cost a second one
92
+ * (CR-002 section 31).
93
+ */
94
+ selectObjects(xpath: string, options?: SelectOptions): Promise<Bound[]>;
60
95
  /** The root element as a DOM element (docx4j XmlUtils.marshaltoW3CDomDocument). */
61
96
  marshalToNode(): Promise<Element>;
97
+ /**
98
+ * Unmarshals, and names the part when the runtime refuses. The runtime's own message says what is
99
+ * wrong and, since `@docx4j/jsonix` 3.4.0, where - `w:sz/@w:val (line 12): Argument [NaN] must be
100
+ * an integer` - but not which part it was reading, which is the first thing a person editing a
101
+ * part's XML needs (CR-002 section 27). The location it found is lifted onto the exception so a
102
+ * caller does not parse the message; `line` is undefined where the parser supplies no position,
103
+ * which is every browser (only xmldom carries a locator).
104
+ */
105
+ private unmarshal;
62
106
  /** The source root element's namespace declarations, prefix to URI (`xmlns=` is the empty prefix). */
63
107
  private recordDeclarations;
64
108
  /**
@@ -1,9 +1,10 @@
1
1
  import { unmarshalNode, marshalNode, NAMESPACE_PREFIXES } from '@docx4j/generated-objects-ts';
2
2
  import { Part } from './Part.mjs';
3
3
  import { Namespaces } from './Namespaces.mjs';
4
- import { Docx4JException } from '../opc/exceptions.mjs';
4
+ import { Docx4JException, PartUnmarshalException } from '../opc/exceptions.mjs';
5
5
  import { parseXml, serializeXml, encodeText, decodeXmlText, XML_DECLARATION } from '../xml/dom.mjs';
6
6
  import { log } from '../model/properties/log.mjs';
7
+ import { bindSnapshot, boundsOf } from '../model/content/binder.mjs';
7
8
  const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
8
9
  const MC_NS = Namespaces.MARKUP_COMPATIBILITY;
9
10
  /** The objects package's prefix table the other way round, prefix to URI, built once. */
@@ -18,6 +19,26 @@ function knownUriOf(prefix) {
18
19
  return prefixToUri.get(prefix);
19
20
  }
20
21
  const warnedPrefixes = new Set();
22
+ /**
23
+ * The element or attribute a located runtime error names, out of its message: jsonix writes
24
+ * `w:sz/@w:val (line 7): ...` when it could place the fault and `w:bogus (line 3): ...` for an
25
+ * element, so the location is everything before the position or the colon - and `w:sz` must not be
26
+ * cut at its own prefix separator, which a naive split on `:` does.
27
+ */
28
+ function locationOf(message, located) {
29
+ var _a;
30
+ if (!located)
31
+ return undefined;
32
+ return (_a = /^(.+?)(?: \(line \d+(?:, column \d+)?\))?: /.exec(message)) === null || _a === void 0 ? void 0 : _a[1];
33
+ }
34
+ /** The objects package's prefix table as XPath wants it, prefix to URI; built once. */
35
+ const NAMESPACE_PREFIXES_BY_PREFIX = (() => {
36
+ const out = {};
37
+ for (const [uri, prefix] of Object.entries(NAMESPACE_PREFIXES))
38
+ if (prefix !== '' && !(prefix in out))
39
+ out[prefix] = uri;
40
+ return out;
41
+ })();
21
42
  /**
22
43
  * A part whose content is a typed element of the object model (docx4j JaxbXmlPart<E>). `T` is the
23
44
  * root element's value type (`Document`, `Styles`, ...). Contents are unmarshalled on first
@@ -36,6 +57,14 @@ export class XmlPart extends Part {
36
57
  get isLoaded() {
37
58
  return this.element !== undefined || this.bytes !== undefined;
38
59
  }
60
+ /**
61
+ * True while the part holds nothing but its source: never unmarshalled, no contents or bytes set.
62
+ * Such a part is written back byte for byte, and what a private read finds in it cannot change,
63
+ * which is what lets a reader remember it (CR-002 section 31).
64
+ */
65
+ get isUntouched() {
66
+ return this.element === undefined && this.bytes === undefined;
67
+ }
39
68
  /** The unmarshalled contents (docx4j getJaxbElement()); throws when not yet unmarshalled. */
40
69
  get contents() {
41
70
  if (this.element === undefined)
@@ -56,7 +85,7 @@ export class XmlPart extends Part {
56
85
  const doc = parseXml(decodeXmlText(bytes));
57
86
  this.recordDeclarations(doc);
58
87
  (_b = this.preprocessor) === null || _b === void 0 ? void 0 : _b.call(this, doc, this);
59
- this.element = await unmarshalNode(doc);
88
+ this.element = await this.unmarshal(doc);
60
89
  (_c = this.rootName) !== null && _c !== void 0 ? _c : (this.rootName = { namespaceURI: this.element.name.namespaceURI, localPart: this.element.name.localPart });
61
90
  this.bytes = undefined;
62
91
  }
@@ -81,7 +110,7 @@ export class XmlPart extends Part {
81
110
  const doc = parseXml(decodeXmlText(bytes));
82
111
  this.recordDeclarations(doc);
83
112
  (_b = this.preprocessor) === null || _b === void 0 ? void 0 : _b.call(this, doc, this);
84
- return (await unmarshalNode(doc)).value;
113
+ return (await this.unmarshal(doc)).value;
85
114
  }
86
115
  /** Replaces the contents (docx4j setJaxbElement / setContents). The root name defaults to the part's. */
87
116
  setContents(value, rootName) {
@@ -116,6 +145,36 @@ export class XmlPart extends Part {
116
145
  throw new Docx4JException(`Part ${this.partName} has no content and no source container`);
117
146
  return loaded;
118
147
  }
148
+ /**
149
+ * The objects an XPath selects in this part, through a DOM marshalled for the purpose
150
+ * (CR-006; docx4j `JaxbXmlPart.getJAXBNodesViaXPath`, which does it with a JAXB `Binder`).
151
+ *
152
+ * Each result carries the node the expression selected, the tree object behind it, the attribute
153
+ * name when an attribute was selected, and the node's path from the root. The DOM is a **snapshot**:
154
+ * changing it changes nothing, and a select leaves the part's own bytes alone - the marshal is
155
+ * thrown away. Every call marshals afresh, since the tree has no change log a cached snapshot could
156
+ * be checked against (CR-006 section 2).
157
+ *
158
+ * The prefixes default to the ones the objects package writes, so `//w:p[w:pPr/w:pStyle/@w:val='Heading1']`
159
+ * needs no mapping; `options.namespaces` adds to or overrides them. The engine is the caller's
160
+ * (`pkg.xpathEngine`: XPath 1.0 by default, 3.1 with `FontoXPathEngine`), and is readied here on
161
+ * the snapshot itself: whether an engine can evaluate a tree is a property of the document that
162
+ * tree is in (CR-002 section 21), and readying it on a marshal of its own would cost a second one
163
+ * (CR-002 section 31).
164
+ */
165
+ async selectObjects(xpath, options = {}) {
166
+ var _a;
167
+ const element = (_a = this.element) !== null && _a !== void 0 ? _a : (await this.getContents(), this.element);
168
+ if (element === undefined)
169
+ throw new Docx4JException(`Part ${this.partName} has no contents to select over`);
170
+ const engine = options.engine;
171
+ if (engine === undefined)
172
+ throw new Docx4JException(`selectObjects on ${this.partName} needs an XPath engine; pkg.selectObjects passes the package's`);
173
+ const snapshot = await bindSnapshot(element);
174
+ await engine.ready(snapshot.root);
175
+ const namespaces = { ...NAMESPACE_PREFIXES_BY_PREFIX, ...options.namespaces };
176
+ return boundsOf(engine.select(xpath, snapshot.root, namespaces), snapshot);
177
+ }
119
178
  /** The root element as a DOM element (docx4j XmlUtils.marshaltoW3CDomDocument). */
120
179
  async marshalToNode() {
121
180
  if (this.element === undefined)
@@ -124,6 +183,30 @@ export class XmlPart extends Part {
124
183
  this.declareIgnorablePrefixes(root);
125
184
  return root;
126
185
  }
186
+ /**
187
+ * Unmarshals, and names the part when the runtime refuses. The runtime's own message says what is
188
+ * wrong and, since `@docx4j/jsonix` 3.4.0, where - `w:sz/@w:val (line 12): Argument [NaN] must be
189
+ * an integer` - but not which part it was reading, which is the first thing a person editing a
190
+ * part's XML needs (CR-002 section 27). The location it found is lifted onto the exception so a
191
+ * caller does not parse the message; `line` is undefined where the parser supplies no position,
192
+ * which is every browser (only xmldom carries a locator).
193
+ */
194
+ async unmarshal(doc) {
195
+ var _a, _b;
196
+ try {
197
+ return await unmarshalNode(doc);
198
+ }
199
+ catch (cause) {
200
+ const located = cause;
201
+ const message = typeof (located === null || located === void 0 ? void 0 : located.message) === 'string' ? located.message : String(cause);
202
+ throw new PartUnmarshalException(`${this.partName}: ${message}`, this.partName.name, {
203
+ cause,
204
+ location: locationOf(message, (located === null || located === void 0 ? void 0 : located.jsonixLocated) === true),
205
+ line: typeof ((_a = located === null || located === void 0 ? void 0 : located.node) === null || _a === void 0 ? void 0 : _a.lineNumber) === 'number' ? located.node.lineNumber : undefined,
206
+ column: typeof ((_b = located === null || located === void 0 ? void 0 : located.node) === null || _b === void 0 ? void 0 : _b.columnNumber) === 'number' ? located.node.columnNumber : undefined,
207
+ });
208
+ }
209
+ }
127
210
  /** The source root element's namespace declarations, prefix to URI (`xmlns=` is the empty prefix). */
128
211
  recordDeclarations(doc) {
129
212
  var _a;
@@ -1,6 +1,7 @@
1
1
  import type * as sml from '@docx4j/generated-objects-ts/modules/org_xlsx4j_sml';
2
+ import type * as x14 from '@docx4j/generated-objects-ts/modules/org_xlsx4j_com_microsoft_schemas_office_spreadsheetml_x2009_x9_main';
3
+ import type * as x15 from '@docx4j/generated-objects-ts/modules/org_xlsx4j_com_microsoft_schemas_office_spreadsheetml_x2010_x11_main';
2
4
  import { XmlPart } from '../XmlPart.mjs';
3
- import { DefaultXmlPart } from '../DefaultXmlPart.mjs';
4
5
  import { BinaryPart } from '../BinaryPart.mjs';
5
6
  import { Part } from '../Part.mjs';
6
7
  import { PartName } from '../../opc/PartName.mjs';
@@ -99,31 +100,31 @@ export declare class PrinterSettingsPart extends BinaryPart {
99
100
  constructor(partName: PartName | string);
100
101
  }
101
102
  /** A slicer cache (`x14:slicerCacheDefinition`), from the workbook. docx4j `SlicerCachePart`. */
102
- export declare class SlicerCachePart extends DefaultXmlPart {
103
+ export declare class SlicerCachePart extends XmlPart<x14.CTSlicerCacheDefinition> {
103
104
  constructor(partName?: PartName | string);
104
105
  }
105
106
  /** A sheet's slicers (`x14:slicers`). docx4j `SlicersPart`. */
106
- export declare class SlicersPart extends DefaultXmlPart {
107
+ export declare class SlicersPart extends XmlPart<x14.CTSlicers> {
107
108
  constructor(partName?: PartName | string);
108
109
  }
109
110
  /** A timeline cache (`x15:timelineCacheDefinition`), from the workbook. docx4j `TimelineCachePart`. */
110
- export declare class TimelineCachePart extends DefaultXmlPart {
111
+ export declare class TimelineCachePart extends XmlPart<x15.CTTimelineCacheDefinition> {
111
112
  constructor(partName?: PartName | string);
112
113
  }
113
114
  /** A sheet's timelines (`x15:timelines`). docx4j `TimelinesPart`. */
114
- export declare class TimelinesPart extends DefaultXmlPart {
115
+ export declare class TimelinesPart extends XmlPart<x15.CTTimelines> {
115
116
  constructor(partName?: PartName | string);
116
117
  }
117
118
  /** A form control's properties (`x14:formControlPr`), from a worksheet. docx4j `ControlPropertiesPart`. */
118
- export declare class ControlPropertiesPart extends DefaultXmlPart {
119
+ export declare class ControlPropertiesPart extends XmlPart<x14.CTFormControlPr> {
119
120
  constructor(partName?: PartName | string);
120
121
  }
121
122
  /** The properties of a custom data part (`x14:datastoreItem`). docx4j `CustomDataPropertiesPart`. */
122
- export declare class CustomDataPropertiesPart extends DefaultXmlPart {
123
+ export declare class CustomDataPropertiesPart extends XmlPart<x14.CTDatastoreItem> {
123
124
  constructor(partName?: PartName | string);
124
125
  }
125
126
  /** A survey (`x15:survey`), from the workbook. docx4j `SurveyPart`. */
126
- export declare class SurveyPart extends DefaultXmlPart {
127
+ export declare class SurveyPart extends XmlPart<x15.CTSurvey> {
127
128
  constructor(partName?: PartName | string);
128
129
  }
129
130
  /**