@stll/folio-core 0.56.0 → 0.57.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 (142) hide show
  1. package/dist/ai-edits/apply.js +45 -14
  2. package/dist/ai-edits/batch-claims.d.ts +3 -1
  3. package/dist/ai-edits/batch-claims.js +25 -1
  4. package/dist/ai-edits/table-row-column-mutations.js +17 -3
  5. package/dist/ai-suggestions/apply.js +20 -1
  6. package/dist/compare/inline-atoms.js +40 -16
  7. package/dist/compare/inline-provenance.js +12 -5
  8. package/dist/compat/eigenpal.d.ts +2 -1
  9. package/dist/compat/eigenpal.js +2 -1
  10. package/dist/controller/canonicalClipboard.js +7 -1
  11. package/dist/controller/canonicalComments.d.ts +41 -0
  12. package/dist/controller/canonicalComments.js +152 -0
  13. package/dist/controller/canonicalComposition.d.ts +4 -0
  14. package/dist/controller/canonicalComposition.js +43 -11
  15. package/dist/controller/canonicalInlineProjection.d.ts +6 -1
  16. package/dist/controller/canonicalInlineProjection.js +86 -3
  17. package/dist/controller/canonicalInput.js +10 -1
  18. package/dist/controller/canonicalInputTimer.d.ts +10 -1
  19. package/dist/controller/canonicalInputTimer.js +24 -1
  20. package/dist/controller/canonicalPublicOperations.d.ts +2 -2
  21. package/dist/controller/canonicalPublicOperations.js +107 -15
  22. package/dist/controller/canonicalSelection.d.ts +18 -0
  23. package/dist/controller/canonicalSelection.js +32 -0
  24. package/dist/controller/canonicalSession.d.ts +3 -0
  25. package/dist/controller/canonicalSession.js +58 -24
  26. package/dist/controller/canonicalStoryEditor.d.ts +2 -0
  27. package/dist/controller/canonicalStoryEditor.js +20 -0
  28. package/dist/controller/folioEditor.js +13 -0
  29. package/dist/controller/headerFooterEditorManager.d.ts +1 -1
  30. package/dist/controller/headerFooterEditorManager.js +25 -11
  31. package/dist/controller/hiddenEditorApi.d.ts +13 -2
  32. package/dist/controller/hiddenEditorApi.js +4 -0
  33. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  34. package/dist/controller/hiddenEditorManager.js +166 -13
  35. package/dist/controller/noteEditorManager.d.ts +1 -1
  36. package/dist/controller/noteEditorManager.js +28 -13
  37. package/dist/docx/builtInStyles.js +13 -6
  38. package/dist/docx/containerChildren.gen.d.ts +3 -1
  39. package/dist/docx/containerChildren.gen.js +33 -1
  40. package/dist/docx/fieldParser.js +14 -2
  41. package/dist/docx/hyperlinkParser.d.ts +1 -1
  42. package/dist/docx/hyperlinkParser.js +20 -1
  43. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  44. package/dist/docx/inlineWrapperContent.js +4 -4
  45. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  46. package/dist/docx/linkedSdtPreservation.js +37 -0
  47. package/dist/docx/paragraphParser.js +88 -67
  48. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  49. package/dist/docx/paragraphPropertySource.js +107 -1
  50. package/dist/docx/preservedRunContent.js +2 -1
  51. package/dist/docx/relsParser.js +26 -2
  52. package/dist/docx/rezip.js +9 -6
  53. package/dist/docx/selectiveSave.js +10 -8
  54. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  55. package/dist/docx/serializer/commentSerializer.js +7 -3
  56. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  57. package/dist/docx/server/materializeYjsDocx.js +14 -6
  58. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  59. package/dist/i18n/messages/ar.gen.js +4 -0
  60. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  61. package/dist/i18n/messages/catalogs.gen.js +68 -0
  62. package/dist/i18n/messages/cs.gen.js +4 -0
  63. package/dist/i18n/messages/de.gen.js +4 -0
  64. package/dist/i18n/messages/en.gen.js +4 -0
  65. package/dist/i18n/messages/es.gen.js +4 -0
  66. package/dist/i18n/messages/et.gen.js +4 -0
  67. package/dist/i18n/messages/fr.gen.js +4 -0
  68. package/dist/i18n/messages/he.gen.js +4 -0
  69. package/dist/i18n/messages/hi.gen.js +4 -0
  70. package/dist/i18n/messages/hu.gen.js +4 -0
  71. package/dist/i18n/messages/lt.gen.js +4 -0
  72. package/dist/i18n/messages/lv.gen.js +4 -0
  73. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  74. package/dist/i18n/messages/pl.gen.js +4 -0
  75. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  76. package/dist/i18n/messages/sk.gen.js +4 -0
  77. package/dist/i18n/messages/tr.gen.js +4 -0
  78. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +2 -1
  81. package/dist/internal/revisionResolutionInline.js +25 -3
  82. package/dist/markdown/renderRuns.js +1 -11
  83. package/dist/panel-layout.d.ts +11 -9
  84. package/dist/panel-layout.js +13 -9
  85. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  86. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  87. package/dist/prosemirror/attrs/index.js +5 -0
  88. package/dist/prosemirror/commands/comments.js +60 -35
  89. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  90. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  91. package/dist/prosemirror/commentOps.js +15 -1
  92. package/dist/prosemirror/conversion/fromProseDoc.js +22 -3
  93. package/dist/prosemirror/conversion/toProseDoc.js +119 -18
  94. package/dist/prosemirror/extensions/core/ParagraphExtension.js +22 -5
  95. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  96. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  97. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +22 -14
  98. package/dist/prosemirror/extensions/marks/markUtils.js +8 -7
  99. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  100. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  101. package/dist/prosemirror/fieldRepresentation.js +18 -0
  102. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  103. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  104. package/dist/prosemirror/listNumbering.js +11 -5
  105. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  106. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  107. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  108. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  109. package/dist/prosemirror/noteReferenceReview.js +14 -1
  110. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  111. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  112. package/dist/prosemirror/plugins/suggestionMode.js +47 -49
  113. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  114. package/dist/prosemirror/preservedXmlReview.js +35 -0
  115. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  116. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  117. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  118. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  119. package/dist/prosemirror/runStyleFormatting.d.ts +3 -1
  120. package/dist/prosemirror/runStyleFormatting.js +3 -1
  121. package/dist/prosemirror/schema/marks.d.ts +1 -0
  122. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  123. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  124. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  125. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  126. package/dist/prosemirror/textInput.d.ts +5 -1
  127. package/dist/prosemirror/textInput.js +3 -3
  128. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  129. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  130. package/dist/prosemirror/validation.js +3 -5
  131. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  132. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  133. package/dist/types/canonicalCapabilities.d.ts +6 -20
  134. package/dist/types/canonicalCapabilities.js +6 -20
  135. package/dist/types/canonicalComments.d.ts +51 -0
  136. package/dist/types/canonicalComments.js +0 -0
  137. package/dist/types/canonicalSections.d.ts +12 -0
  138. package/dist/types/canonicalSections.js +0 -0
  139. package/dist/utils/headingCollector.js +45 -4
  140. package/dist/utils/outlineDepth.d.ts +9 -0
  141. package/dist/utils/outlineDepth.js +9 -0
  142. package/package.json +2 -2
@@ -3,7 +3,7 @@ import { anchorTargetAttrs, normalizeUserUrl, sanitizeExternalUrl } from "../../
3
3
  import { expectHyperlinkMarkAttrs } from "../../attrs/index.js";
4
4
  import { withCanonicalCommand } from "../../canonicalCommands.js";
5
5
  import { canonicalSelectionRange } from "../../canonicalSelectionRange.js";
6
- import { removeHyperlinkInRange } from "../../hyperlinkRemoval.js";
6
+ import { assertHyperlinkRemovalRange, removeHyperlinkInRange } from "../../hyperlinkRemoval.js";
7
7
  import { getDocumentBuiltInStyles } from "../../plugins/documentStyleState.js";
8
8
  import { createMarkExtension } from "../create.js";
9
9
  import { isMarkActive } from "./markUtils.js";
@@ -149,26 +149,34 @@ const HyperlinkExtension = createMarkExtension({
149
149
  const removalRange = (state) => {
150
150
  const { empty, $from } = state.selection;
151
151
  const { from, to } = canonicalSelectionRange(state);
152
+ assertHyperlinkRemovalRange({
153
+ doc: state.doc,
154
+ from,
155
+ to
156
+ });
152
157
  if (!empty) return {
153
158
  from,
154
159
  to
155
160
  };
156
- const hlType = documentHyperlinkType(state);
157
- if (!$from.marks().some((mark) => mark.type === hlType)) return void 0;
158
- let start = from;
159
- let end = to;
161
+ const linkMark = $from.marks().find((mark) => mark.type === documentHyperlinkType(state));
162
+ if (!linkMark) return void 0;
163
+ let contiguous;
164
+ let selected;
160
165
  $from.parent.forEach((node, offset) => {
161
- const nodeStart = $from.start() + offset;
162
- const nodeEnd = nodeStart + node.nodeSize;
163
- if (node.isText && nodeStart <= from && from <= nodeEnd && node.marks.some((mark) => mark.type === hlType)) {
164
- start = Math.min(start, nodeStart);
165
- end = Math.max(end, nodeEnd);
166
+ if (!node.isInline || !node.marks.some((mark) => mark.eq(linkMark))) {
167
+ contiguous = void 0;
168
+ return;
166
169
  }
170
+ const start = $from.start() + offset;
171
+ const end = start + node.nodeSize;
172
+ if (contiguous?.to === start) contiguous.to = end;
173
+ else contiguous = {
174
+ from: start,
175
+ to: end
176
+ };
177
+ if (start <= from && from <= end) selected = contiguous;
167
178
  });
168
- return {
169
- from: start,
170
- to: end
171
- };
179
+ return selected;
172
180
  };
173
181
  const removeHyperlink = withCanonicalCommand((state, dispatch) => {
174
182
  const range = removalRange(state);
@@ -1,7 +1,7 @@
1
1
  import { FONT_THEME_VALUES } from "../../../types/documentEnumValues.js";
2
2
  import { mergeFontFamily } from "../../../utils/fontFamilyMerge.js";
3
3
  import { normalizeHorizontalScalePercent } from "../../../utils/horizontalScale.js";
4
- import { expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectEmphasisMarkAttrs, expectFontFamilyMarkAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectStrikeMarkAttrs, expectTextEffectMarkAttrs } from "../../attrs/index.js";
4
+ import { expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectEmphasisMarkAttrs, expectFontFamilyMarkAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectStrikeMarkAttrs, expectTextEffectMarkAttrs } from "../../attrs/index.js";
5
5
  import { canonicalRunFormatting, withCanonicalCommand } from "../../canonicalCommands.js";
6
6
  import { canonicalSelectionRange } from "../../canonicalSelectionRange.js";
7
7
  import { runShadingAttrsToShading, shadingToRunShadingAttrs } from "../../conversion/runShadingMark.js";
@@ -147,14 +147,15 @@ function saveStoredMarksToParagraph(state, tr, marks) {
147
147
  const paragraph = $from.parent;
148
148
  if (paragraph.type.name !== "paragraph") return tr;
149
149
  if (paragraph.textContent.length > 0) return tr;
150
- if (marks.length === 0) return tr.setNodeMarkup($from.before(), void 0, {
151
- ...paragraph.attrs,
152
- defaultTextFormatting: null
153
- });
154
- const defaultTextFormatting = marksToTextFormatting(marks);
150
+ const attrs = expectParagraphAttrs(paragraph);
151
+ const defaultTextFormatting = marks.length === 0 ? null : marksToTextFormatting(marks);
155
152
  return tr.setNodeMarkup($from.before(), void 0, {
156
153
  ...paragraph.attrs,
157
- defaultTextFormatting
154
+ defaultTextFormatting,
155
+ _originalFormatting: {
156
+ ...attrs._originalFormatting,
157
+ runProperties: defaultTextFormatting ?? void 0
158
+ }
158
159
  });
159
160
  }
160
161
  function dispatchStoredMarks(state, dispatch, marks) {
@@ -1,3 +1,4 @@
1
+ import { expectFootnoteRefMarkAttrs } from "../../attrs/index.js";
1
2
  //#region src/prosemirror/extensions/marks/noteReferenceDeletion.ts
2
3
  /** Keep the text carrying one OOXML note reference together during deletion. */
3
4
  const expandNoteReferenceDeletionRange = (doc, from, to) => {
@@ -7,6 +8,8 @@ const expandNoteReferenceDeletionRange = (doc, from, to) => {
7
8
  doc.nodesBetween(from, to, (node, pos) => {
8
9
  const reference = node.marks.find((mark) => mark.type.name === "footnoteRef");
9
10
  if (!node.isText || !reference) return;
11
+ const { occurrenceId } = expectFootnoteRefMarkAttrs(reference);
12
+ const sameOccurrence = (sibling) => sibling.isText && sibling.marks.some((mark) => mark.type.name === "footnoteRef" && expectFootnoteRefMarkAttrs(mark).occurrenceId === occurrenceId);
10
13
  intersectsReference = true;
11
14
  const $pos = doc.resolve(pos);
12
15
  const parent = $pos.parent;
@@ -15,12 +18,12 @@ const expandNoteReferenceDeletionRange = (doc, from, to) => {
15
18
  let referenceTo = pos + node.nodeSize;
16
19
  for (let left = index - 1; left >= 0; left--) {
17
20
  const sibling = parent.child(left);
18
- if (!sibling.isText || !sibling.marks.some((mark) => mark.eq(reference))) break;
21
+ if (!sameOccurrence(sibling)) break;
19
22
  referenceFrom -= sibling.nodeSize;
20
23
  }
21
24
  for (let right = index + 1; right < parent.childCount; right++) {
22
25
  const sibling = parent.child(right);
23
- if (!sibling.isText || !sibling.marks.some((mark) => mark.eq(reference))) break;
26
+ if (!sameOccurrence(sibling)) break;
24
27
  referenceTo += sibling.nodeSize;
25
28
  }
26
29
  expandedFrom = Math.min(expandedFrom, referenceFrom);
@@ -0,0 +1,8 @@
1
+ import { Node } from "prosemirror-model";
2
+ //#region src/prosemirror/fieldRepresentation.d.ts
3
+ /** Current children alone decide whether a field needs editable structural content. */
4
+ declare const fieldRequiresStructuredContent: (children: readonly Node[]) => boolean;
5
+ /** Resolution returns text-only fields to the same atom representation as initial conversion. */
6
+ declare const canonicalFieldNode: (node: Node) => Node;
7
+ //#endregion
8
+ export { canonicalFieldNode, fieldRequiresStructuredContent };
@@ -0,0 +1,18 @@
1
+ import "./extensions/marks/InlineWrapperExtension.js";
2
+ //#region src/prosemirror/fieldRepresentation.ts
3
+ /** Current children alone decide whether a field needs editable structural content. */
4
+ const fieldRequiresStructuredContent = (children) => children.some((child) => !child.isText || child.marks.some((mark) => mark.type.name === "hyperlink" || mark.type.name === "inlineWrapper" || mark.type.name === "insertion" || mark.type.name === "deletion"));
5
+ /** Resolution returns text-only fields to the same atom representation as initial conversion. */
6
+ const canonicalFieldNode = (node) => {
7
+ if (node.type.name !== "structuredField") return node;
8
+ const attrs = {
9
+ ...node.attrs,
10
+ displayText: node.textContent
11
+ };
12
+ if (fieldRequiresStructuredContent(node.content.content)) return node.attrs["displayText"] === node.textContent ? node : node.type.create(attrs, node.content, node.marks);
13
+ let marks = node.marks;
14
+ for (const mark of node.firstChild?.marks ?? []) marks = mark.addToSet(marks);
15
+ return node.type.schema.node("field", attrs, void 0, marks);
16
+ };
17
+ //#endregion
18
+ export { canonicalFieldNode, fieldRequiresStructuredContent };
@@ -1,9 +1,21 @@
1
+ import { Node } from "prosemirror-model";
1
2
  import { EditorState, Transaction } from "prosemirror-state";
2
3
  //#region src/prosemirror/hyperlinkRemoval.d.ts
4
+ declare const HyperlinkRemovalRefusal_base: import("better-result").TaggedErrorClass<"HyperlinkRemovalRefusal">;
5
+ declare class HyperlinkRemovalRefusal extends HyperlinkRemovalRefusal_base<{
6
+ message: string;
7
+ }> {}
8
+ type HyperlinkRemovalRangeOptions = {
9
+ doc: Node;
10
+ from: number;
11
+ to: number;
12
+ };
13
+ /** Canonical inline elements have no addressable interior gaps. */
14
+ declare const assertHyperlinkRemovalRange: ({ doc, from, to }: HyperlinkRemovalRangeOptions) => void;
3
15
  /**
4
16
  * Remove the hyperlinks from [`from`, `to`) of `tr.doc`: directly, or, with
5
17
  * suggestions on, as a tracked replacement of the linked text by plain text.
6
18
  */
7
19
  declare const removeHyperlinkInRange: (state: EditorState, tr: Transaction, from: number, to: number) => Transaction;
8
20
  //#endregion
9
- export { removeHyperlinkInRange };
21
+ export { HyperlinkRemovalRefusal, assertHyperlinkRemovalRange, removeHyperlinkInRange };
@@ -3,19 +3,33 @@ import { getDocumentBuiltInStyles, getDocumentStyleResolver } from "./plugins/do
3
3
  import { isSuggestionModeActive, makeRevisionInfo, suggestRangeDeletion } from "./plugins/suggestionMode.js";
4
4
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "./runFormattingReconciliation.js";
5
5
  import { paragraphRunStyleContextAt } from "./runStyleFormatting.js";
6
+ import { TaggedError } from "better-result";
6
7
  //#region src/prosemirror/hyperlinkRemoval.ts
7
- /** Contiguous stretches of linked, not yet deleted, text in [`from`, `to`) of `doc`. */
8
+ var HyperlinkRemovalRefusal = class extends TaggedError("HyperlinkRemovalRefusal") {};
9
+ const partialInlineRefusal = () => new HyperlinkRemovalRefusal({ message: "Hyperlink removal cannot split an indivisible inline element." });
10
+ /** Canonical inline elements have no addressable interior gaps. */
11
+ const assertHyperlinkRemovalRange = ({ doc, from, to }) => {
12
+ for (const position of [from, to]) {
13
+ const resolved = doc.resolve(position);
14
+ for (let depth = 1; depth <= resolved.depth; depth += 1) {
15
+ const node = resolved.node(depth);
16
+ if (node.isInline && node.marks.some(({ type }) => type.name === "hyperlink")) throw partialInlineRefusal();
17
+ }
18
+ }
19
+ };
20
+ /** Contiguous stretches of linked, not yet deleted, inline content in [`from`, `to`) of `doc`. */
8
21
  const linkedRanges = (doc, from, to) => {
9
22
  const ranges = [];
10
23
  doc.nodesBetween(from, to, (node, position) => {
11
- if (!node.isText || !node.marks.some(({ type }) => type.name === "hyperlink") || node.marks.some(({ type }) => type.name === "deletion")) return true;
24
+ if (!node.isInline || !node.marks.some(({ type }) => type.name === "hyperlink") || node.marks.some(({ type }) => type.name === "deletion")) return true;
12
25
  const start = Math.max(position, from);
13
26
  const end = Math.min(position + node.nodeSize, to);
14
27
  if (start >= end) return true;
28
+ if (!node.isText && (start !== position || end !== position + node.nodeSize)) throw partialInlineRefusal();
15
29
  const piece = {
16
30
  from: start,
17
31
  to: end,
18
- node: node.cut(start - position, end - position)
32
+ node: node.isText ? node.cut(start - position, end - position) : node
19
33
  };
20
34
  const last = ranges.at(-1);
21
35
  if (last && last.to === start) {
@@ -26,7 +40,7 @@ const linkedRanges = (doc, from, to) => {
26
40
  to: end,
27
41
  pieces: [piece]
28
42
  });
29
- return true;
43
+ return false;
30
44
  });
31
45
  return ranges;
32
46
  };
@@ -60,8 +74,8 @@ const withoutHyperlinkStyle = (state, doc, piece) => {
60
74
  previousFormatting
61
75
  };
62
76
  };
63
- /** Replace the marks of the text at [`from`, `to`) of `tr.doc` with `marks`. */
64
- const setTextMarks = (tr, from, to, current, marks) => {
77
+ /** Replace the marks of the inline content at [`from`, `to`) of `tr.doc` with `marks`. */
78
+ const setInlineMarks = (tr, from, to, current, marks) => {
65
79
  for (const mark of current) if (!mark.isInSet(marks)) tr.removeMark(from, to, mark);
66
80
  for (const mark of marks) if (!mark.isInSet(current)) tr.addMark(from, to, mark);
67
81
  };
@@ -70,6 +84,11 @@ const setTextMarks = (tr, from, to, current, marks) => {
70
84
  * suggestions on, as a tracked replacement of the linked text by plain text.
71
85
  */
72
86
  const removeHyperlinkInRange = (state, tr, from, to) => {
87
+ assertHyperlinkRemovalRange({
88
+ doc: tr.doc,
89
+ from,
90
+ to
91
+ });
73
92
  const { schema } = state;
74
93
  const hyperlinkType = schema.marks["hyperlink"];
75
94
  if (!hyperlinkType) return tr;
@@ -108,7 +127,7 @@ const removeHyperlinkInRange = (state, tr, from, to) => {
108
127
  ...withoutHyperlinkStyle(state, tr.doc, piece)
109
128
  }));
110
129
  if (!insertion || !deletionType) {
111
- for (const { piece, marks } of unlinked) setTextMarks(tr, piece.from, piece.to, piece.node.marks, marks.filter(({ type }) => type !== hyperlinkType));
130
+ for (const { piece, marks } of unlinked) setInlineMarks(tr, piece.from, piece.to, piece.node.marks, marks.filter(({ type }) => type !== hyperlinkType));
112
131
  continue;
113
132
  }
114
133
  const plain = unlinked.map(({ piece, marks, previousFormatting }) => {
@@ -119,11 +138,11 @@ const removeHyperlinkInRange = (state, tr, from, to) => {
119
138
  tr.insert(range.to, plain);
120
139
  for (const { piece, marks, previousFormatting } of unlinked.toReversed()) {
121
140
  const change = previousFormatting && !hasPendingPropertyChange(piece.node.marks) ? propertyChange(previousFormatting) : null;
122
- setTextMarks(tr, piece.from, piece.to, piece.node.marks, change ? change.addToSet(marks) : marks);
141
+ setInlineMarks(tr, piece.from, piece.to, piece.node.marks, change ? change.addToSet(marks) : marks);
123
142
  }
124
143
  suggestRangeDeletion(state, tr, range.from, range.to);
125
144
  }
126
145
  return tr;
127
146
  };
128
147
  //#endregion
129
- export { removeHyperlinkInRange };
148
+ export { HyperlinkRemovalRefusal, assertHyperlinkRemovalRange, removeHyperlinkInRange };
@@ -10,7 +10,7 @@ import { directParagraphIndentation } from "./paragraphIndentation.js";
10
10
  import { getDocumentNumbering } from "./plugins/documentNumbering.js";
11
11
  import { getDocumentStyleResolver } from "./plugins/documentStyleState.js";
12
12
  import { SUGGESTION_META, makeRevisionInfo } from "./plugins/suggestionMode.js";
13
- import { listAttrsFromNumbering, listLevelIndentAttrPatch } from "./styles/resolvedStyleAttrs.js";
13
+ import { listAttrsFromNumbering, listIndentationProvenancePatch } from "./styles/resolvedStyleAttrs.js";
14
14
  //#region src/prosemirror/listNumbering.ts
15
15
  /**
16
16
  * Which numbering instance a list command puts a paragraph in, and the
@@ -199,10 +199,16 @@ const listItemAttrs = (attrs, { numId, ilvl }, numbering) => ({
199
199
  numId,
200
200
  ilvl
201
201
  }, numbering),
202
- ...listLevelIndentAttrPatch(directParagraphIndentation(attrs), {
203
- numId,
204
- ilvl
205
- }, numbering),
202
+ ...listIndentationProvenancePatch({
203
+ direct: directParagraphIndentation(attrs),
204
+ styleFormatting: attrs._styleResolvedFormatting,
205
+ numberingSource: "paragraph",
206
+ numPr: {
207
+ numId,
208
+ ilvl
209
+ },
210
+ numbering
211
+ }),
206
212
  listImplicitChildLevelAdvances: attrs["listImplicitChildLevelAdvances"] ?? null,
207
213
  listFoldedMarkerSuffix: attrs["listFoldedMarkerSuffix"] ?? void 0,
208
214
  listMarkerSecondSlotOffsetTwips: attrs["listMarkerSecondSlotOffsetTwips"] ?? null
@@ -0,0 +1,5 @@
1
+ //#region src/prosemirror/noteReferenceOccurrenceId.d.ts
2
+ /** Occurrence identities are opaque, nonblank attribution tokens. */
3
+ declare const isNoteReferenceOccurrenceId: (value: unknown) => value is string;
4
+ //#endregion
5
+ export { isNoteReferenceOccurrenceId };
@@ -0,0 +1,5 @@
1
+ //#region src/prosemirror/noteReferenceOccurrenceId.ts
2
+ /** Occurrence identities are opaque, nonblank attribution tokens. */
3
+ const isNoteReferenceOccurrenceId = (value) => typeof value === "string" && value.trim().length > 0;
4
+ //#endregion
5
+ export { isNoteReferenceOccurrenceId };
@@ -0,0 +1,21 @@
1
+ import { Node } from "prosemirror-model";
2
+ import { Plugin, Transaction } from "prosemirror-state";
3
+ //#region src/prosemirror/noteReferenceOccurrences.d.ts
4
+ declare const NoteReferenceEditRefusal_base: import("better-result").TaggedErrorClass<"NoteReferenceEditRefusal">;
5
+ declare class NoteReferenceEditRefusal extends NoteReferenceEditRefusal_base<{
6
+ message: string;
7
+ }> {}
8
+ declare const NoteReferenceReplayDefect_base: import("better-result").TaggedErrorClass<"NoteReferenceReplayDefect">;
9
+ declare class NoteReferenceReplayDefect extends NoteReferenceReplayDefect_base<{
10
+ message: string;
11
+ }> {}
12
+ declare const markNoteReferenceReplay: (transaction: Transaction) => void;
13
+ declare const noteReferenceTransactionIssue: (transaction: Transaction) => NoteReferenceEditRefusal | NoteReferenceReplayDefect | undefined;
14
+ declare const mintNoteReferenceOccurrenceId: () => string;
15
+ /** Saving asserts the edit-owner invariant and emits each occurrence once. */
16
+ declare const coalesceNoteReferenceOccurrences: (paragraph: Node) => Node;
17
+ declare const assertNoteReferenceOccurrences: (doc: Node) => void;
18
+ /** Production edit boundary: refuse before committing an unsaveable occurrence. */
19
+ declare const noteReferenceOccurrencePlugin: () => Plugin<any>;
20
+ //#endregion
21
+ export { NoteReferenceEditRefusal, NoteReferenceReplayDefect, assertNoteReferenceOccurrences, coalesceNoteReferenceOccurrences, markNoteReferenceReplay, mintNoteReferenceOccurrenceId, noteReferenceOccurrencePlugin, noteReferenceTransactionIssue };
@@ -0,0 +1,185 @@
1
+ import { readFootnoteRefMarkAttrs } from "./attrs/index.js";
2
+ import { Result, TaggedError, panic } from "better-result";
3
+ import { Fragment, Mark, Slice } from "prosemirror-model";
4
+ import { Plugin } from "prosemirror-state";
5
+ import { isHistoryTransaction } from "prosemirror-history";
6
+ //#region src/prosemirror/noteReferenceOccurrences.ts
7
+ var NoteReferenceEditRefusal = class extends TaggedError("NoteReferenceEditRefusal") {};
8
+ var NoteReferenceReplayDefect = class extends TaggedError("NoteReferenceReplayDefect") {};
9
+ const NOTE_OCCURRENCE_REPLAY_META = "noteReferenceOccurrenceReplay";
10
+ const markNoteReferenceReplay = (transaction) => {
11
+ transaction.setMeta(NOTE_OCCURRENCE_REPLAY_META, true);
12
+ };
13
+ const NOTE_OCCURRENCE_REPORT_META = "noteReferenceOccurrenceReport";
14
+ const noteReferenceTransactionIssue = (transaction) => {
15
+ const issue = transaction.getMeta(NOTE_OCCURRENCE_REPORT_META);
16
+ return issue instanceof NoteReferenceEditRefusal || issue instanceof NoteReferenceReplayDefect ? issue : void 0;
17
+ };
18
+ const mintNoteReferenceOccurrenceId = () => crypto.randomUUID();
19
+ /** Identity establishes boundaries; text only validates the already bounded unit. */
20
+ const readOccurrences = (paragraph) => {
21
+ const referenceType = paragraph.type.schema.marks["footnoteRef"];
22
+ if (!referenceType || !paragraph.rangeHasMark(0, paragraph.content.size, referenceType)) return Result.ok([]);
23
+ const occurrences = [];
24
+ const seen = /* @__PURE__ */ new Set();
25
+ let previous;
26
+ let issue;
27
+ paragraph.forEach((node, from) => {
28
+ if (issue !== void 0) return;
29
+ const reference = node.marks.find((mark) => mark.type.name === "footnoteRef");
30
+ if (!reference) {
31
+ previous = void 0;
32
+ return;
33
+ }
34
+ const attrs = readFootnoteRefMarkAttrs(reference);
35
+ if (!attrs.ok || !node.isText) {
36
+ issue = "Note references require attributed text occurrences.";
37
+ return;
38
+ }
39
+ const id = attrs.value.occurrenceId;
40
+ if (previous?.id === id) {
41
+ if (!Mark.sameSet(previous.node.marks, node.marks)) {
42
+ issue = "A note reference cannot have different formatting or revision owners within one occurrence.";
43
+ return;
44
+ }
45
+ previous.text += node.text ?? "";
46
+ previous.to = from + node.nodeSize;
47
+ return;
48
+ }
49
+ if (seen.has(id)) {
50
+ issue = "A note-reference occurrence must remain contiguous.";
51
+ return;
52
+ }
53
+ seen.add(id);
54
+ previous = {
55
+ id,
56
+ node,
57
+ text: node.text ?? "",
58
+ from,
59
+ to: from + node.nodeSize
60
+ };
61
+ occurrences.push(previous);
62
+ });
63
+ for (const occurrence of occurrences) {
64
+ const reference = occurrence.node.marks.find((mark) => mark.type.name === "footnoteRef");
65
+ if (!reference) return panic("Missing note-reference mark in an occurrence.");
66
+ const attrs = readFootnoteRefMarkAttrs(reference);
67
+ if (!attrs.ok) return panic("Invalid attributed note-reference occurrence.");
68
+ if (occurrence.text !== String(attrs.value.id)) issue ??= "Edit the whole note reference; its rendered label is indivisible.";
69
+ }
70
+ return issue === void 0 ? Result.ok(occurrences) : Result.err(new NoteReferenceEditRefusal({ message: issue }));
71
+ };
72
+ /** Saving asserts the edit-owner invariant and emits each occurrence once. */
73
+ const coalesceNoteReferenceOccurrences = (paragraph) => {
74
+ if (saveOccurrenceValidator?.cachedIds(paragraph)?.length === 0) return paragraph;
75
+ const referenceType = paragraph.type.schema.marks["footnoteRef"];
76
+ if (!referenceType || !paragraph.rangeHasMark(0, paragraph.content.size, referenceType)) return paragraph;
77
+ const result = readOccurrences(paragraph);
78
+ if (result.isErr()) return panic(result.error.message);
79
+ if (result.value.every(({ node, from, to }) => node.nodeSize === to - from)) return paragraph;
80
+ const byStart = new Map(result.value.map((occurrence) => [occurrence.from, occurrence]));
81
+ const nodes = [];
82
+ let through = -1;
83
+ paragraph.forEach((node, from) => {
84
+ if (from < through) return;
85
+ const occurrence = byStart.get(from);
86
+ if (occurrence) {
87
+ nodes.push(node.type.schema.text(occurrence.text, node.marks));
88
+ through = occurrence.to;
89
+ } else nodes.push(node);
90
+ });
91
+ return paragraph.copy(Fragment.fromArray(nodes));
92
+ };
93
+ const createOccurrenceValidator = () => {
94
+ const cache = /* @__PURE__ */ new WeakMap();
95
+ const validate = (node) => {
96
+ const cached = cache.get(node);
97
+ if (cached) return cached;
98
+ const ids = /* @__PURE__ */ new Set();
99
+ const include = (id) => {
100
+ if (ids.has(id)) return new NoteReferenceEditRefusal({ message: "Separate note references require distinct occurrence identities." });
101
+ ids.add(id);
102
+ };
103
+ if (node.inlineContent) {
104
+ const own = readOccurrences(node);
105
+ if (own.isErr()) return Result.err(own.error);
106
+ for (const occurrence of own.value) {
107
+ const issue = include(occurrence.id);
108
+ if (issue) return Result.err(issue);
109
+ }
110
+ }
111
+ for (let index = 0; index < node.childCount; index += 1) {
112
+ const child = node.child(index);
113
+ if (child.isLeaf) continue;
114
+ const nested = validate(child);
115
+ if (nested.isErr()) return nested;
116
+ for (const id of nested.value) {
117
+ const issue = include(id);
118
+ if (issue) return Result.err(issue);
119
+ }
120
+ }
121
+ const result = Result.ok([...ids]);
122
+ cache.set(node, result);
123
+ return result;
124
+ };
125
+ return {
126
+ validate,
127
+ cachedIds: (node) => {
128
+ const facts = cache.get(node);
129
+ return facts?.isOk() ? facts.value : void 0;
130
+ },
131
+ validateSave: (doc) => {
132
+ const facts = cache.get(doc);
133
+ if (facts?.isOk() && facts.value.length === 0) return facts;
134
+ cache.delete(doc);
135
+ return validate(doc);
136
+ }
137
+ };
138
+ };
139
+ let saveOccurrenceValidator;
140
+ const assertNoteReferenceOccurrences = (doc) => {
141
+ const result = (saveOccurrenceValidator ??= createOccurrenceValidator()).validateSave(doc);
142
+ if (result.isErr()) panic(result.error.message);
143
+ };
144
+ /** Production edit boundary: refuse before committing an unsaveable occurrence. */
145
+ const noteReferenceOccurrencePlugin = () => {
146
+ const { validate } = createOccurrenceValidator();
147
+ return new Plugin({
148
+ filterTransaction(transaction) {
149
+ if (!transaction.docChanged) return true;
150
+ const result = validate(transaction.doc);
151
+ if (result.isOk()) return true;
152
+ const replay = isHistoryTransaction(transaction) || transaction.getMeta(NOTE_OCCURRENCE_REPLAY_META) === true;
153
+ transaction.setMeta(NOTE_OCCURRENCE_REPORT_META, replay ? new NoteReferenceReplayDefect({ message: result.error.message }) : result.error);
154
+ return replay;
155
+ },
156
+ props: { transformPasted(slice) {
157
+ const identities = /* @__PURE__ */ new Map();
158
+ const remint = (fragment) => {
159
+ const nodes = [];
160
+ fragment.forEach((node) => {
161
+ const marks = node.marks.map((mark) => {
162
+ if (mark.type.name !== "footnoteRef") return mark;
163
+ const attrs = readFootnoteRefMarkAttrs(mark);
164
+ if (!attrs.ok) return mark;
165
+ const id = attrs.value.occurrenceId;
166
+ let replacement = identities.get(id);
167
+ if (replacement === void 0) {
168
+ replacement = mintNoteReferenceOccurrenceId();
169
+ identities.set(id, replacement);
170
+ }
171
+ return mark.type.create({
172
+ ...mark.attrs,
173
+ occurrenceId: replacement
174
+ });
175
+ });
176
+ nodes.push(node.copy(remint(node.content)).mark(marks));
177
+ });
178
+ return Fragment.fromArray(nodes);
179
+ };
180
+ return new Slice(remint(slice.content), slice.openStart, slice.openEnd);
181
+ } }
182
+ });
183
+ };
184
+ //#endregion
185
+ export { NoteReferenceEditRefusal, NoteReferenceReplayDefect, assertNoteReferenceOccurrences, coalesceNoteReferenceOccurrences, markNoteReferenceReplay, mintNoteReferenceOccurrenceId, noteReferenceOccurrencePlugin, noteReferenceTransactionIssue };
@@ -2,6 +2,7 @@ import { withoutLostCommentThreads } from "../ai-edits/comment-lifecycle.js";
2
2
  import { visitCommentMarkers } from "../docx/commentAnchorIndex.js";
3
3
  import { isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
4
4
  import { pluginsForHeadlessRevisionResolution } from "../internal/headlessRevisionResolutionGuard.js";
5
+ import { addTrackedDeletionMark } from "./addTrackedDeletionMark.js";
5
6
  import { expectFootnoteRefMarkAttrs, expectParagraphAttrs } from "./attrs/index.js";
6
7
  import { rejectAIEditRevision } from "./commands/comments.js";
7
8
  import { proseDocToBlocks } from "./conversion/fromProseDoc.js";
@@ -13,6 +14,12 @@ import { schema, singletonManager } from "./schema/index.js";
13
14
  import { Transform } from "prosemirror-transform";
14
15
  import { EditorState } from "prosemirror-state";
15
16
  //#region src/prosemirror/noteReferenceReview.ts
17
+ /**
18
+ * A note goes with its reference. Deleting a note's reference while tracking
19
+ * changes deletes the note's text too, so rejecting that deletion must give
20
+ * the note its text back, whichever surface resolves it: the body editor
21
+ * resolves the reference, and this resolves the note's story to match.
22
+ */
16
23
  const noteKey = (kind, id) => `${kind}:${id}`;
17
24
  /**
18
25
  * The notes a story's text refers to, and whether each reference is pending
@@ -155,7 +162,13 @@ const deleteNoteWithReference = (transform, reference) => {
155
162
  kind: "del",
156
163
  info: markInfo
157
164
  });
158
- if (node.isInline && !deletion.type.isInSet(node.marks)) transform.addMark(position, position + node.nodeSize, deletion);
165
+ if (node.isInline && !deletion.type.isInSet(node.marks)) addTrackedDeletionMark({
166
+ insertionPolicy: "preserve-pending",
167
+ tr: transform,
168
+ from: position,
169
+ to: position + node.nodeSize,
170
+ mark: deletion
171
+ });
159
172
  return !node.isInline;
160
173
  });
161
174
  return transform;
@@ -3,6 +3,9 @@ import { StyleResolver } from "../styles/styleResolver.js";
3
3
  import { documentStylesKey, getDocumentBuiltInStyles, getDocumentStyleDefinitions, getDocumentStyleResolver } from "./documentStyleState.js";
4
4
  import { EditorState, Plugin } from "prosemirror-state";
5
5
  //#region src/prosemirror/plugins/documentStyles.d.ts
6
+ type DocumentStylesInput = document_d_exports.StyleDefinitions | StyleResolver | null | undefined;
7
+ /** Canonical projections own formatting; expose the resolver without PM repair transactions. */
8
+ declare const createDocumentStyleContextPlugin: (styles: DocumentStylesInput) => Plugin;
6
9
  /**
7
10
  * Create the plugin holding a StyleResolver for the document's `styles` for
8
11
  * the lifetime of the EditorState. The resolver is fixed per document load;
@@ -14,4 +17,4 @@ declare function createDocumentStylesPlugin(styles: document_d_exports.StyleDefi
14
17
  /** Reconfigure so ProseMirror replaces the keyed plugin's resolver state. */
15
18
  declare const withDocumentStyles: (state: EditorState, styles: document_d_exports.StyleDefinitions | StyleResolver | null | undefined) => EditorState;
16
19
  //#endregion
17
- export { createDocumentStylesPlugin, documentStylesKey, getDocumentBuiltInStyles, getDocumentStyleDefinitions, getDocumentStyleResolver, withDocumentStyles };
20
+ export { createDocumentStyleContextPlugin, createDocumentStylesPlugin, documentStylesKey, getDocumentBuiltInStyles, getDocumentStyleDefinitions, getDocumentStyleResolver, withDocumentStyles };
@@ -76,6 +76,21 @@ const bareResolver = () => {
76
76
  bareStyleResolver ??= createStyleResolver(void 0);
77
77
  return bareStyleResolver;
78
78
  };
79
+ const documentStyleContextSpec = (styles) => {
80
+ let resolver;
81
+ if (styles instanceof StyleResolver) resolver = styles;
82
+ else if (styles) resolver = createStyleResolver(styles);
83
+ else resolver = null;
84
+ return {
85
+ key: documentStylesKey,
86
+ state: {
87
+ init: () => resolver,
88
+ apply: (_tr, value) => value
89
+ }
90
+ };
91
+ };
92
+ /** Canonical projections own formatting; expose the resolver without PM repair transactions. */
93
+ const createDocumentStyleContextPlugin = (styles) => new Plugin(documentStyleContextSpec(styles));
79
94
  /**
80
95
  * Create the plugin holding a StyleResolver for the document's `styles` for
81
96
  * the lifetime of the EditorState. The resolver is fixed per document load;
@@ -84,16 +99,8 @@ const bareResolver = () => {
84
99
  * have one.
85
100
  */
86
101
  function createDocumentStylesPlugin(styles) {
87
- let resolver;
88
- if (styles instanceof StyleResolver) resolver = styles;
89
- else if (styles) resolver = createStyleResolver(styles);
90
- else resolver = null;
91
102
  return new Plugin({
92
- key: documentStylesKey,
93
- state: {
94
- init: () => resolver,
95
- apply: (_tr, value) => value
96
- },
103
+ ...documentStyleContextSpec(styles),
97
104
  appendTransaction: (transactions, _oldState, newState) => {
98
105
  const styleResolver = documentStylesKey.getState(newState) ?? bareResolver();
99
106
  const tr = resolveEditedParagraphStyles(transactions, newState, styleResolver, () => getDocumentNumbering(newState)) ?? newState.tr;
@@ -109,4 +116,4 @@ const withDocumentStyles = (state, styles) => {
109
116
  return state.reconfigure({ plugins: retained }).reconfigure({ plugins: [...retained, createDocumentStylesPlugin(styles)] });
110
117
  };
111
118
  //#endregion
112
- export { createDocumentStylesPlugin, documentStylesKey, getDocumentBuiltInStyles, getDocumentStyleDefinitions, getDocumentStyleResolver, withDocumentStyles };
119
+ export { createDocumentStyleContextPlugin, createDocumentStylesPlugin, documentStylesKey, getDocumentBuiltInStyles, getDocumentStyleDefinitions, getDocumentStyleResolver, withDocumentStyles };