@docx4j/core-ts 0.1.4 → 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 (48) 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 +80 -1
  7. package/dist/model/content/List.mjs +140 -22
  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 +29 -2
  11. package/dist/model/content/Range.mjs +137 -7
  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 +6 -4
  16. package/dist/model/content/index.mjs +4 -2
  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/office-js/supported.generated.mjs +1 -0
  33. package/dist/opc/exceptions.d.mts +25 -0
  34. package/dist/opc/exceptions.mjs +20 -0
  35. package/dist/opc/index.d.mts +1 -1
  36. package/dist/opc/index.mjs +1 -1
  37. package/dist/opc/mce/understood.mjs +37 -8
  38. package/dist/packages/WordprocessingMLPackage.d.mts +101 -2
  39. package/dist/packages/WordprocessingMLPackage.mjs +155 -1
  40. package/dist/parts/XmlPart.d.mts +44 -0
  41. package/dist/parts/XmlPart.mjs +86 -3
  42. package/dist/parts/sml/index.d.mts +9 -8
  43. package/dist/parts/sml/index.mjs +23 -19
  44. package/dist/parts/wml/index.d.mts +10 -2
  45. package/dist/parts/wml/index.mjs +11 -3
  46. package/dist/parts/wml/spliceableStyles.d.mts +1 -0
  47. package/dist/parts/wml/spliceableStyles.mjs +122 -0
  48. package/package.json +2 -2
@@ -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;
@@ -3,15 +3,17 @@ 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';
7
- export { type Author, type CommentParts, type CommentPartsAccess, type CommentsExtensible, type CommentMarker, COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.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
+ 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';
@@ -5,17 +5,19 @@ 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';
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
+ }
@@ -3,6 +3,19 @@ import { type Element, type RevisionHolder } from './tree.mjs';
3
3
  import type { Author } from './comments.mjs';
4
4
  /** Office JS `Word.ChangeTrackingMode`. */
5
5
  export type ChangeTrackingMode = 'Off' | 'TrackAll' | 'TrackMineOnly';
6
+ /**
7
+ * `w:id` on these is the one annotation id space (ECMA-376 17.13.5.4): revisions, bookmarks,
8
+ * comment range marks and permissions share it. `w:comment/@w:id` (docx4j `Comments.Comment`,
9
+ * which also extends `CTMarkup`) is a different space and is excluded, so that phase G's
10
+ * comment ids and phase F's revision ids never have to agree.
11
+ */
12
+ export declare const MARKUP_TYPES: ReadonlySet<string>;
13
+ /**
14
+ * The highest annotation id in a tree (`MARKUP_TYPES` only, the one `w:id` space of ECMA-376
15
+ * 17.13.5.4). Shared so that the tracker's scan of the unmarshalled parts and the package's
16
+ * private read of the rest cannot drift apart (CR-002 section 28.2).
17
+ */
18
+ export declare function highestAnnotationId(root: object, from?: number): number;
6
19
  /** What a package offers so that its edits can be tracked; `WordprocessingMLPackage` implements it. */
7
20
  export interface TrackingHost {
8
21
  /**
@@ -15,9 +28,43 @@ export interface TrackingHost {
15
28
  readonly trackedChangeDate?: Date | undefined;
16
29
  /** The trees to scan for the highest annotation id in use: the parts already unmarshalled. */
17
30
  markupRoots(): object[];
31
+ /**
32
+ * A floor for the annotation id, above every id in the parts that were **not** unmarshalled
33
+ * (`seedAnnotationIds()` reads them privately). 0 until it is seeded; `markupRoots()` covers the
34
+ * rest, so the next id is one above the greater of the two (CR-002 section 28.2).
35
+ */
36
+ readonly annotationIdFloor: number;
37
+ /**
38
+ * The last annotation id issued in the document, or `undefined` until the first, when the tracker
39
+ * scans for the highest in use. The host holds it rather than the tracker because a document has
40
+ * one id space and may have several trackers over its life - `withTracking` can make one with
41
+ * another mode - and a tracker with a counter of its own reissues the ids another one gave out
42
+ * (CR-002 section 31).
43
+ */
44
+ annotationIdCounter: number | undefined;
45
+ }
46
+ /**
47
+ * What `WordprocessingMLPackage.withTracking` records for the duration of one call: who, when, and
48
+ * which mode (`TrackAll` unless said otherwise). The document's own `w:trackRevisions` is not
49
+ * touched - an editor's agent records its edits as itself in a room whose setting belongs to the
50
+ * document, not to the caller (CR-002 section 28.1).
51
+ */
52
+ export interface TrackingScope {
53
+ author: Author;
54
+ /** The `w:date` for every revision in the call; `undefined` means now, to the minute. */
55
+ date?: Date | undefined;
56
+ /** `TrackAll` unless given; `Off` runs `fn` with tracking suppressed for the call. */
57
+ mode?: ChangeTrackingMode | undefined;
18
58
  }
19
59
  /** The tracker of a package whose mode is not `Off`; `undefined` when it is (duck typed, no import cycle). */
20
60
  export declare function trackerOf(pkg: unknown): ChangeTracker | undefined;
61
+ /**
62
+ * Now, with the seconds and milliseconds dropped, which is what Word stamps a revision with.
63
+ * Not cosmetic: Word groups adjacent revisions by author **and** timestamp, so a per-second stamp
64
+ * makes one change of every keystroke run and an editor's typing unreviewable (CR-002 section 28.2).
65
+ * A caller that sets `trackedChangeDate` gets exactly the date it asked for, truncated or not.
66
+ */
67
+ export declare function nowToTheMinute(): Date;
21
68
  /** An `XmlCalendar` in UTC, as Word writes `w:date` (an xsd:dateTime ending in `Z`). */
22
69
  export declare function calendarOf(date: Date): wml.XmlCalendar;
23
70
  /** The `Date` of a `w:date`, or undefined when there is none. */
@@ -33,11 +80,19 @@ export declare function dateOf(cal: wml.XmlCalendar | undefined): Date | undefin
33
80
  export declare class ChangeTracker {
34
81
  readonly host: TrackingHost;
35
82
  readonly mode: ChangeTrackingMode;
36
- private counter;
37
83
  constructor(host: TrackingHost, mode: ChangeTrackingMode);
38
84
  /** The `w:author` a new revision carries: the package author's name. */
39
85
  get author(): string;
40
- /** The next annotation id: one above the highest in use in the parts already unmarshalled. */
86
+ /**
87
+ * The next annotation id: one above the highest in use anywhere in the document - the parts
88
+ * already unmarshalled, scanned here, and the rest through the host's `annotationIdFloor`,
89
+ * which `seedAnnotationIds()` fills from their bytes without unmarshalling them (CR-002
90
+ * section 28.2; before that this saw only the unmarshalled parts, so a revision in an unread
91
+ * footnote or comment part did not raise the counter).
92
+ *
93
+ * The scan runs once per document, not per tracker: the counter is the host's. The floor is
94
+ * consulted on every call, so a `seedAnnotationIds()` awaited after the first id still counts.
95
+ */
41
96
  nextId(): number;
42
97
  /** The `w:id`, `w:author` and `w:date` a new revision carries. */
43
98
  markup(): {
@@ -9,7 +9,7 @@ import { W_NS, linkParents, walk, typeNameOf, revisionKindOf } from './tree.mjs'
9
9
  * which also extends `CTMarkup`) is a different space and is excluded, so that phase G's
10
10
  * comment ids and phase F's revision ids never have to agree.
11
11
  */
12
- const MARKUP_TYPES = new Set([
12
+ export const MARKUP_TYPES = new Set([
13
13
  'org_docx4j_wml.CTMarkup', 'org_docx4j_wml.CTBookmark', 'org_docx4j_wml.CTBookmarkRange',
14
14
  'org_docx4j_wml.CTCellMergeTrackChange', 'org_docx4j_wml.CTMarkupRange', 'org_docx4j_wml.CTMathRunTrackChange',
15
15
  'org_docx4j_wml.CTMoveBookmark', 'org_docx4j_wml.CTMoveFromRangeEnd', 'org_docx4j_wml.CTMoveToRangeEnd',
@@ -19,10 +19,36 @@ const MARKUP_TYPES = new Set([
19
19
  'org_docx4j_wml.CTTrackChangeNumbering', 'org_docx4j_wml.CTTrackChangeRange',
20
20
  'org_docx4j_wml.ParaRPrChange', 'org_docx4j_wml.RunDel', 'org_docx4j_wml.RunIns', 'org_docx4j_wml.RunTrackChange',
21
21
  ]);
22
+ /**
23
+ * The highest annotation id in a tree (`MARKUP_TYPES` only, the one `w:id` space of ECMA-376
24
+ * 17.13.5.4). Shared so that the tracker's scan of the unmarshalled parts and the package's
25
+ * private read of the rest cannot drift apart (CR-002 section 28.2).
26
+ */
27
+ export function highestAnnotationId(root, from = 0) {
28
+ let highest = from;
29
+ walk(root, (value) => {
30
+ const tn = value.TYPE_NAME;
31
+ const id = value.id;
32
+ if (tn !== undefined && MARKUP_TYPES.has(tn) && typeof id === 'number' && id > highest)
33
+ highest = id;
34
+ });
35
+ return highest;
36
+ }
22
37
  /** The tracker of a package whose mode is not `Off`; `undefined` when it is (duck typed, no import cycle). */
23
38
  export function trackerOf(pkg) {
24
39
  return pkg === null || pkg === void 0 ? void 0 : pkg.changeTracker;
25
40
  }
41
+ /**
42
+ * Now, with the seconds and milliseconds dropped, which is what Word stamps a revision with.
43
+ * Not cosmetic: Word groups adjacent revisions by author **and** timestamp, so a per-second stamp
44
+ * makes one change of every keystroke run and an editor's typing unreviewable (CR-002 section 28.2).
45
+ * A caller that sets `trackedChangeDate` gets exactly the date it asked for, truncated or not.
46
+ */
47
+ export function nowToTheMinute() {
48
+ const now = new Date();
49
+ now.setUTCSeconds(0, 0);
50
+ return now;
51
+ }
26
52
  /** An `XmlCalendar` in UTC, as Word writes `w:date` (an xsd:dateTime ending in `Z`). */
27
53
  export function calendarOf(date) {
28
54
  return {
@@ -58,26 +84,32 @@ export class ChangeTracker {
58
84
  get author() {
59
85
  return this.host.author.name;
60
86
  }
61
- /** The next annotation id: one above the highest in use in the parts already unmarshalled. */
87
+ /**
88
+ * The next annotation id: one above the highest in use anywhere in the document - the parts
89
+ * already unmarshalled, scanned here, and the rest through the host's `annotationIdFloor`,
90
+ * which `seedAnnotationIds()` fills from their bytes without unmarshalling them (CR-002
91
+ * section 28.2; before that this saw only the unmarshalled parts, so a revision in an unread
92
+ * footnote or comment part did not raise the counter).
93
+ *
94
+ * The scan runs once per document, not per tracker: the counter is the host's. The floor is
95
+ * consulted on every call, so a `seedAnnotationIds()` awaited after the first id still counts.
96
+ */
62
97
  nextId() {
63
- if (this.counter === undefined) {
64
- let highest = 0;
65
- for (const root of this.host.markupRoots()) {
66
- walk(root, (value) => {
67
- const tn = value.TYPE_NAME;
68
- const id = value.id;
69
- if (tn !== undefined && MARKUP_TYPES.has(tn) && typeof id === 'number' && id > highest)
70
- highest = id;
71
- });
72
- }
73
- this.counter = highest;
98
+ const host = this.host;
99
+ let counter = host.annotationIdCounter;
100
+ if (counter === undefined) {
101
+ counter = 0;
102
+ for (const root of host.markupRoots())
103
+ counter = highestAnnotationId(root, counter);
74
104
  }
75
- return ++this.counter;
105
+ counter = Math.max(counter, host.annotationIdFloor) + 1;
106
+ host.annotationIdCounter = counter;
107
+ return counter;
76
108
  }
77
109
  /** The `w:id`, `w:author` and `w:date` a new revision carries. */
78
110
  markup() {
79
111
  var _a;
80
- return { id: this.nextId(), author: this.author, date: calendarOf((_a = this.host.trackedChangeDate) !== null && _a !== void 0 ? _a : new Date()) };
112
+ return { id: this.nextId(), author: this.author, date: calendarOf((_a = this.host.trackedChangeDate) !== null && _a !== void 0 ? _a : nowToTheMinute()) };
81
113
  }
82
114
  /** A `w:ins` / `w:del` attribute set: the id, this package's author and the date. */
83
115
  trackChange() {