@stll/folio-core 0.47.4 → 0.47.6

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 (50) hide show
  1. package/dist/compare/inline-atoms.js +218 -31
  2. package/dist/compare/section-boundary-properties.js +1 -0
  3. package/dist/content-controls/mutateContentControls.js +1 -3
  4. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  5. package/dist/controller/headerFooterEditorManager.js +2 -1
  6. package/dist/controller/noteEditorManager.d.ts +2 -1
  7. package/dist/controller/noteEditorManager.js +2 -1
  8. package/dist/docx/commentRangeIntegrity.js +7 -0
  9. package/dist/docx/documentParser.js +55 -3
  10. package/dist/docx/inlineWrapperContent.d.ts +18 -16
  11. package/dist/docx/inlineWrapperContent.js +18 -16
  12. package/dist/docx/normalizeBaseDirection.js +4 -0
  13. package/dist/docx/rezip.js +1 -0
  14. package/dist/docx/selectiveSave.js +11 -2
  15. package/dist/docx/serializer/documentSerializer.js +16 -1
  16. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  17. package/dist/docx/serializer/tableSerializer.js +5 -2
  18. package/dist/docx/server/createBilingualDocument.js +23 -14
  19. package/dist/docx/tableParser.js +101 -75
  20. package/dist/markdown/renderRuns.js +4 -0
  21. package/dist/markdown/renderTable.js +46 -12
  22. package/dist/prosemirror/attrs/index.d.ts +3 -2
  23. package/dist/prosemirror/attrs/index.js +85 -4
  24. package/dist/prosemirror/commands/comments.js +61 -3
  25. package/dist/prosemirror/commentReferenceAttrs.js +2 -2
  26. package/dist/prosemirror/commentReferenceIntegrity.d.ts +7 -3
  27. package/dist/prosemirror/commentReferenceIntegrity.js +9 -1
  28. package/dist/prosemirror/conversion/fromProseDoc.js +177 -51
  29. package/dist/prosemirror/conversion/toProseDoc.js +108 -55
  30. package/dist/prosemirror/extensions/markRegistry.d.ts +5 -8
  31. package/dist/prosemirror/extensions/markRegistry.js +5 -8
  32. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +13 -6
  33. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +24 -6
  34. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +2 -1
  35. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +32 -7
  36. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +9 -5
  37. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -2
  38. package/dist/prosemirror/runIdentity.d.ts +5 -5
  39. package/dist/prosemirror/runIdentity.js +5 -5
  40. package/dist/prosemirror/schema/index.d.ts +2 -2
  41. package/dist/prosemirror/schema/marks.d.ts +28 -2
  42. package/dist/prosemirror/schema/nodes.d.ts +6 -4
  43. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  44. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  45. package/dist/prosemirror/utils/extractTrackedChanges.js +21 -6
  46. package/dist/prosemirror/validation.js +48 -7
  47. package/dist/types/content.d.ts +2 -2
  48. package/dist/types/index.d.ts +3 -1
  49. package/dist/utils/mergeDocumentContent.js +1 -7
  50. package/package.json +2 -2
@@ -44,13 +44,10 @@ type SchemaMarkName = keyof typeof MARK_EXTENSIONS;
44
44
  * Five of these marks are containers on the save leg as well, and their
45
45
  * relative order is that leg's rather than a choice made here.
46
46
  * `extractParagraphContent` opens a comment range around everything a leaf
47
- * produces (it flushes the open run and hyperlink and drops the tracked wrapper
48
- * before pushing the marker: `fromProseDoc.ts:2359-2369`), then the revision
49
- * wrapper (`fromProseDoc.ts:2530`), then the wrapper stack inside that revision
50
- * (`nestInlineWrapperGroups`, `fromProseDoc.ts:2200`), then the hyperlink and
51
- * the runs it holds (`fromProseDoc.ts:2567`). So the save writes
52
- * `w:commentRangeStart … w:ins > w:bdo > w:hyperlink > w:r`, and the editor DOM
53
- * nests `span.docx-comment > span.docx-insertion > bdo > a`.
47
+ * produces. For an editor-created revision, the existing wrapper stack owns
48
+ * the change, followed by the hyperlink and the runs it holds. So the save
49
+ * writes `w:commentRangeStart … w:bdo > w:ins > w:hyperlink > w:r`, and the
50
+ * editor DOM nests `span.docx-comment > bdo > span.docx-insertion > a`.
54
51
  * `markNestingOrder.test.ts` holds the two legs to each other.
55
52
  *
56
53
  * `runIdentity` is the fifth: `w:r` is the element it names, and it is the
@@ -60,6 +57,6 @@ type SchemaMarkName = keyof typeof MARK_EXTENSIONS;
60
57
  * own in OOXML, so the save leg ranks it against nothing and its place here is
61
58
  * a presentation decision.
62
59
  */
63
- declare const MARK_NESTING_ORDER: readonly ["bold", "italic", "underline", "strike", "textColor", "runShading", "highlight", "fontSize", "fontFamily", "language", "superscript", "subscript", "allCaps", "smallCaps", "footnoteRef", "characterSpacing", "emboss", "imprint", "hidden", "textShadow", "emphasisMark", "textOutline", "rtl", "textEffect", "runFormattingOverride", "characterStyle", "comment", "insertion", "deletion", "inlineWrapper", "hyperlink", "runPropertyChange", "runIdentity"];
60
+ declare const MARK_NESTING_ORDER: readonly ["bold", "italic", "underline", "strike", "textColor", "runShading", "highlight", "fontSize", "fontFamily", "language", "superscript", "subscript", "allCaps", "smallCaps", "footnoteRef", "characterSpacing", "emboss", "imprint", "hidden", "textShadow", "emphasisMark", "textOutline", "rtl", "textEffect", "runFormattingOverride", "characterStyle", "comment", "inlineWrapper", "insertion", "deletion", "hyperlink", "runPropertyChange", "runIdentity"];
64
61
  //#endregion
65
62
  export { MARK_EXTENSIONS, MARK_NESTING_ORDER, SchemaMarkName };
@@ -83,13 +83,10 @@ const markNestingOrder = (names) => names;
83
83
  * Five of these marks are containers on the save leg as well, and their
84
84
  * relative order is that leg's rather than a choice made here.
85
85
  * `extractParagraphContent` opens a comment range around everything a leaf
86
- * produces (it flushes the open run and hyperlink and drops the tracked wrapper
87
- * before pushing the marker: `fromProseDoc.ts:2359-2369`), then the revision
88
- * wrapper (`fromProseDoc.ts:2530`), then the wrapper stack inside that revision
89
- * (`nestInlineWrapperGroups`, `fromProseDoc.ts:2200`), then the hyperlink and
90
- * the runs it holds (`fromProseDoc.ts:2567`). So the save writes
91
- * `w:commentRangeStart … w:ins > w:bdo > w:hyperlink > w:r`, and the editor DOM
92
- * nests `span.docx-comment > span.docx-insertion > bdo > a`.
86
+ * produces. For an editor-created revision, the existing wrapper stack owns
87
+ * the change, followed by the hyperlink and the runs it holds. So the save
88
+ * writes `w:commentRangeStart … w:bdo > w:ins > w:hyperlink > w:r`, and the
89
+ * editor DOM nests `span.docx-comment > bdo > span.docx-insertion > a`.
93
90
  * `markNestingOrder.test.ts` holds the two legs to each other.
94
91
  *
95
92
  * `runIdentity` is the fifth: `w:r` is the element it names, and it is the
@@ -127,9 +124,9 @@ const MARK_NESTING_ORDER = markNestingOrder([
127
124
  "runFormattingOverride",
128
125
  "characterStyle",
129
126
  "comment",
127
+ "inlineWrapper",
130
128
  "insertion",
131
129
  "deletion",
132
- "inlineWrapper",
133
130
  "hyperlink",
134
131
  "runPropertyChange",
135
132
  "runIdentity"
@@ -7,6 +7,7 @@ import { panic } from "better-result";
7
7
  /**
8
8
  * Hyperlink Mark Extension
9
9
  */
10
+ const DOCX_HYPERLINK_INDEX_ATTRIBUTE = "data-docx-hyperlink-index";
10
11
  /**
11
12
  * Sanitize an href arriving from (or being emitted to) the DOM: pasted HTML,
12
13
  * clipboard content, or a mark attr already stored on the document. Only
@@ -90,19 +91,25 @@ const HyperlinkExtension = createMarkExtension({
90
91
  inclusive: false,
91
92
  parseDOM: [{
92
93
  tag: "a[href]",
93
- getAttrs: (dom) => ({
94
- href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
95
- tooltip: dom.getAttribute("title") ?? void 0,
96
- target: dom.getAttribute("target") ?? void 0
97
- })
94
+ getAttrs: (dom) => {
95
+ const index = dom.getAttribute(DOCX_HYPERLINK_INDEX_ATTRIBUTE);
96
+ const docxHyperlinkIndex = index !== null && /^\d+$/u.test(index) && Number.isSafeInteger(Number(index)) ? Number(index) : void 0;
97
+ return {
98
+ href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
99
+ tooltip: dom.getAttribute("title") ?? void 0,
100
+ target: dom.getAttribute("target") ?? void 0,
101
+ ...docxHyperlinkIndex === void 0 ? {} : { _docxHyperlinkIndex: docxHyperlinkIndex }
102
+ };
103
+ }
98
104
  }],
99
105
  toDOM(mark) {
100
- const { href, target, tooltip } = expectHyperlinkMarkAttrs(mark);
106
+ const { href, target, tooltip, _docxHyperlinkIndex } = expectHyperlinkMarkAttrs(mark);
101
107
  const domAttrs = {
102
108
  href: sanitizeStoredHref(href),
103
109
  ...anchorTargetAttrs(target)
104
110
  };
105
111
  if (tooltip) domAttrs["title"] = tooltip;
112
+ if (typeof _docxHyperlinkIndex === "number") domAttrs[DOCX_HYPERLINK_INDEX_ATTRIBUTE] = String(_docxHyperlinkIndex);
106
113
  return [
107
114
  "a",
108
115
  domAttrs,
@@ -16,6 +16,7 @@ import { panic } from "better-result";
16
16
  * extend the wrapper over text the author wrote outside it.
17
17
  */
18
18
  const INLINE_WRAPPER_MARK_NAME = "inlineWrapper";
19
+ const INLINE_WRAPPER_HYPERLINK_ORIGIN_ATTRIBUTE = "data-inline-wrapper-hyperlink-origin";
19
20
  /** What a tagged layer's element name is spelled as on the rendered span. */
20
21
  const TAGGED_ELEMENT_ATTRIBUTES = {
21
22
  smartTag: "data-smart-tag-element",
@@ -54,22 +55,39 @@ const InlineWrapperExtension = createMarkExtension({
54
55
  name: INLINE_WRAPPER_MARK_NAME,
55
56
  schemaMarkName: INLINE_WRAPPER_MARK_NAME,
56
57
  markSpec: {
57
- attrs: {
58
- /** Outermost first. `null` is no wrapper, which is the absence of the mark. */
59
- stack: { default: null } },
58
+ attrs: {
59
+ /** Outermost first. `null` is no wrapper, which is the absence of the mark. */
60
+ stack: { default: null },
61
+ _docxHyperlinkIndex: { default: null },
62
+ _docxInsideHyperlinkStackStart: { default: null }
63
+ },
60
64
  inclusive: false,
61
65
  parseDOM: [{
62
66
  tag: `[${INLINE_WRAPPER_STACK_ATTRIBUTE}]`,
63
67
  getAttrs(dom) {
64
68
  const stack = parseInlineWrapperStack(dom.getAttribute(INLINE_WRAPPER_STACK_ATTRIBUTE));
65
- return stack === null ? false : { stack };
69
+ if (stack === null) return false;
70
+ const origin = dom.getAttribute(INLINE_WRAPPER_HYPERLINK_ORIGIN_ATTRIBUTE);
71
+ if (origin === null) return { stack };
72
+ const match = /^(\d+):(\d+)$/u.exec(origin);
73
+ if (match === null) return false;
74
+ const hyperlinkIndex = Number(match[1]);
75
+ const stackStart = Number(match[2]);
76
+ if (!Number.isSafeInteger(hyperlinkIndex) || !Number.isSafeInteger(stackStart) || stackStart >= stack.length) return false;
77
+ return {
78
+ stack,
79
+ _docxHyperlinkIndex: hyperlinkIndex,
80
+ _docxInsideHyperlinkStackStart: stackStart
81
+ };
66
82
  }
67
83
  }],
68
84
  toDOM(mark) {
69
- const { stack } = expectInlineWrapperMarkAttrs(mark);
85
+ const { stack, _docxHyperlinkIndex, _docxInsideHyperlinkStackStart } = expectInlineWrapperMarkAttrs(mark);
70
86
  const innermost = stack.at(-1);
71
87
  if (innermost === void 0) panic("An inline wrapper mark carries at least one layer");
72
- return layerElement(innermost, { [INLINE_WRAPPER_STACK_ATTRIBUTE]: serializeInlineWrapperStack(stack) });
88
+ const attributes = { [INLINE_WRAPPER_STACK_ATTRIBUTE]: serializeInlineWrapperStack(stack) };
89
+ if (typeof _docxHyperlinkIndex === "number" && typeof _docxInsideHyperlinkStackStart === "number") attributes[INLINE_WRAPPER_HYPERLINK_ORIGIN_ATTRIBUTE] = `${_docxHyperlinkIndex}:${_docxInsideHyperlinkStackStart}`;
90
+ return layerElement(innermost, attributes);
73
91
  }
74
92
  }
75
93
  });
@@ -105,7 +105,8 @@ const RunIdentityExtension = createMarkExtension({
105
105
  attrs: {
106
106
  id: {},
107
107
  preservedAttributes: { default: null },
108
- preserved: { default: null }
108
+ preserved: { default: null },
109
+ emptyFormatting: { default: null }
109
110
  },
110
111
  inclusive: false,
111
112
  parseDOM: [{
@@ -1,5 +1,5 @@
1
1
  import { AUTHOR_COLORS, getAuthorColorIdx } from "../../../utils/authorColors.js";
2
- import { expectTrackedChangeMarkAttrs } from "../../attrs/index.js";
2
+ import { expectTrackedChangeMarkAttrs, isTrackedRevisionAncestorArray } from "../../attrs/index.js";
3
3
  import { createMarkExtension } from "../create.js";
4
4
  //#region src/prosemirror/extensions/marks/TrackedChangeExtensions.ts
5
5
  /**
@@ -31,6 +31,15 @@ const SUGGESTION_TINT = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, t
31
31
  const SUGGESTION_TINT_LAYER = `linear-gradient(${SUGGESTION_TINT}, ${SUGGESTION_TINT})`;
32
32
  const suggestedInsertionStyle = () => `color: ${SUGGESTION_COLOR}; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: ${SUGGESTION_COLOR}; background-image: ${SUGGESTION_TINT_LAYER};`;
33
33
  const suggestedDeletionStyle = () => `color: ${SUGGESTION_COLOR}; text-decoration: line-through; text-decoration-style: dotted; text-decoration-color: ${SUGGESTION_COLOR}; background-image: ${SUGGESTION_TINT_LAYER};`;
34
+ const revisionAncestorsFromDom = (serialized) => {
35
+ if (serialized === void 0) return null;
36
+ try {
37
+ const value = JSON.parse(serialized);
38
+ return isTrackedRevisionAncestorArray(value) ? value : false;
39
+ } catch {
40
+ return false;
41
+ }
42
+ };
34
43
  /**
35
44
  * Insertion mark — text added in tracked changes
36
45
  * Renders with per-author colored underline.
@@ -47,21 +56,27 @@ const InsertionExtension = createMarkExtension({
47
56
  moveKind: { default: null },
48
57
  initials: { default: null },
49
58
  provenance: { default: "user" },
50
- suggestionId: { default: null }
59
+ suggestionId: { default: null },
60
+ _docxOuterWrapperCount: { default: null },
61
+ _docxRevisionAncestors: { default: null }
51
62
  },
52
63
  inclusive: false,
53
64
  parseDOM: [{
54
65
  tag: "span.docx-insertion",
55
66
  getAttrs(dom) {
67
+ const ancestors = revisionAncestorsFromDom(dom.dataset["revisionAncestors"]);
68
+ if (ancestors === false) return false;
56
69
  return {
57
70
  revisionId: Number.parseInt(dom.dataset["revisionId"] ?? "0", 10),
58
71
  author: dom.dataset["author"] ?? "",
59
- date: dom.dataset["date"] ?? null
72
+ date: dom.dataset["date"] ?? null,
73
+ _docxOuterWrapperCount: dom.dataset["outerWrapperCount"] ? Number(dom.dataset["outerWrapperCount"]) : null,
74
+ _docxRevisionAncestors: ancestors
60
75
  };
61
76
  }
62
77
  }],
63
78
  toDOM(mark) {
64
- const { revisionId, author, date, provenance, suggestionId } = expectTrackedChangeMarkAttrs(mark);
79
+ const { revisionId, author, date, provenance, suggestionId, _docxOuterWrapperCount, _docxRevisionAncestors } = expectTrackedChangeMarkAttrs(mark);
65
80
  const idx = getAuthorColorIdx(author);
66
81
  const color = AUTHOR_COLORS[idx] ?? "#000000";
67
82
  const titleParts = [author, date ? new Date(date).toLocaleDateString() : ""].filter(Boolean);
@@ -76,6 +91,8 @@ const InsertionExtension = createMarkExtension({
76
91
  ...suggested ? { "data-provenance": "suggested" } : {},
77
92
  ...suggested && suggestionId ? { "data-suggestion-id": suggestionId } : {},
78
93
  ...date ? { "data-date": date } : {},
94
+ ..._docxOuterWrapperCount != null ? { "data-outer-wrapper-count": String(_docxOuterWrapperCount) } : {},
95
+ ..._docxRevisionAncestors ? { "data-revision-ancestors": JSON.stringify(_docxRevisionAncestors) } : {},
79
96
  ...titleParts.length > 0 ? { title: `${suggested ? "Suggested" : "Inserted"}: ${titleParts.join(", ")}` } : {},
80
97
  style: suggested ? suggestedInsertionStyle() : insertionStyle(color)
81
98
  },
@@ -101,21 +118,27 @@ const DeletionExtension = createMarkExtension({
101
118
  initials: { default: null },
102
119
  provenance: { default: "user" },
103
120
  suggestionId: { default: null },
104
- _historicalFormatting: { default: null }
121
+ _historicalFormatting: { default: null },
122
+ _docxOuterWrapperCount: { default: null },
123
+ _docxRevisionAncestors: { default: null }
105
124
  },
106
125
  inclusive: false,
107
126
  parseDOM: [{
108
127
  tag: "span.docx-deletion",
109
128
  getAttrs(dom) {
129
+ const ancestors = revisionAncestorsFromDom(dom.dataset["revisionAncestors"]);
130
+ if (ancestors === false) return false;
110
131
  return {
111
132
  revisionId: Number.parseInt(dom.dataset["revisionId"] ?? "0", 10),
112
133
  author: dom.dataset["author"] ?? "",
113
- date: dom.dataset["date"] ?? null
134
+ date: dom.dataset["date"] ?? null,
135
+ _docxOuterWrapperCount: dom.dataset["outerWrapperCount"] ? Number(dom.dataset["outerWrapperCount"]) : null,
136
+ _docxRevisionAncestors: ancestors
114
137
  };
115
138
  }
116
139
  }],
117
140
  toDOM(mark) {
118
- const { revisionId, author, date, provenance, suggestionId } = expectTrackedChangeMarkAttrs(mark);
141
+ const { revisionId, author, date, provenance, suggestionId, _docxOuterWrapperCount, _docxRevisionAncestors } = expectTrackedChangeMarkAttrs(mark);
119
142
  const idx = getAuthorColorIdx(author);
120
143
  const color = AUTHOR_COLORS[idx] ?? "#000000";
121
144
  const titleParts = [author, date ? new Date(date).toLocaleDateString() : ""].filter(Boolean);
@@ -130,6 +153,8 @@ const DeletionExtension = createMarkExtension({
130
153
  ...suggested ? { "data-provenance": "suggested" } : {},
131
154
  ...suggested && suggestionId ? { "data-suggestion-id": suggestionId } : {},
132
155
  ...date ? { "data-date": date } : {},
156
+ ..._docxOuterWrapperCount != null ? { "data-outer-wrapper-count": String(_docxOuterWrapperCount) } : {},
157
+ ..._docxRevisionAncestors ? { "data-revision-ancestors": JSON.stringify(_docxRevisionAncestors) } : {},
133
158
  ...titleParts.length > 0 ? { title: `${suggested ? "Suggested deletion" : "Deleted"}: ${titleParts.join(", ")}` } : {},
134
159
  style: suggested ? suggestedDeletionStyle() : deletionStyle(color)
135
160
  },
@@ -13,11 +13,15 @@ const surveyCommentReferences = (doc) => {
13
13
  doc.descendants((node, position) => {
14
14
  if (node.type.name === "commentReference") {
15
15
  const attrs = readCommentReferenceAttrs(node);
16
- references.push({
17
- commentId: attrs.ok ? attrs.value.commentId : -1,
16
+ references.push(attrs.ok ? {
17
+ status: "valid",
18
+ commentId: attrs.value.commentId,
18
19
  position,
19
- size: node.nodeSize,
20
- malformed: !attrs.ok
20
+ size: node.nodeSize
21
+ } : {
22
+ status: "malformed",
23
+ position,
24
+ size: node.nodeSize
21
25
  });
22
26
  return false;
23
27
  }
@@ -60,7 +64,7 @@ const CommentReferenceExtension = createNodeExtension({
60
64
  tag: "span[data-docx-comment-reference]",
61
65
  getAttrs(dom) {
62
66
  const raw = dom.getAttribute("data-docx-comment-reference");
63
- if (raw === null || !/^(?:0|[1-9]\d*)$/.test(raw)) return false;
67
+ if (raw === null || !/^-?(?:0|[1-9]\d*)$/.test(raw)) return false;
64
68
  const commentId = Number(raw);
65
69
  return Number.isSafeInteger(commentId) ? { commentId } : false;
66
70
  }
@@ -367,7 +367,7 @@ const statedCellWidth = (value, type) => ({
367
367
  }
368
368
  });
369
369
  const tableCellSpec = {
370
- content: "(paragraph | table | textBox | preservedBlock | blockBookmarkBoundary)+",
370
+ content: "(paragraph | table | textBox | blockSdt | preservedBlock | blockBookmarkBoundary)+",
371
371
  tableRole: "cell",
372
372
  isolating: true,
373
373
  attrs: {
@@ -436,7 +436,7 @@ const tableCellSpec = {
436
436
  }
437
437
  };
438
438
  const tableHeaderSpec = {
439
- content: "(paragraph | table | textBox | preservedBlock | blockBookmarkBoundary)+",
439
+ content: "(paragraph | table | textBox | blockSdt | preservedBlock | blockBookmarkBoundary)+",
440
440
  tableRole: "header_cell",
441
441
  isolating: true,
442
442
  attrs: {
@@ -13,16 +13,16 @@ declare const RUN_IDENTITY_MARK_NAME = "runIdentity";
13
13
  type RunIdentityPayload = {
14
14
  preservedAttributes?: readonly document_d_exports.PreservedAttribute[] | undefined;
15
15
  preserved?: document_d_exports.PreservedMarkup | undefined;
16
+ emptyFormatting?: boolean | undefined;
16
17
  };
17
18
  /** A run identity's attrs in canonical form. */
18
- declare const runIdentityAttrs: (id: number, { preservedAttributes, preserved }?: RunIdentityPayload) => RunIdentityMarkAttrs;
19
+ declare const runIdentityAttrs: (id: number, { preservedAttributes, preserved, emptyFormatting }?: RunIdentityPayload) => RunIdentityMarkAttrs;
19
20
  /**
20
21
  * Whether a run holds markup the mark exists to carry.
21
22
  *
22
- * The mint condition, minus the page break: a run with neither remainder nor
23
- * sink and no break to rejoin its leaves across needs no identity, and 57.5%
24
- * of corpus files hold none.
23
+ * The payload condition. The conversion also mints an identity for runs whose
24
+ * page break or mixed inline atoms split one authored run into several leaves.
25
25
  */
26
- declare const hasRunIdentityPayload: ({ preservedAttributes, preserved }: RunIdentityPayload) => boolean;
26
+ declare const hasRunIdentityPayload: ({ preservedAttributes, preserved, emptyFormatting }: RunIdentityPayload) => boolean;
27
27
  //#endregion
28
28
  export { RUN_IDENTITY_ATTRIBUTE, RUN_IDENTITY_MARK_NAME, RunIdentityPayload, hasRunIdentityPayload, runIdentityAttrs };
@@ -18,19 +18,19 @@ const preservedMarkup = ({ children }) => ({ children: (children ?? []).map(({ i
18
18
  xml
19
19
  })) });
20
20
  /** A run identity's attrs in canonical form. */
21
- const runIdentityAttrs = (id, { preservedAttributes, preserved } = {}) => {
21
+ const runIdentityAttrs = (id, { preservedAttributes, preserved, emptyFormatting } = {}) => {
22
22
  const attrs = { id };
23
23
  if (preservedAttributes && preservedAttributes.length > 0) attrs.preservedAttributes = preservedAttributes.map(preservedAttribute);
24
24
  if (preserved && (preserved.children?.length ?? 0) > 0) attrs.preserved = preservedMarkup(preserved);
25
+ if (emptyFormatting) attrs.emptyFormatting = true;
25
26
  return attrs;
26
27
  };
27
28
  /**
28
29
  * Whether a run holds markup the mark exists to carry.
29
30
  *
30
- * The mint condition, minus the page break: a run with neither remainder nor
31
- * sink and no break to rejoin its leaves across needs no identity, and 57.5%
32
- * of corpus files hold none.
31
+ * The payload condition. The conversion also mints an identity for runs whose
32
+ * page break or mixed inline atoms split one authored run into several leaves.
33
33
  */
34
- const hasRunIdentityPayload = ({ preservedAttributes, preserved }) => (preservedAttributes?.length ?? 0) > 0 || (preserved?.children?.length ?? 0) > 0;
34
+ const hasRunIdentityPayload = ({ preservedAttributes, preserved, emptyFormatting }) => (preservedAttributes?.length ?? 0) > 0 || (preserved?.children?.length ?? 0) > 0 || emptyFormatting === true;
35
35
  //#endregion
36
36
  export { RUN_IDENTITY_ATTRIBUTE, RUN_IDENTITY_MARK_NAME, hasRunIdentityPayload, runIdentityAttrs };
@@ -1,4 +1,4 @@
1
- import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RunFormattingOverrideAttrs, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, UnderlineAttrs } from "./marks.js";
1
+ import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RunFormattingOverrideAttrs, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedRevisionAncestor, UnderlineAttrs } from "./marks.js";
2
2
  import { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, PageBreakRunAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, PreservedBlockAttrs, PreservedXmlAttrs, PreservedXmlLevel, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "./nodes.js";
3
3
  import { ExtensionManager } from "../extensions/ExtensionManager.js";
4
4
  //#region src/prosemirror/schema/index.d.ts
@@ -11,4 +11,4 @@ type DocxSchema = typeof schema;
11
11
  type DocxNode = ReturnType<typeof schema.node>;
12
12
  type DocxMark = ReturnType<typeof schema.mark>;
13
13
  //#endregion
14
- export { type BlockSdtAttrs, type BookmarkBoundaryAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type InlineWrapperAttrs, type InlineWrapperKind, type InlineWrapperLayer, type LanguageAttrs, type MathAttrs, type PageBreakRunAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type PreservedBlockAttrs, type PreservedXmlAttrs, type PreservedXmlLevel, type RunFormattingOverrideAttrs, type RunIdentityMarkAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type UnderlineAttrs, schema, singletonManager };
14
+ export { type BlockSdtAttrs, type BookmarkBoundaryAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type InlineWrapperAttrs, type InlineWrapperKind, type InlineWrapperLayer, type LanguageAttrs, type MathAttrs, type PageBreakRunAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type PreservedBlockAttrs, type PreservedXmlAttrs, type PreservedXmlLevel, type RunFormattingOverrideAttrs, type RunIdentityMarkAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type TrackedRevisionAncestor, type UnderlineAttrs, schema, singletonManager };
@@ -99,6 +99,15 @@ type CommentAttrs = {
99
99
  */
100
100
  declare const TRACKED_CHANGE_PROVENANCE_VALUES: readonly ["user", "suggested"];
101
101
  type TrackedChangeProvenance = (typeof TRACKED_CHANGE_PROVENANCE_VALUES)[number];
102
+ type TrackedRevisionAncestor = {
103
+ type: "insertion" | "deletion" | "moveFrom" | "moveTo";
104
+ revisionId: number;
105
+ author: string;
106
+ date?: string;
107
+ utcDate?: string;
108
+ initials?: string;
109
+ outerWrapperCount: number;
110
+ };
102
111
  type TrackedChangeMarkAttrs = {
103
112
  revisionId: number;
104
113
  author: string;
@@ -108,6 +117,10 @@ type TrackedChangeMarkAttrs = {
108
117
  /** Optional author initials (w:initials) carried through the round-trip. */
109
118
  initials?: string;
110
119
  moveKind?: "moveTo" | "moveFrom";
120
+ /** Number of inline-wrapper layers authored outside this revision. */
121
+ _docxOuterWrapperCount?: number;
122
+ /** Outer revisions, in authored order, that ProseMirror cannot hold as nested marks. */
123
+ _docxRevisionAncestors?: readonly TrackedRevisionAncestor[];
111
124
  /** Defaults to `"user"`; `"suggested"` for AI-proposed, non-serialized edits. */
112
125
  provenance: TrackedChangeProvenance;
113
126
  /**
@@ -136,7 +149,7 @@ type RunPropertyChangeMarkAttrs = {
136
149
  * in the schema would force a resolution.
137
150
  *
138
151
  * Minted only when the run has something to carry — a page break, an
139
- * attribute remainder, or a property-set remainder — so a document that
152
+ * attribute remainder, or authored run properties — so a document that
140
153
  * carries none pays nothing.
141
154
  */
142
155
  type RunIdentityMarkAttrs = {
@@ -146,6 +159,8 @@ type RunIdentityMarkAttrs = {
146
159
  preservedAttributes?: readonly document_d_exports.PreservedAttribute[];
147
160
  /** The `w:rPr` children no reader took a value from, with their ordinals. */
148
161
  preserved?: document_d_exports.PreservedMarkup;
162
+ /** Preserve an authored empty `w:rPr`, distinct from no run properties. */
163
+ emptyFormatting?: true;
149
164
  };
150
165
  /**
151
166
  * One transparent wrapper a leaf sits inside, minus the content ProseMirror
@@ -170,6 +185,17 @@ type InlineWrapperKind = InlineWrapperLayer["kind"];
170
185
  */
171
186
  type InlineWrapperAttrs = {
172
187
  stack: readonly InlineWrapperLayer[];
188
+ /**
189
+ * The imported hyperlink whose children the suffix of `stack` came from.
190
+ *
191
+ * ProseMirror stores marks as an unordered set, so a link mark and a wrapper
192
+ * mark otherwise cannot say whether `<w:bdo>` surrounded `<w:hyperlink>` or
193
+ * was one of its children. This is parser provenance, not editable wrapper
194
+ * data: it is meaningful only alongside the matching hyperlink mark.
195
+ */
196
+ _docxHyperlinkIndex?: number;
197
+ /** The first stack layer authored inside `_docxHyperlinkIndex`. */
198
+ _docxInsideHyperlinkStackStart?: number;
173
199
  };
174
200
  declare const COMPLEX_SCRIPT_RUN_PROPERTY_KEYS: readonly ["boldCs", "italicCs", "fontSizeCs"];
175
201
  type ComplexScriptRunPropertyKey = (typeof COMPLEX_SCRIPT_RUN_PROPERTY_KEYS)[number];
@@ -278,4 +304,4 @@ type HyperlinkAttrs = {
278
304
  _docxHyperlinkIndex?: number;
279
305
  };
280
306
  //#endregion
281
- export { AuthoredRunFormattingValues, COMPLEX_SCRIPT_RUN_PROPERTY_KEYS, CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, ComplexScriptRunPropertyKey, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RUN_FORMATTING_BOOLEAN_PROPERTIES, RUN_FORMATTING_PROPERTY_SPECS, RUN_FORMATTING_VALUE_PROPERTIES, RunFormattingBooleanProperty, RunFormattingOverrideAttrs, RunFormattingValueProperty, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TRACKED_CHANGE_PROVENANCE_VALUES, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedChangeProvenance, UnderlineAttrs };
307
+ export { AuthoredRunFormattingValues, COMPLEX_SCRIPT_RUN_PROPERTY_KEYS, CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, ComplexScriptRunPropertyKey, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RUN_FORMATTING_BOOLEAN_PROPERTIES, RUN_FORMATTING_PROPERTY_SPECS, RUN_FORMATTING_VALUE_PROPERTIES, RunFormattingBooleanProperty, RunFormattingOverrideAttrs, RunFormattingValueProperty, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TRACKED_CHANGE_PROVENANCE_VALUES, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedChangeProvenance, TrackedRevisionAncestor, UnderlineAttrs };
@@ -1,5 +1,5 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
- import { TrackedChangeProvenance } from "./marks.js";
2
+ import { InlineWrapperLayer, TrackedChangeProvenance } from "./marks.js";
3
3
  import "../../types/index.js";
4
4
  import { OutlineStyleAttr } from "../../types/documentEnumValues.js";
5
5
  import { SpacingExplicit } from "../../types/formatting.js";
@@ -212,9 +212,9 @@ type ParagraphAttrs = {
212
212
  id: number;
213
213
  name: string;
214
214
  }[];
215
- /** Empty `w:hyperlink` elements cannot be represented as text marks.
216
- * Preserve their relationship metadata at the paragraph boundary so a
217
- * no-edit DOCX round-trip does not silently drop hyperlink elements. */
215
+ /** A `w:hyperlink` with no projectable leaf cannot be represented as a text mark.
216
+ * Preserve its metadata and any empty transparent-wrapper children at the
217
+ * paragraph boundary so an editor round trip does not drop either element. */
218
218
  _emptyHyperlinks?: {
219
219
  /** ProseMirror inline offset where the zero-width hyperlink appeared. */
220
220
  offset: number;
@@ -225,6 +225,8 @@ type ParagraphAttrs = {
225
225
  target?: document_d_exports.Hyperlink["target"];
226
226
  history?: document_d_exports.Hyperlink["history"];
227
227
  docLocation?: document_d_exports.Hyperlink["docLocation"];
228
+ /** Empty transparent-wrapper nests authored as children of this link. */
229
+ _docxEmptyWrapperStacks?: readonly (readonly InlineWrapperLayer[])[];
228
230
  }[];
229
231
  /**
230
232
  * Run-in heading flag: this paragraph's mark carries
@@ -16,7 +16,7 @@ declare const TRACKED_RUN_INLINE_ATOM_DISPOSITIONS: Readonly<{
16
16
  readonly hardBreak: "carry";
17
17
  readonly image: "carry";
18
18
  readonly math: "carry";
19
- readonly moveRangeBoundary: "transparent";
19
+ readonly moveRangeBoundary: "carry";
20
20
  readonly pageBreakRun: "page-break-carrier";
21
21
  readonly preservedXml: "carry";
22
22
  readonly rangeAnchor: "transparent";
@@ -15,7 +15,7 @@ const TRACKED_RUN_INLINE_ATOM_DISPOSITIONS = Object.freeze({
15
15
  hardBreak: "carry",
16
16
  image: "carry",
17
17
  math: "carry",
18
- moveRangeBoundary: "transparent",
18
+ moveRangeBoundary: "carry",
19
19
  pageBreakRun: "page-break-carrier",
20
20
  preservedXml: "carry",
21
21
  rangeAnchor: "transparent",
@@ -1,4 +1,4 @@
1
- import { expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
1
+ import { expectRunPropertyChangeMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
2
2
  import { nodePropertyRevisionSites, propertyRevisionMetadata, propertyRevisionRecords } from "../revisionCarriers.js";
3
3
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
4
4
  //#region src/prosemirror/utils/extractTrackedChanges.ts
@@ -46,6 +46,7 @@ function extractTrackedChanges(state) {
46
46
  const runPropertyChangeType = schema.marks["runPropertyChange"];
47
47
  if (!insertionType && !deletionType && !runPropertyChangeType) return EMPTY_RESULT;
48
48
  const raw = [];
49
+ const nestedRevisionIds = /* @__PURE__ */ new Set();
49
50
  const commentToRevision = /* @__PURE__ */ new Map();
50
51
  const commentMetadata = /* @__PURE__ */ new Map();
51
52
  const rowRevisionScopes = [];
@@ -223,14 +224,28 @@ function extractTrackedChanges(state) {
223
224
  if (mark.type === insertionType || mark.type === deletionType) {
224
225
  tcMark = mark;
225
226
  if (foldIntoRowRevision(rowRevisionScopes.at(-1), mark)) continue;
227
+ const attrs = expectTrackedChangeMarkAttrs(mark);
228
+ if (attrs._docxRevisionAncestors?.length) nestedRevisionIds.add(attrs.revisionId);
229
+ for (const ancestor of attrs._docxRevisionAncestors ?? []) {
230
+ nestedRevisionIds.add(ancestor.revisionId);
231
+ raw.push({
232
+ type: ancestor.type === "insertion" || ancestor.type === "moveTo" ? "insertion" : "deletion",
233
+ text: inlineText,
234
+ author: ancestor.author,
235
+ date: ancestor.date,
236
+ from: pos,
237
+ to: pos + node.nodeSize,
238
+ revisionId: ancestor.revisionId
239
+ });
240
+ }
226
241
  raw.push({
227
242
  type: mark.type === insertionType ? "insertion" : "deletion",
228
243
  text: inlineText,
229
- author: mark.attrs["author"] || "",
230
- date: mark.attrs["date"],
244
+ author: attrs.author,
245
+ date: attrs.date,
231
246
  from: pos,
232
247
  to: pos + node.nodeSize,
233
- revisionId: mark.attrs["revisionId"]
248
+ revisionId: attrs.revisionId
234
249
  });
235
250
  }
236
251
  }
@@ -380,7 +395,7 @@ function extractTrackedChanges(state) {
380
395
  merged.push({ ...entry });
381
396
  continue;
382
397
  }
383
- const key = `${entry.type}|${entry.author}|${entry.date ?? ""}`;
398
+ const key = `${entry.type}|${entry.author}|${entry.date ?? ""}|${nestedRevisionIds.has(entry.revisionId) ? entry.revisionId : ""}`;
384
399
  const group = inlineGroups.get(key);
385
400
  if (group) {
386
401
  const sep = group.to === entry.from ? "" : " ";
@@ -403,7 +418,7 @@ function extractTrackedChanges(state) {
403
418
  for (let i = 0; i < merged.length; i++) {
404
419
  const curr = merged[i];
405
420
  const next = merged[i + 1];
406
- if (curr.type === "deletion" && next && next.type === "insertion" && curr.author === next.author && curr.to === next.from) {
421
+ if (curr.type === "deletion" && next && next.type === "insertion" && !nestedRevisionIds.has(curr.revisionId) && !nestedRevisionIds.has(next.revisionId) && curr.author === next.author && curr.to === next.from) {
407
422
  final.push({
408
423
  type: "replacement",
409
424
  text: next.text,