@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
@@ -10,7 +10,7 @@ function withRPr(run, rPr) {
10
10
  import { Font } from './Font.mjs';
11
11
  import { runFontSelectorOf } from '../fonts/lookup.mjs';
12
12
  import { Range } from './Range.mjs';
13
- import { searchPattern, findAll } from './search.mjs';
13
+ import { searchPattern, matchesOf, expandReplacement } from './search.mjs';
14
14
  import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
15
15
  import { cellOf } from './Table.mjs';
16
16
  import { ContentControl, collectRunControls } from './ContentControl.mjs';
@@ -336,7 +336,16 @@ export class Paragraph {
336
336
  }
337
337
  /** Matches within this paragraph. */
338
338
  search(text, options) {
339
- return findAll(this.text, searchPattern(text, options)).map(([s, e]) => new Range(this, s, e));
339
+ return this.searchMatches(text, options).map((m) => m.range);
340
+ }
341
+ /**
342
+ * `search`, keeping each match's capture groups, which is what a replacement's `$1` or Word's `\1`
343
+ * expands from (CR-002 section 26). Not an Office JS member; `replaceText` is its only caller here
344
+ * and `Body.replaceText` the other.
345
+ */
346
+ searchMatches(text, options) {
347
+ return matchesOf(this.text, searchPattern(text, options))
348
+ .map((match) => ({ range: new Range(this, match.index, match.index + match[0].length), match }));
340
349
  }
341
350
  /**
342
351
  * Replaces every match of `find` with `replace`, last match first so that the offsets of the
@@ -344,9 +353,11 @@ export class Paragraph {
344
353
  * the package's `changeTrackingMode` is on.
345
354
  */
346
355
  replaceText(find, replace, options) {
347
- const matches = this.search(find, options);
348
- for (let i = matches.length - 1; i >= 0; i--)
349
- matches[i].insertText(replace, 'Replace');
356
+ const matches = this.searchMatches(find, options);
357
+ for (let i = matches.length - 1; i >= 0; i--) {
358
+ const { range, match } = matches[i];
359
+ range.insertText(expandReplacement(replace, match, options), 'Replace');
360
+ }
350
361
  return matches.length;
351
362
  }
352
363
  /** The tracked changes in this paragraph, in document order (CR-002 phase F). */
@@ -672,8 +683,8 @@ export class Paragraph {
672
683
  return commentApi().commentsOf(this);
673
684
  }
674
685
  /** Comments the whole paragraph (sugar over `getRange().insertComment`). */
675
- async insertComment(text) {
676
- return commentApi().insertComment(this.getRange(), text);
686
+ async insertComment(content, options) {
687
+ return commentApi().insertComment(this.getRange(), content, options);
677
688
  }
678
689
  // --- content controls (CR-002 phase E) ---
679
690
  /**
@@ -5,6 +5,7 @@ import type { BlockElement } from './Body.mjs';
5
5
  import { type Element, type TextViewOptions } from './tree.mjs';
6
6
  import { ContentControl, type ContentControlType } from './ContentControl.mjs';
7
7
  import { type SearchOptions } from './search.mjs';
8
+ import { type CommentContent, type CommentOptions } from './comments.mjs';
8
9
  import type { Comment } from './Comment.mjs';
9
10
  import type { TrackedChange } from './TrackedChange.mjs';
10
11
  /**
@@ -54,7 +55,11 @@ export declare class Range {
54
55
  /** Removes the span's text. */
55
56
  delete(): void;
56
57
  search(text: string, options?: SearchOptions): Range[];
57
- /** Replaces every match in this span, last first so the offsets stay valid; returns the count (CR-002 section 3.7). */
58
+ /**
59
+ * Replaces every match in this span, last first so the offsets stay valid; returns the count
60
+ * (CR-002 section 3.7). The replacement's `$1` or Word's `\1` expands as it does on a paragraph
61
+ * (section 26; this one was missed there, section 31).
62
+ */
58
63
  replaceText(find: string, replace: string, options?: SearchOptions): number;
59
64
  /** The tracked changes this span covers, in document order (CR-002 phase F). */
60
65
  getTrackedChanges(): TrackedChange[];
@@ -68,7 +73,7 @@ export declare class Range {
68
73
  * boundaries, as `font` does), the comment itself and the side-part entries, creating any of
69
74
  * the comment parts the document lacks. Asynchronous because it unmarshals those parts.
70
75
  */
71
- insertComment(text: string): Promise<Comment>;
76
+ insertComment(content: CommentContent, options?: CommentOptions): Promise<Comment>;
72
77
  /**
73
78
  * Wraps the runs this span covers in a run-level content control (Office JS
74
79
  * `Range.insertContentControl`), splitting the runs at the boundaries as `font` does. An empty
@@ -8,7 +8,7 @@ import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_do
8
8
  import { Namespaces } from '../../parts/Namespaces.mjs';
9
9
  import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
10
10
  import { contentOf } from './ooxml.mjs';
11
- import { searchPattern, findAll } from './search.mjs';
11
+ import { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
12
12
  import { commentApi } from './comments.mjs';
13
13
  /**
14
14
  * A subset of Office JS `Word.Range`: a span of text within one paragraph, [start, end) in the
@@ -151,11 +151,19 @@ export class Range {
151
151
  const base = this.start;
152
152
  return findAll(this.text, searchPattern(text, options)).map(([s, e]) => new Range(this.paragraph, base + s, base + e));
153
153
  }
154
- /** Replaces every match in this span, last first so the offsets stay valid; returns the count (CR-002 section 3.7). */
154
+ /**
155
+ * Replaces every match in this span, last first so the offsets stay valid; returns the count
156
+ * (CR-002 section 3.7). The replacement's `$1` or Word's `\1` expands as it does on a paragraph
157
+ * (section 26; this one was missed there, section 31).
158
+ */
155
159
  replaceText(find, replace, options) {
156
- const matches = this.search(find, options);
157
- for (let i = matches.length - 1; i >= 0; i--)
158
- matches[i].insertText(replace, 'Replace');
160
+ const base = this.start;
161
+ const matches = matchesOf(this.text, searchPattern(find, options));
162
+ for (let i = matches.length - 1; i >= 0; i--) {
163
+ const match = matches[i];
164
+ new Range(this.paragraph, base + match.index, base + match.index + match[0].length)
165
+ .insertText(expandReplacement(replace, match, options), 'Replace');
166
+ }
159
167
  return matches.length;
160
168
  }
161
169
  /** The tracked changes this span covers, in document order (CR-002 phase F). */
@@ -188,8 +196,8 @@ export class Range {
188
196
  * boundaries, as `font` does), the comment itself and the side-part entries, creating any of
189
197
  * the comment parts the document lacks. Asynchronous because it unmarshals those parts.
190
198
  */
191
- async insertComment(text) {
192
- return commentApi().insertComment(this, text);
199
+ async insertComment(content, options) {
200
+ return commentApi().insertComment(this, content, options);
193
201
  }
194
202
  // --- content controls (CR-002 phase E) ---
195
203
  /**
@@ -259,7 +267,12 @@ export class Range {
259
267
  const hash = value.indexOf('#');
260
268
  const address = hash === -1 ? value : value.slice(0, hash);
261
269
  const location = hash === -1 ? '' : value.slice(hash + 1);
262
- const holder = hyperlinkOf({ content: [] });
270
+ // w:history="1" is what Word writes on every hyperlink it makes, and what docx4j writes when
271
+ // it builds one (MainDocumentPart.addHyperlink): "add the target to the viewed hyperlinks"
272
+ // (ECMA-376 17.16.22). Readers do not depend on it, but a document holding one holder from
273
+ // each source should not differ over it. Found by the docx4j-ts-editor session, ED-003
274
+ // section 11.8.
275
+ const holder = hyperlinkOf({ content: [], history: true });
263
276
  if (location !== '')
264
277
  holder.value.anchor = location;
265
278
  if (address !== '')
@@ -0,0 +1,44 @@
1
+ import { type Jsonix } from '@docx4j/generated-objects-ts';
2
+ /** What an XPath selected, and the tree object behind it (CR-006 section 2). */
3
+ export interface Bound {
4
+ /**
5
+ * The DOM node the expression selected. It is in a snapshot marshalled for this call, so changing
6
+ * it changes nothing: edits go through the content API or `setContents`, never here.
7
+ */
8
+ node: Node;
9
+ /**
10
+ * The tree object: for an element, what a walk of the typed tree would hand you at that path,
11
+ * never a `{ name, value }` wrapper; for an attribute or a text node, the object of the element
12
+ * that owns it. Undefined for an element the model does not build - a wildcard child is its own
13
+ * object, so this is undefined only above the root or for a node the marshal did not report.
14
+ */
15
+ object: unknown;
16
+ /** Set when an attribute was selected: its qualified name on `object`'s element. */
17
+ attribute?: string;
18
+ /** The node's path from the part's root, `/w:document/w:body/w:p[3]/w:r[2]`. */
19
+ path: string;
20
+ }
21
+ /** A marshalled snapshot and the element-to-object map recorded while it was written. */
22
+ export interface BoundSnapshot {
23
+ root: Element;
24
+ /** The object each element was marshalled from; a wildcard child maps to the original tree node. */
25
+ objects: WeakMap<Element, unknown>;
26
+ }
27
+ /**
28
+ * Marshals a root element and records what each DOM element came from. One marshal per call: the
29
+ * tree has no change log a cached snapshot could be checked against, and a stale answer is worse
30
+ * than a slow one (docx4j's `refreshXmlFirst`, always true here). The editor measured 349 to 411 ms
31
+ * for a 255-page main document part, against 975 ms for its first unmarshal, and runs a selection
32
+ * on Enter - so no cache is needed, which is what CR-006 section 2 was uncertain about.
33
+ */
34
+ export declare function bindSnapshot<T>(element: Jsonix.TypedNamedValue<T>): Promise<BoundSnapshot>;
35
+ /** Turns selected nodes into `Bound`s against a snapshot. */
36
+ export declare function boundsOf(nodes: readonly Node[], snapshot: BoundSnapshot): Bound[];
37
+ /**
38
+ * The node's path from the document element, in the prefixed form the part itself uses, with a
39
+ * position on every step: `/w:document/w:body/w:p[3]/w:r[2]`, and `/@w:val` for an attribute. The
40
+ * position counts siblings of the same qualified name, as XPath does, and is written even when
41
+ * there is only one - a path that is stable whether or not a sibling is added later is worth more
42
+ * than a short one, and docx4j's harness records the same form.
43
+ */
44
+ export declare function pathOf(node: Node): string;
@@ -0,0 +1,95 @@
1
+ // CR-006: XPath over the WordprocessingML tree, through a read-only binder recorded while marshalling.
2
+ //
3
+ // docx4j answers `getJAXBNodesViaXPath` with a JAXB `Binder<Node>`, which keeps the link between a
4
+ // DOM node and the object it came from in both directions. This runtime has no binder, so CR-002
5
+ // section 3.6 left XPath over the typed tree out; the demand appeared (the editor's Developer
6
+ // perspective selects by XPath) and this is the cheap half - DOM node to object, which is what a
7
+ // selection needs. Nothing is written back through the DOM (CR-006 section 7).
8
+ //
9
+ // The link is recorded by `@docx4j/jsonix` 3.4.0's `onElement` callback, reached through the objects
10
+ // facade's `marshalNode(value, { onElement })` (objects CR-007): the runtime calls it once per
11
+ // class-typed value with the element it was written into, and once per wildcard DOM child with the
12
+ // **imported copy** of that child - `writeNode` does `importNode`, so the element in the snapshot is
13
+ // not the node in the tree and a map keyed on it would miss exactly the content a caller most wants
14
+ // (jsonix-CR-006 section 2, finding 3).
15
+ import { marshalNode } from '@docx4j/generated-objects-ts';
16
+ /**
17
+ * Marshals a root element and records what each DOM element came from. One marshal per call: the
18
+ * tree has no change log a cached snapshot could be checked against, and a stale answer is worse
19
+ * than a slow one (docx4j's `refreshXmlFirst`, always true here). The editor measured 349 to 411 ms
20
+ * for a 255-page main document part, against 975 ms for its first unmarshal, and runs a selection
21
+ * on Enter - so no cache is needed, which is what CR-006 section 2 was uncertain about.
22
+ */
23
+ export async function bindSnapshot(element) {
24
+ const objects = new WeakMap();
25
+ const root = await marshalNode(element, {
26
+ onElement: (node, value) => {
27
+ if (isElement(node))
28
+ objects.set(node, value);
29
+ },
30
+ });
31
+ return { root, objects };
32
+ }
33
+ /** Turns selected nodes into `Bound`s against a snapshot. */
34
+ export function boundsOf(nodes, snapshot) {
35
+ return nodes.map((node) => {
36
+ const owner = owningElement(node);
37
+ const bound = {
38
+ node,
39
+ object: owner === undefined ? undefined : snapshot.objects.get(owner),
40
+ path: pathOf(node),
41
+ };
42
+ if (node.nodeType === 2)
43
+ bound.attribute = node.name;
44
+ return bound;
45
+ });
46
+ }
47
+ /**
48
+ * The element an XPath result belongs to: itself for an element, its owner for an attribute, its
49
+ * parent for a text, comment or processing-instruction node. A document node has none.
50
+ */
51
+ function owningElement(node) {
52
+ if (node.nodeType === 1)
53
+ return node;
54
+ if (node.nodeType === 2) {
55
+ const owner = node.ownerElement;
56
+ return owner === null ? undefined : owner;
57
+ }
58
+ const parent = node.parentNode;
59
+ return parent !== null && parent.nodeType === 1 ? parent : undefined;
60
+ }
61
+ /**
62
+ * The node's path from the document element, in the prefixed form the part itself uses, with a
63
+ * position on every step: `/w:document/w:body/w:p[3]/w:r[2]`, and `/@w:val` for an attribute. The
64
+ * position counts siblings of the same qualified name, as XPath does, and is written even when
65
+ * there is only one - a path that is stable whether or not a sibling is added later is worth more
66
+ * than a short one, and docx4j's harness records the same form.
67
+ */
68
+ export function pathOf(node) {
69
+ if (node.nodeType === 2) {
70
+ const owner = node.ownerElement;
71
+ return `${owner === null ? '' : pathOf(owner)}/@${node.name}`;
72
+ }
73
+ if (node.nodeType === 3 || node.nodeType === 4) {
74
+ const parent = node.parentNode;
75
+ return `${parent === null ? '' : pathOf(parent)}/text()`;
76
+ }
77
+ if (node.nodeType !== 1)
78
+ return '';
79
+ const element = node;
80
+ const parent = element.parentNode;
81
+ if (parent === null || parent.nodeType !== 1)
82
+ return `/${element.nodeName}`;
83
+ let index = 0;
84
+ for (let sibling = parent.firstChild; sibling; sibling = sibling.nextSibling) {
85
+ if (sibling.nodeType === 1 && sibling.nodeName === element.nodeName) {
86
+ index++;
87
+ if (sibling === element)
88
+ break;
89
+ }
90
+ }
91
+ return `${pathOf(parent)}/${element.nodeName}[${index}]`;
92
+ }
93
+ function isElement(node) {
94
+ return typeof node === 'object' && node !== null && node.nodeType === 1;
95
+ }
@@ -16,6 +16,24 @@ export declare const COMMENT_REFERENCE_STYLE = "CommentReference";
16
16
  * Who a comment (and, with phase F, a tracked change) is by. `WordprocessingMLPackage.author`;
17
17
  * there is no signed-in user here, so the package carries the identity.
18
18
  */
19
+ /**
20
+ * What a comment is made of: the text, or the paragraphs of a comment that was edited rich
21
+ * (CR-002 section 28.3). A string splits on newlines, as it did before this option existed.
22
+ */
23
+ export type CommentContent = string | Element<wml.P>[];
24
+ /**
25
+ * Who made a comment and when, and its `w:id`, for a caller that knows better than the package does:
26
+ * in a co-editing room the author is the peer who made it and the date is when they made it
27
+ * (CR-002 section 28.3). Anything omitted falls back to what the package would have used.
28
+ */
29
+ export interface CommentOptions {
30
+ author?: Author | undefined;
31
+ /** Overrides the initials derived from the author's name. */
32
+ initials?: string | undefined;
33
+ date?: Date | undefined;
34
+ /** Checked unused across the comments part and the story's markers; a clash throws. */
35
+ id?: number | undefined;
36
+ }
19
37
  export interface Author {
20
38
  name: string;
21
39
  /** Word's `w:initials`; derived from the name when not given. */
@@ -69,7 +87,7 @@ export declare function commentPartsAccess(): CommentPartsAccess;
69
87
  /** How `Body`, `Paragraph` and `Range` reach the `Comment` views; registered by `Comment.mts`. */
70
88
  export interface CommentApi {
71
89
  commentsOf(scope: Body | Paragraph | Range): Promise<Comment[]>;
72
- insertComment(range: Range, text: string): Promise<Comment>;
90
+ insertComment(range: Range, content: CommentContent, options?: CommentOptions): Promise<Comment>;
73
91
  }
74
92
  export declare function setCommentApi(implementation: CommentApi): void;
75
93
  export declare function commentApi(): CommentApi;
@@ -4,14 +4,16 @@ 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
6
  export { List, ListItem, ListItemNotFoundError, NumberingFacade, listsOf, listLabelsOf, numberingEmulatorOf, levelOf, type ListLevelType, type ListNumbering, type ListBullet, type StartListOptions, type ListLabel, } from './List.mjs';
7
- export { type Author, type CommentParts, type CommentPartsAccess, type CommentsExtensible, type CommentMarker, COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
7
+ export { type Author, type CommentContent, type CommentOptions, 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';
10
10
  export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, writableWidthEmu, type ImageFormat, type ImageInfo, type InlinePictureOptions, type NewPicture } from './InlinePicture.mjs';
11
11
  export { ContentControl, collectControls, collectRunControls, W14_NS, W15_NS, type ContentControlType, type ContentControlForm, type ContentControlAppearance } from './ContentControl.mjs';
12
12
  export { contentOf, isFlatOpc, rewriteRelationshipIds, type OoxmlOptions } from './ooxml.mjs';
13
- export { type SearchOptions, searchPattern } from './search.mjs';
13
+ export { type SearchOptions, searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
14
14
  export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf, type TextSegment, type Located, type TextView, type TextViewOptions, type RevisionKind, type RevisionHolder } from './tree.mjs';
15
15
  export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext, type TrackedChangeType, type TrackedChangeTarget } from './TrackedChange.mjs';
16
- export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, } from './tracking.mjs';
16
+ export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, type TrackingScope, } from './tracking.mjs';
17
17
  export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS, type Element, type Wrapper, type WmlOptions, type Raw, type Interpolated, type RunOptions, type RunFormatting, type ParagraphOptions, type TableOptions, type CellOptions, type SdtKind, type SdtForm, type SdtOptions } from '@docx4j/generated-objects-ts/builders/wml';
18
+ export { StylesFacade, type StylePackageLike } from './stylesFacade.mjs';
19
+ export { bindSnapshot, boundsOf, pathOf, type Bound, type BoundSnapshot } from './binder.mjs';
@@ -12,10 +12,12 @@ export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
12
12
  export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, writableWidthEmu } from './InlinePicture.mjs';
13
13
  export { ContentControl, collectControls, collectRunControls, W14_NS, W15_NS } from './ContentControl.mjs';
14
14
  export { contentOf, isFlatOpc, rewriteRelationshipIds } from './ooxml.mjs';
15
- export { searchPattern } from './search.mjs';
15
+ export { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
16
16
  export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf } from './tree.mjs';
17
17
  // change tracking (CR-002 phase F)
18
18
  export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext } from './TrackedChange.mjs';
19
19
  export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, } from './tracking.mjs';
20
20
  // The objects package's builders, re-exported so that one import serves the content API and the tree it works on.
21
21
  export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS } from '@docx4j/generated-objects-ts/builders/wml';
22
+ export { StylesFacade } from './stylesFacade.mjs';
23
+ export { bindSnapshot, boundsOf, pathOf } from './binder.mjs';
@@ -1,12 +1,37 @@
1
- /** Office JS Word.SearchOptions, the subset honoured here. */
1
+ /** Office JS Word.SearchOptions, the subset honoured here, plus `matchRegExp` (CR-002 section 26). */
2
2
  export interface SearchOptions {
3
3
  matchCase?: boolean;
4
4
  matchWholeWord?: boolean;
5
5
  /** Word wildcards: `?` one character, `*` any run, `[a-z]`, `[!x]`, `<` / `>` word start and end, `@` one or more of the previous. */
6
6
  matchWildcards?: boolean;
7
+ /**
8
+ * The search text is an ECMAScript regular expression, compiled with `u` (extension beyond
9
+ * Office JS, as `replaceText` is; `Word.SearchOptions` has no such member, so it stays out of
10
+ * `Word.supported`). Never `m` or `s`: a match stays inside one paragraph, as every search here
11
+ * does (CR-002 sections 26 and 29). `matchWildcards` as well is an error - they are two
12
+ * languages for the same argument.
13
+ */
14
+ matchRegExp?: boolean;
7
15
  }
8
16
  /** A regular expression for a search text under the options; global so it can iterate matches. */
9
17
  export declare function searchPattern(text: string, options?: SearchOptions): RegExp;
10
18
  export declare function escapeRegExp(s: string): string;
11
- /** All [start, end) matches of a pattern in a text. */
19
+ /**
20
+ * All [start, end) matches of a pattern in a text. Kept as it is - it is a public export, and every
21
+ * caller that only wants spans would have to change - so the groups a replacement needs come from
22
+ * `matchesOf` instead (CR-002 section 26's review note, trap 1).
23
+ */
12
24
  export declare function findAll(text: string, pattern: RegExp): [number, number][];
25
+ /**
26
+ * Every match of a pattern in a text, with its capture groups: what `replaceText` needs to expand
27
+ * `$1` and Word's `\1`. An empty match advances by one rather than looping forever, as `findAll`
28
+ * has always done.
29
+ */
30
+ export declare function matchesOf(text: string, pattern: RegExp): RegExpExecArray[];
31
+ /**
32
+ * A replacement with its group references expanded against one match: `$1` to `$9`, `$&` and `$$`
33
+ * for a regular expression (JavaScript's `String.prototype.replace` rules), and Word's `\1` to
34
+ * `\9` for wildcards (CR-002 section 26). A reference to a group the pattern does not have expands
35
+ * to nothing, as both languages do.
36
+ */
37
+ export declare function expandReplacement(replacement: string, match: RegExpExecArray, options?: SearchOptions): string;
@@ -1,5 +1,19 @@
1
+ import { Docx4JException } from '../../opc/exceptions.mjs';
1
2
  /** A regular expression for a search text under the options; global so it can iterate matches. */
2
3
  export function searchPattern(text, options = {}) {
4
+ if (options.matchRegExp && options.matchWildcards) {
5
+ throw new RangeError('matchRegExp and matchWildcards are two languages for the same argument; choose one');
6
+ }
7
+ // The `u` flag goes on this branch ONLY. Under it several escapes that are merely redundant become
8
+ // syntax errors, and the other two branches write some: the wildcard `[...]` doubles backslashes
9
+ // and `escapeRegExp` escapes `-`. So it must not be moved to the one `new RegExp` below.
10
+ if (options.matchRegExp) {
11
+ // A user expression may have a top-level alternation, so `matchWholeWord` has to wrap it in a
12
+ // group: `(?<!\w)cat|dog(?!\w)` is two alternatives with one guard each and matches `dog` in
13
+ // `dogma`. A non-capturing group keeps the group numbers `$1` refers to.
14
+ const source = options.matchWholeWord ? `(?<![\\w])(?:${text})(?![\\w])` : text;
15
+ return new RegExp(source, options.matchCase ? 'gu' : 'giu');
16
+ }
3
17
  let source;
4
18
  if (options.matchWildcards) {
5
19
  source = '';
@@ -39,6 +53,14 @@ export function searchPattern(text, options = {}) {
39
53
  else if (c === '\\' && i + 1 < text.length) {
40
54
  source += '\\' + text[++i];
41
55
  }
56
+ else if (c === '(' || c === ')') {
57
+ // Word's wildcard grouping, which `\1` to `\9` in a replacement refer to (CR-002 section 26);
58
+ // a literal parenthesis is `\(`, taken by the escape case above. Before section 26 both were
59
+ // escaped to literals, so this changes what a pattern with them finds: `f(x)` matched the
60
+ // text "f(x)" and now matches "fx", as Word's wildcards do. An unbalanced one is refused
61
+ // below (CR-002 section 31).
62
+ source += c;
63
+ }
42
64
  else
43
65
  source += escapeRegExp(c);
44
66
  }
@@ -48,13 +70,32 @@ export function searchPattern(text, options = {}) {
48
70
  }
49
71
  if (options.matchWholeWord)
50
72
  source = `(?<![\\w])${source}(?![\\w])`;
51
- return new RegExp(source, options.matchCase ? 'g' : 'gi');
73
+ try {
74
+ return new RegExp(source, options.matchCase ? 'g' : 'gi');
75
+ }
76
+ catch (cause) {
77
+ // Only a wildcard pattern can fail here - a plain text is escaped whole - and the RegExp's own
78
+ // message quotes the translation, a pattern the caller never wrote.
79
+ throw new Docx4JException(`Not a valid wildcard pattern: ${text}`, { cause });
80
+ }
52
81
  }
53
82
  export function escapeRegExp(s) {
54
83
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
55
84
  }
56
- /** All [start, end) matches of a pattern in a text. */
85
+ /**
86
+ * All [start, end) matches of a pattern in a text. Kept as it is - it is a public export, and every
87
+ * caller that only wants spans would have to change - so the groups a replacement needs come from
88
+ * `matchesOf` instead (CR-002 section 26's review note, trap 1).
89
+ */
57
90
  export function findAll(text, pattern) {
91
+ return matchesOf(text, pattern).map((m) => [m.index, m.index + m[0].length]);
92
+ }
93
+ /**
94
+ * Every match of a pattern in a text, with its capture groups: what `replaceText` needs to expand
95
+ * `$1` and Word's `\1`. An empty match advances by one rather than looping forever, as `findAll`
96
+ * has always done.
97
+ */
98
+ export function matchesOf(text, pattern) {
58
99
  const out = [];
59
100
  pattern.lastIndex = 0;
60
101
  for (let m = pattern.exec(text); m !== null; m = pattern.exec(text)) {
@@ -62,7 +103,22 @@ export function findAll(text, pattern) {
62
103
  pattern.lastIndex++;
63
104
  continue;
64
105
  }
65
- out.push([m.index, m.index + m[0].length]);
106
+ out.push(m);
66
107
  }
67
108
  return out;
68
109
  }
110
+ /**
111
+ * A replacement with its group references expanded against one match: `$1` to `$9`, `$&` and `$$`
112
+ * for a regular expression (JavaScript's `String.prototype.replace` rules), and Word's `\1` to
113
+ * `\9` for wildcards (CR-002 section 26). A reference to a group the pattern does not have expands
114
+ * to nothing, as both languages do.
115
+ */
116
+ export function expandReplacement(replacement, match, options = {}) {
117
+ if (options.matchRegExp) {
118
+ return replacement.replace(/\$(\$|&|[1-9])/g, (_, token) => { var _a; return token === '$' ? '$' : token === '&' ? match[0] : (_a = match[Number(token)]) !== null && _a !== void 0 ? _a : ''; });
119
+ }
120
+ if (options.matchWildcards) {
121
+ return replacement.replace(/\\([1-9])/g, (_, digit) => { var _a; return (_a = match[Number(digit)]) !== null && _a !== void 0 ? _a : ''; });
122
+ }
123
+ return replacement;
124
+ }
@@ -0,0 +1,37 @@
1
+ import type { StyleDefinitionsPart, MainDocumentPart } from '../../parts/wml/index.mjs';
2
+ /** What the facade needs of its package; `WordprocessingMLPackage` satisfies it. */
3
+ export interface StylePackageLike {
4
+ getMainDocumentPart(): MainDocumentPart;
5
+ refreshPropertyResolver(): void;
6
+ /** Creates the styles part, with the defaults, when the document has none. */
7
+ ensureStyleDefinitionsPart(): StyleDefinitionsPart;
8
+ }
9
+ /**
10
+ * The document's style definitions as verbs that need no `Body`: `pkg.styles`.
11
+ *
12
+ * `ensure` is the one verb, and the point of it is the closure: a style is useless without what it
13
+ * is `w:basedOn` and `w:link`ed to, and a caller asking for `FootnoteText` means the four styles
14
+ * that make a footnote look like a footnote, not one.
15
+ */
16
+ export declare class StylesFacade {
17
+ private readonly pkg;
18
+ constructor(pkg: StylePackageLike);
19
+ /**
20
+ * Adds each style the document lacks, from the same defaults `createPackage()` writes, together
21
+ * with the styles it is `w:basedOn` and `w:link`ed to, recursively. Returns the ids actually
22
+ * added, in the order they were written, so a caller can tell whether it changed anything.
23
+ *
24
+ * A style already in the document is left exactly as it is - the document's own definition wins
25
+ * over the default, always, since a consumer must not silently restyle a document it was asked to
26
+ * add a footnote to. An id the defaults do not carry throws a `Docx4JException` naming it: the
27
+ * caller asked for a definition nothing here has, and inventing one would be worse than saying so.
28
+ * The whole closure is worked out before anything is written, so a call that throws has changed
29
+ * nothing (CR-002 section 31).
30
+ *
31
+ * A document that already has every id keeps its styles part **byte for byte**: the part is not
32
+ * unmarshalled at all when its XML names them, the check `ensureCommentStyles` uses, and when that
33
+ * text check misses (another prefix, single quotes) it is read privately rather than unmarshalled,
34
+ * so the part is still written back from its source unless something is added.
35
+ */
36
+ ensure(ids: string | readonly string[]): Promise<string[]>;
37
+ }
@@ -0,0 +1,114 @@
1
+ import { deepCopy, unmarshalNode } from '@docx4j/generated-objects-ts';
2
+ import { linkParents } from './tree.mjs';
3
+ import { parseXml } from '../../xml/dom.mjs';
4
+ import { DEFAULT_STYLES_XML } from '../../parts/wml/defaultStyles.mjs';
5
+ import { SPLICEABLE_STYLES_XML } from '../../parts/wml/spliceableStyles.mjs';
6
+ import { Docx4JException } from '../../opc/exceptions.mjs';
7
+ /**
8
+ * Where a definition is looked up, unmarshalled once and never handed out (a copy is spliced in):
9
+ * the defaults `createPackage()` writes, and then the nine styles docx4j's own `styles.xml` carries
10
+ * **commented out** - which is why a created document has no `FootnoteText` although the file
11
+ * appears to contain one, and why this facade exists (CR-002 section 22.2).
12
+ */
13
+ let sources;
14
+ function styleSources() {
15
+ return (sources !== null && sources !== void 0 ? sources : (sources = (async () => {
16
+ const read = async (xml) => { var _a; return (_a = (await unmarshalNode(parseXml(xml))).value.style) !== null && _a !== void 0 ? _a : []; };
17
+ const defaults = await read(DEFAULT_STYLES_XML);
18
+ return { defaults, all: [...defaults, ...(await read(SPLICEABLE_STYLES_XML))] };
19
+ })()));
20
+ }
21
+ /**
22
+ * The document's style definitions as verbs that need no `Body`: `pkg.styles`.
23
+ *
24
+ * `ensure` is the one verb, and the point of it is the closure: a style is useless without what it
25
+ * is `w:basedOn` and `w:link`ed to, and a caller asking for `FootnoteText` means the four styles
26
+ * that make a footnote look like a footnote, not one.
27
+ */
28
+ export class StylesFacade {
29
+ constructor(pkg) {
30
+ this.pkg = pkg;
31
+ }
32
+ /**
33
+ * Adds each style the document lacks, from the same defaults `createPackage()` writes, together
34
+ * with the styles it is `w:basedOn` and `w:link`ed to, recursively. Returns the ids actually
35
+ * added, in the order they were written, so a caller can tell whether it changed anything.
36
+ *
37
+ * A style already in the document is left exactly as it is - the document's own definition wins
38
+ * over the default, always, since a consumer must not silently restyle a document it was asked to
39
+ * add a footnote to. An id the defaults do not carry throws a `Docx4JException` naming it: the
40
+ * caller asked for a definition nothing here has, and inventing one would be worse than saying so.
41
+ * The whole closure is worked out before anything is written, so a call that throws has changed
42
+ * nothing (CR-002 section 31).
43
+ *
44
+ * A document that already has every id keeps its styles part **byte for byte**: the part is not
45
+ * unmarshalled at all when its XML names them, the check `ensureCommentStyles` uses, and when that
46
+ * text check misses (another prefix, single quotes) it is read privately rather than unmarshalled,
47
+ * so the part is still written back from its source unless something is added.
48
+ */
49
+ async ensure(ids) {
50
+ var _a, _b;
51
+ const wanted = typeof ids === 'string' ? [ids] : [...ids];
52
+ for (const id of wanted) {
53
+ if (typeof id !== 'string' || id === '')
54
+ throw new Docx4JException(`Not a styleId: ${String(id)}`);
55
+ }
56
+ if (wanted.length === 0)
57
+ return [];
58
+ const existing = this.pkg.getMainDocumentPart().styleDefinitionsPart;
59
+ // The cheap path: an untouched part whose XML names them all is left alone, so a document that
60
+ // has them keeps its styles part byte for byte. The comments must go first - docx4j's own
61
+ // default styles carry nine definitions COMMENTED OUT, so a plain `includes` finds
62
+ // `w:styleId="FootnoteText"` in a document that does not have the style and this would answer
63
+ // "nothing to do" for exactly the documents the facade exists for.
64
+ if (existing !== undefined && !existing.isUnmarshalled) {
65
+ const xml = (await existing.getXml()).replace(/<!--[\s\S]*?-->/g, '');
66
+ if (wanted.every((id) => xml.includes(`w:styleId="${id}"`)))
67
+ return [];
68
+ }
69
+ // What the document holds now, read without unmarshalling; a part created below holds the defaults.
70
+ const source = await styleSources();
71
+ const present = existing === undefined ? source.defaults : ((_a = (await existing.readContents()).style) !== null && _a !== void 0 ? _a : []);
72
+ const plan = closureOf(wanted, present, source.all);
73
+ // a document with no styles part gets the defaults, whether or not anything else is needed
74
+ const part = existing !== null && existing !== void 0 ? existing : this.pkg.ensureStyleDefinitionsPart();
75
+ if (plan.length === 0)
76
+ return [];
77
+ const styles = await part.getContents();
78
+ const list = ((_b = styles.style) !== null && _b !== void 0 ? _b : (styles.style = []));
79
+ for (const template of plan) {
80
+ const style = deepCopy(template);
81
+ list.push(style);
82
+ linkParents(style, styles);
83
+ }
84
+ this.pkg.refreshPropertyResolver();
85
+ return plan.map((style) => style.styleId);
86
+ }
87
+ }
88
+ /**
89
+ * The definitions to splice, in the order they are written: each wanted id the document lacks,
90
+ * after what it is `w:basedOn` and `w:link`ed to, recursively. Throws for an id nothing carries.
91
+ */
92
+ function closureOf(wanted, present, source) {
93
+ const plan = [];
94
+ const has = (id) => present.some((s) => s.styleId === id) || plan.some((s) => s.styleId === id);
95
+ const add = (id, seen) => {
96
+ var _a, _b;
97
+ if (has(id) || seen.has(id))
98
+ return;
99
+ seen.add(id);
100
+ const template = source.find((s) => s.styleId === id);
101
+ if (template === undefined) {
102
+ throw new Docx4JException(`No definition to splice for the style ${id}: neither docx4j's default styles nor the nine it comments out carry one. Define it yourself on the styles part.`);
103
+ }
104
+ // what it is based on and linked to must be there too, and before it reads better in the part
105
+ for (const dependency of [(_a = template.basedOn) === null || _a === void 0 ? void 0 : _a.val, (_b = template.link) === null || _b === void 0 ? void 0 : _b.val]) {
106
+ if (typeof dependency === 'string')
107
+ add(dependency, seen);
108
+ }
109
+ plan.push(template);
110
+ };
111
+ for (const id of wanted)
112
+ add(id, new Set());
113
+ return plan;
114
+ }