@stll/folio-core 0.59.0 → 0.61.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 (226) hide show
  1. package/README.md +23 -1
  2. package/dist/ai-edits/apply.js +139 -32
  3. package/dist/ai-edits/headless.d.ts +3 -0
  4. package/dist/ai-edits/headless.js +57 -63
  5. package/dist/ai-edits/note-story-inventory.d.ts +7 -0
  6. package/dist/ai-edits/note-story-inventory.js +11 -0
  7. package/dist/ai-edits/read.js +16 -9
  8. package/dist/ai-edits/revisionStretches.js +52 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +6 -1
  10. package/dist/compare/reproducible-package.js +2 -1
  11. package/dist/compare/verification.js +1 -0
  12. package/dist/compat/eigenpal.d.ts +4 -2
  13. package/dist/compat/eigenpal.js +2 -2
  14. package/dist/controller/canonicalClipboard.js +2 -1
  15. package/dist/controller/canonicalListRendering.d.ts +4 -6
  16. package/dist/controller/canonicalListRendering.js +3 -157
  17. package/dist/controller/canonicalSession.js +3 -2
  18. package/dist/controller/canonicalStructure.js +10 -7
  19. package/dist/controller/fontReadiness.js +1 -1
  20. package/dist/controller/headerFooterEditorManager.d.ts +1 -0
  21. package/dist/controller/headerFooterEditorManager.js +11 -0
  22. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  23. package/dist/controller/hiddenEditorManager.js +3 -1
  24. package/dist/controller/layoutPipeline.d.ts +3 -1
  25. package/dist/controller/layoutPipeline.js +19 -4
  26. package/dist/controller/layoutSession.d.ts +4 -1
  27. package/dist/display-list/build/fontTable.js +7 -5
  28. package/dist/display-list/build/furniture.d.ts +1 -0
  29. package/dist/display-list/build/furniture.js +1 -0
  30. package/dist/display-list/build/imagePrimitives.js +3 -6
  31. package/dist/display-list/build/watermarkPrimitives.d.ts +2 -1
  32. package/dist/display-list/build/watermarkPrimitives.js +9 -3
  33. package/dist/display-list/editor/pageRenderer.js +1 -0
  34. package/dist/docx/bookmarkIdNormalization.d.ts +10 -0
  35. package/dist/docx/bookmarkIdNormalization.js +100 -0
  36. package/dist/docx/bookmarkParser.js +4 -2
  37. package/dist/docx/canonicalStoryRepack.js +3 -2
  38. package/dist/docx/commentTextParagraph.d.ts +6 -0
  39. package/dist/docx/commentTextParagraph.js +15 -0
  40. package/dist/docx/conformance.js +6 -1
  41. package/dist/docx/diagramPreview.js +2 -1
  42. package/dist/docx/documentParser.d.ts +13 -1
  43. package/dist/docx/documentParser.js +1 -1
  44. package/dist/docx/encryption/compoundFile.d.ts +0 -8
  45. package/dist/docx/encryption/compoundFile.js +2 -1
  46. package/dist/docx/encryption/cryptoBytes.d.ts +2 -2
  47. package/dist/docx/encryption/cryptoBytes.js +3 -18
  48. package/dist/docx/encryption/encryptionInfo.d.ts +0 -5
  49. package/dist/docx/encryption/encryptionInfo.js +2 -1
  50. package/dist/docx/ensureParaIds.js +2 -1
  51. package/dist/docx/groupDrawingParser.js +6 -1
  52. package/dist/docx/importedIdCollisions.d.ts +136 -0
  53. package/dist/docx/importedIdCollisions.js +215 -0
  54. package/dist/docx/listModelNormalization.d.ts +10 -0
  55. package/dist/docx/listModelNormalization.js +166 -0
  56. package/dist/docx/metadataPrivacy.js +3 -2
  57. package/dist/docx/metafileRaster.js +2 -1
  58. package/dist/docx/numberingParser.js +2 -2
  59. package/dist/docx/packageParts.js +6 -5
  60. package/dist/docx/paragraphParser.js +6 -4
  61. package/dist/docx/paragraphPropertySource.js +1 -2
  62. package/dist/docx/parseContext.d.ts +2 -1
  63. package/dist/docx/parseContext.js +1 -0
  64. package/dist/docx/parseWarningMessage.js +1 -0
  65. package/dist/docx/parser.js +10 -5
  66. package/dist/docx/preservedRunContent.js +11 -2
  67. package/dist/docx/relsParser.js +1 -1
  68. package/dist/docx/removeHeaderFooterParts.js +8 -7
  69. package/dist/docx/replyToComment.js +3 -13
  70. package/dist/docx/rezip.js +90 -90
  71. package/dist/docx/runParser.js +12 -6
  72. package/dist/docx/runPropertyPresence.d.ts +11 -0
  73. package/dist/docx/runPropertyPresence.js +14 -0
  74. package/dist/docx/selectedXmlIdentityScan.d.ts +10 -0
  75. package/dist/docx/selectedXmlIdentityScan.js +36 -0
  76. package/dist/docx/selectiveSave.js +19 -17
  77. package/dist/docx/selectiveXmlPatch.js +44 -52
  78. package/dist/docx/serializer/commentSerializer.d.ts +3 -1
  79. package/dist/docx/serializer/commentSerializer.js +11 -2
  80. package/dist/docx/serializer/runSerializer.js +5 -4
  81. package/dist/docx/serializer/tableSerializer.js +2 -1
  82. package/dist/docx/serializer/trackedChangeAttributes.d.ts +1 -3
  83. package/dist/docx/serializer/trackedChangeAttributes.js +1 -11
  84. package/dist/docx/server/applyDocxXmlPatchProposal.js +2 -2
  85. package/dist/docx/server/boundedArchive.js +2 -1
  86. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +2 -1
  87. package/dist/docx/server/inspectDocxPackage.js +5 -1
  88. package/dist/docx/server/validateDocxConformance.js +2 -1
  89. package/dist/docx/tableParser.js +7 -4
  90. package/dist/docx/tableRowPropertyPresence.d.ts +9 -0
  91. package/dist/docx/tableRowPropertyPresence.js +15 -0
  92. package/dist/docx/unzip.js +2 -1
  93. package/dist/docx/xmlEncoding.js +6 -5
  94. package/dist/fonts/embeddedFontFingerprint.d.ts +5 -0
  95. package/dist/fonts/embeddedFontFingerprint.js +18 -0
  96. package/dist/fonts/embeddedFonts.d.ts +1 -1
  97. package/dist/fonts/embeddedFonts.js +1 -1
  98. package/dist/fonts/fontsourceFaces.js +2 -1
  99. package/dist/fonts/headlessMeasure.js +0 -0
  100. package/dist/fonts/sfnt/parse.js +6 -9
  101. package/dist/fonts/sfnt/subset.js +2 -1
  102. package/dist/fonts/sfnt/tables.js +2 -3
  103. package/dist/fonts/sfnt/woff.js +2 -1
  104. package/dist/headless-layout.js +1 -0
  105. package/dist/index.d.ts +4 -2
  106. package/dist/index.js +2 -2
  107. package/dist/internal/revisionResolutionInline.d.ts +2 -1
  108. package/dist/internal/revisionResolutionInline.js +22 -1
  109. package/dist/internal/wholeStoryRevisionResolution.js +24 -7
  110. package/dist/layout-bridge/convert/flowConversionShared.d.ts +4 -0
  111. package/dist/layout-bridge/convert/footnoteLayout.d.ts +5 -1
  112. package/dist/layout-bridge/convert/footnoteLayout.js +16 -3
  113. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -0
  114. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -14
  115. package/dist/layout-bridge/convert/paragraphConversion.js +8 -1
  116. package/dist/layout-bridge/convert/paragraphRuns.js +2 -1
  117. package/dist/layout-bridge/convert/toFlowBlocks.js +25 -1
  118. package/dist/layout-engine/measure/cache.js +39 -7
  119. package/dist/layout-engine/measure/crossRunWords.js +12 -2
  120. package/dist/layout-engine/measure/lineTypography.js +1 -0
  121. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  122. package/dist/layout-engine/measure/listMarkerWidth.js +1 -0
  123. package/dist/layout-engine/measure/measureContainer.js +3 -4
  124. package/dist/layout-engine/measure/measureHelpers.d.ts +6 -9
  125. package/dist/layout-engine/measure/measureHelpers.js +21 -18
  126. package/dist/layout-engine/measure/measureParagraph.js +2 -0
  127. package/dist/layout-engine/measure/measureTypes.d.ts +2 -0
  128. package/dist/layout-engine/types.d.ts +4 -1
  129. package/dist/layout-painter/renderPage.d.ts +1 -0
  130. package/dist/layout-painter/renderPage.js +11 -3
  131. package/dist/layout-painter/renderParagraph.js +13 -3
  132. package/dist/layout-painter/renderWatermark.d.ts +1 -0
  133. package/dist/layout-painter/renderWatermark.js +7 -3
  134. package/dist/managers/AutoSaveManager.js +2 -1
  135. package/dist/managers/EditorModeManager.d.ts +10 -1
  136. package/dist/managers/EditorModeManager.js +15 -1
  137. package/dist/managers/TableSelectionManager.d.ts +1 -1
  138. package/dist/managers/autoSaveCodec.js +2 -6
  139. package/dist/managers/editorShortcuts.d.ts +2 -1
  140. package/dist/managers/editorShortcuts.js +2 -1
  141. package/dist/managers/reviewDisplayMode.d.ts +8 -0
  142. package/dist/managers/reviewDisplayMode.js +15 -0
  143. package/dist/paged-layout/pageText.d.ts +11 -2
  144. package/dist/paged-layout/pageText.js +8 -3
  145. package/dist/pdf/fonts.js +5 -14
  146. package/dist/pdf/images.js +3 -2
  147. package/dist/pdf/objects.d.ts +0 -11
  148. package/dist/pdf/objects.js +7 -9
  149. package/dist/pdf/writePdf.js +2 -1
  150. package/dist/prosemirror/canonicalSelectionRange.d.ts +4 -1
  151. package/dist/prosemirror/canonicalSelectionRange.js +9 -3
  152. package/dist/prosemirror/commands/comments.js +51 -13
  153. package/dist/prosemirror/containerFinalParagraph.d.ts +8 -8
  154. package/dist/prosemirror/containerFinalParagraph.js +8 -8
  155. package/dist/prosemirror/conversion/fromProseDoc.js +9 -4
  156. package/dist/prosemirror/conversion/toProseDoc.js +4 -4
  157. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +8 -2
  158. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +39 -3
  159. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.d.ts +8 -2
  160. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +4 -3
  161. package/dist/prosemirror/extensions/features/ImageDragExtension.js +4 -3
  162. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +4 -3
  163. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +9 -3
  164. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +6 -5
  165. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +8 -2
  166. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +11 -3
  167. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +4 -3
  168. package/dist/prosemirror/extensions/marks/markUtils.js +1 -1
  169. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +16 -2
  170. package/dist/prosemirror/findReplaceSelection.d.ts +11 -1
  171. package/dist/prosemirror/findReplaceSelection.js +15 -1
  172. package/dist/prosemirror/markupViewProjection.d.ts +10 -1
  173. package/dist/prosemirror/markupViewProjection.js +21 -3
  174. package/dist/prosemirror/pluginKeyOwner.d.ts +13 -0
  175. package/dist/prosemirror/pluginKeyOwner.js +35 -0
  176. package/dist/prosemirror/plugins/aiCitationDecorations.d.ts +11 -4
  177. package/dist/prosemirror/plugins/aiCitationDecorations.js +16 -3
  178. package/dist/prosemirror/plugins/aiSuggestionDecorations.d.ts +11 -4
  179. package/dist/prosemirror/plugins/aiSuggestionDecorations.js +18 -3
  180. package/dist/prosemirror/plugins/anonymizationDecorations.d.ts +11 -11
  181. package/dist/prosemirror/plugins/anonymizationDecorations.js +23 -13
  182. package/dist/prosemirror/plugins/autocompleteSuggestion.d.ts +8 -2
  183. package/dist/prosemirror/plugins/autocompleteSuggestion.js +27 -5
  184. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +8 -2
  185. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -3
  186. package/dist/prosemirror/plugins/createDocScanPlugin.d.ts +6 -5
  187. package/dist/prosemirror/plugins/createDocScanPlugin.js +1 -1
  188. package/dist/prosemirror/plugins/documentNumbering.js +5 -3
  189. package/dist/prosemirror/plugins/documentStyleState.d.ts +8 -2
  190. package/dist/prosemirror/plugins/documentStyleState.js +2 -2
  191. package/dist/prosemirror/plugins/documentStyles.js +1 -1
  192. package/dist/prosemirror/plugins/selectionTracker.d.ts +8 -2
  193. package/dist/prosemirror/plugins/selectionTracker.js +4 -3
  194. package/dist/prosemirror/plugins/suggestionMode.d.ts +8 -2
  195. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  196. package/dist/prosemirror/plugins/templateDirectives.d.ts +8 -2
  197. package/dist/prosemirror/plugins/templateDirectives.js +19 -4
  198. package/dist/prosemirror/plugins/templatePreviewValues.d.ts +8 -2
  199. package/dist/prosemirror/plugins/templatePreviewValues.js +27 -5
  200. package/dist/prosemirror/plugins/templateSlashMenu.d.ts +8 -2
  201. package/dist/prosemirror/plugins/templateSlashMenu.js +23 -5
  202. package/dist/prosemirror/plugins/transactionInvariants.js +29 -4
  203. package/dist/prosemirror/replacedAnnotations.d.ts +15 -1
  204. package/dist/prosemirror/replacedAnnotations.js +71 -12
  205. package/dist/prosemirror/runFormattingReconciliation.d.ts +3 -1
  206. package/dist/prosemirror/runFormattingReconciliation.js +10 -2
  207. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +5 -7
  208. package/dist/prosemirror/runIdentityAcrossRevisions.js +66 -38
  209. package/dist/prosemirror/runPropertyChangeResolution.js +13 -1
  210. package/dist/utils/base64.d.ts +1 -31
  211. package/dist/utils/base64.js +19 -33
  212. package/dist/utils/clipboard.js +5 -4
  213. package/dist/utils/colorResolver.js +11 -2
  214. package/dist/utils/findReplace.d.ts +5 -0
  215. package/dist/utils/fontLoader.d.ts +2 -2
  216. package/dist/utils/fontLoader.js +2 -7
  217. package/dist/utils/fontResolver.d.ts +7 -3
  218. package/dist/utils/fontResolver.js +6 -8
  219. package/dist/utils/replaceText.js +21 -4
  220. package/dist/utils/sliceText.d.ts +2 -0
  221. package/dist/utils/sliceText.js +2 -0
  222. package/dist/utils/textEncoding.d.ts +2 -0
  223. package/dist/utils/textEncoding.js +2 -0
  224. package/dist/utils/tiffConverter.d.ts +0 -9
  225. package/dist/utils/tiffConverter.js +2 -4
  226. package/package.json +73 -31
@@ -1,8 +1,32 @@
1
+ import { createPluginKeyOwner } from "../pluginKeyOwner.js";
1
2
  import { scanDirectives } from "./templateDirectives.js";
2
- import { Plugin, PluginKey } from "prosemirror-state";
3
+ import { Plugin } from "prosemirror-state";
3
4
  import { Decoration, DecorationSet } from "prosemirror-view";
4
5
  import { isFieldPath } from "@stll/template-conditions";
5
6
  //#region src/prosemirror/plugins/templatePreviewValues.ts
7
+ /**
8
+ * Template Preview Values Plugin
9
+ *
10
+ * Live fill preview for template documents: given a map of field path →
11
+ * typed value, every matching `{{path}}` marker is visually replaced by
12
+ * the value — the marker text is hidden by an inline decoration and the
13
+ * value is injected as a widget in its place. The document itself is
14
+ * never modified; clearing the preview restores the markers.
15
+ *
16
+ * The same preview hides conditional blocks the host reports as not
17
+ * applying: `conditions` maps an `{% if expr %}` expression to whether
18
+ * its block applies, and a `false` block is hidden from its opener
19
+ * through its `{% endif %}`. folio evaluates no expression.
20
+ *
21
+ * Two render modes: `highlighted` paints the substituted values with the
22
+ * preview accent so it is unmistakably a preview, and keeps every
23
+ * directive tag visible; `plain` renders the values as ordinary text and
24
+ * drops the tag paragraphs of a block the host has ruled on, so it
25
+ * approximates the generated document rather than the template.
26
+ *
27
+ * Updates are pushed via {@link setTemplatePreviewValues}; the host wires
28
+ * this to its fill inputs.
29
+ */
6
30
  /** Concatenated plain text of a preview value. */
7
31
  const templatePreviewValueText = (value) => typeof value === "string" ? value : value.runs.map((run) => run.text).join("");
8
32
  /** Stable identity of a preview value (text + formatting), for decoration
@@ -26,9 +50,7 @@ const templatePreviewHidesWholeBlock = (range, block) => range.from <= block.pmS
26
50
  const ORIGINAL_CLASS = "folio-template-preview-original";
27
51
  /** Hides a conditional block the host reported as not applying. */
28
52
  const HIDDEN_CLASS = "folio-template-preview-hidden";
29
- const KEY_HOLDER_SYMBOL = Symbol.for("stll.folio.templatePreviewValuesKey");
30
- const keyHolder = globalThis;
31
- const templatePreviewValuesKey = keyHolder[KEY_HOLDER_SYMBOL] ?? (keyHolder[KEY_HOLDER_SYMBOL] = new PluginKey("templatePreviewValues"));
53
+ const templatePreviewValuesKey = createPluginKeyOwner("templatePreviewValues");
32
54
  /** A rich span as DOM: text wrapped in `<em>` / `<strong>` per its flags. */
33
55
  function buildSpanNode(span) {
34
56
  let node = document.createTextNode(span.text);
@@ -267,7 +289,7 @@ function buildDecorationSet(doc, { entries, hidden }, mode) {
267
289
  }
268
290
  function createTemplatePreviewValuesPlugin() {
269
291
  return new Plugin({
270
- key: templatePreviewValuesKey,
292
+ key: templatePreviewValuesKey.createKey(),
271
293
  state: {
272
294
  init() {
273
295
  return {
@@ -1,4 +1,4 @@
1
- import { EditorState, Plugin, PluginKey, Transaction } from "prosemirror-state";
1
+ import { EditorState, Plugin, Transaction } from "prosemirror-state";
2
2
  //#region src/prosemirror/plugins/templateSlashMenu.d.ts
3
3
  type TemplateSlashMenuState = {
4
4
  active: false;
@@ -34,7 +34,13 @@ type TemplateSlashMenuPluginOptions = {
34
34
  */
35
35
  onKeyAction?: (action: TemplateSlashMenuKeyAction) => boolean;
36
36
  };
37
- declare const templateSlashMenuKey: PluginKey<TemplateSlashMenuState>;
37
+ declare const templateSlashMenuKey: Readonly<{
38
+ key: string;
39
+ createKey: () => import("prosemirror-state").PluginKey<TemplateSlashMenuState>;
40
+ owns: (plugin: Plugin<TemplateSlashMenuState>) => boolean;
41
+ get: (state: EditorState) => Plugin<TemplateSlashMenuState> | undefined;
42
+ getState: (state: EditorState) => TemplateSlashMenuState | undefined;
43
+ }>;
38
44
  declare const templateSlashMenuPlugin: (options?: TemplateSlashMenuPluginOptions) => Plugin<TemplateSlashMenuState>;
39
45
  declare const getTemplateSlashMenu: (state: EditorState) => TemplateSlashMenuState;
40
46
  /** Clear the trigger. Set on the host's marker-insert transaction so the
@@ -1,6 +1,26 @@
1
+ import { createPluginKeyOwner } from "../pluginKeyOwner.js";
1
2
  import { getTemplateDirectives } from "./templateDirectives.js";
2
- import { Plugin, PluginKey } from "prosemirror-state";
3
+ import { Plugin } from "prosemirror-state";
3
4
  //#region src/prosemirror/plugins/templateSlashMenu.ts
5
+ /**
6
+ * Template slash-menu trigger plugin.
7
+ *
8
+ * Watches the template editor for a `/` typed at a marker boundary (start of a
9
+ * text block or right after whitespace) and tracks the query the author types
10
+ * after it. The plugin owns only the *trigger* state — `{active, from, query}` —
11
+ * and the keyboard contract while active; the floating menu UI and the actual
12
+ * marker insertion live in the host (template-studio), which subscribes via
13
+ * {@link TemplateSlashMenuPluginOptions.onChange} and resolves keys via
14
+ * {@link TemplateSlashMenuPluginOptions.onKeyAction}.
15
+ *
16
+ * The trigger guard is deliberately strict so the menu never misfires inside
17
+ * ordinary legal prose: dates (`30/06`), `and/or`, and URLs all have a non-space
18
+ * char before the `/`, so only a leading or post-whitespace `/` opens the menu.
19
+ *
20
+ * Insertion is host-driven: a doc-changing transaction the host dispatches to
21
+ * insert the marker carries a `clear` meta so the plugin tears down without the
22
+ * generic doc-change reset racing the host's selection move.
23
+ */
4
24
  const IDLE = {
5
25
  active: false,
6
26
  from: null,
@@ -8,9 +28,7 @@ const IDLE = {
8
28
  query: ""
9
29
  };
10
30
  const QUERY_CHAR = /[\p{L}\p{N}_.-]/u;
11
- const KEY_HOLDER_SYMBOL = Symbol.for("stll.folio.templateSlashMenuKey");
12
- const keyHolder = globalThis;
13
- const templateSlashMenuKey = keyHolder[KEY_HOLDER_SYMBOL] ?? (keyHolder[KEY_HOLDER_SYMBOL] = new PluginKey("templateSlashMenu"));
31
+ const templateSlashMenuKey = createPluginKeyOwner("templateSlashMenu");
14
32
  /** Whether the char immediately before `pos` is a marker boundary (start of the
15
33
  * text block or whitespace). This is the misfire guard: a `/` mid-token (dates,
16
34
  * URLs, and/or) has a word char before it and never opens the menu. */
@@ -78,7 +96,7 @@ const tryOpenTrigger = (view) => {
78
96
  };
79
97
  const templateSlashMenuPlugin = (options = {}) => {
80
98
  return new Plugin({
81
- key: templateSlashMenuKey,
99
+ key: templateSlashMenuKey.createKey(),
82
100
  state: {
83
101
  init: () => IDLE,
84
102
  apply: (tr, value, _oldState, newState) => {
@@ -1,16 +1,41 @@
1
+ import { sliceText } from "../../utils/sliceText.js";
1
2
  import { expectParagraphAttrs } from "../attrs/index.js";
2
3
  import { effectiveParagraphNumberingReference, paragraphNumberingSourcesFromBoundary } from "../numberingAttr.js";
4
+ import { createPluginKeyOwner } from "../pluginKeyOwner.js";
3
5
  import { validateProseMirrorDocument } from "../validation.js";
4
6
  import { getDocumentNumbering, hasDocumentNumbering } from "./documentNumbering.js";
5
- import { Plugin, PluginKey } from "prosemirror-state";
7
+ import { Plugin } from "prosemirror-state";
6
8
  //#region src/prosemirror/plugins/transactionInvariants.ts
9
+ /**
10
+ * Transaction invariants: a development and test aid that checks the editor
11
+ * document after every transaction, so a command that leaves a document the
12
+ * save path will refuse fails where it happens rather than at save.
13
+ *
14
+ * Off by default. {@link ExtensionManager} installs it when
15
+ * `globalThis.__folioTransactionInvariants` is `{ enabled: true }` at the time
16
+ * the runtime is initialized; the core test preload sets that flag, which turns
17
+ * every editor test built on the extension manager into a validity check. A
18
+ * host may set it in a development build. Production code never reads it
19
+ * otherwise.
20
+ *
21
+ * The checks are the ones the document-model validator makes that the editor
22
+ * state can answer on its own:
23
+ * - the ProseMirror document is structurally valid (node and mark attributes,
24
+ * bookmark pairing), incrementally cached per node;
25
+ * - every paragraph numbering reference names an instance the document's
26
+ * numbering defines.
27
+ */
7
28
  /** Whether new extension-manager runtimes install the invariant plugin. */
8
29
  const areTransactionInvariantsEnabled = () => globalThis.__folioTransactionInvariants?.enabled === true;
9
30
  const MAX_REPORTED_STEPS_LENGTH = 4e3;
10
31
  const describeSteps = (transactions) => {
11
32
  const steps = transactions.flatMap((transaction) => transaction.steps.map((step) => step.toJSON()));
12
33
  const json = JSON.stringify(steps);
13
- return json.length > MAX_REPORTED_STEPS_LENGTH ? `${json.slice(0, MAX_REPORTED_STEPS_LENGTH)}… (${json.length} characters)` : json;
34
+ return json.length > MAX_REPORTED_STEPS_LENGTH ? `${sliceText({
35
+ text: json,
36
+ units: "codeUnits",
37
+ end: MAX_REPORTED_STEPS_LENGTH
38
+ })}… (${json.length} characters)` : json;
14
39
  };
15
40
  var TransactionInvariantError = class extends Error {
16
41
  issues;
@@ -69,14 +94,14 @@ const introducedIssues = (before, after, transactions) => {
69
94
  return false;
70
95
  });
71
96
  };
72
- const transactionInvariantsKey = new PluginKey("transactionInvariants");
97
+ const transactionInvariantsKey = createPluginKeyOwner("transactionInvariants");
73
98
  /**
74
99
  * Throw a {@link TransactionInvariantError} naming the offending steps when a
75
100
  * document-changing transaction leaves the state with an issue it did not
76
101
  * have before.
77
102
  */
78
103
  const createTransactionInvariantPlugin = () => new Plugin({
79
- key: transactionInvariantsKey,
104
+ key: transactionInvariantsKey.createKey(),
80
105
  appendTransaction(transactions, oldState, newState) {
81
106
  if (!transactions.some((transaction) => transaction.docChanged)) return null;
82
107
  const issues = checkEditorStateInvariants(newState);
@@ -19,6 +19,20 @@ declare const NON_INCLUSIVE_MARK_DISPOSITION: {
19
19
  };
20
20
  /** The mark names in the schema that a replacement must not silently drop. */
21
21
  declare const nonInclusiveMarkNames: (schema: Schema) => string[];
22
+ /** One typed point for a comment whose aligned coverage has no content left. */
23
+ declare const commentPoint: (schema: Schema, id: number) => Node;
24
+ type ResolvedCommentPointsOptions = {
25
+ doc: Node;
26
+ removes: (node: Node, position: number) => boolean;
27
+ retiresStructure: (node: Node, position: number) => boolean;
28
+ };
29
+ /** The same collector serves standalone commands and the existing bulk walk. */
30
+ declare const createResolvedCommentPointCollector: ({ removes, retiresStructure }: Omit<ResolvedCommentPointsOptions, "doc">) => {
31
+ visit: (node: Node, position: number, previous?: Node) => boolean;
32
+ finish: () => Map<number, number>;
33
+ };
34
+ /** Point fallback excludes comments belonging to retired blocks and table cells. */
35
+ declare const resolvedCommentPoints: ({ doc, removes, retiresStructure }: ResolvedCommentPointsOptions) => Map<number, number>;
22
36
  type ReplacedAnnotations = {
23
37
  /** Marks the replacement must carry, in document order. */
24
38
  carried: readonly Mark[];
@@ -88,4 +102,4 @@ declare const annotatedReplacement: ({ annotations: { carried, leading, trailing
88
102
  */
89
103
  declare const inheritedReplacementMarks: (doc: Node, from: number, to: number) => readonly Mark[];
90
104
  //#endregion
91
- export { NON_INCLUSIVE_MARK_DISPOSITION, ReplacedAnnotations, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, surveyReplacedAnnotations };
105
+ export { NON_INCLUSIVE_MARK_DISPOSITION, ReplacedAnnotations, addCarriedMarks, annotatedReplacement, commentPoint, commentsAcrossBlockBoundary, commentsForTrackedInsertion, createResolvedCommentPointCollector, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, resolvedCommentPoints, surveyReplacedAnnotations };
@@ -1,9 +1,11 @@
1
+ import { expectParagraphAttrs, expectTableCellAttrs, expectTableRowAttrs } from "./attrs/index.js";
1
2
  import { BOOKMARK_BOUNDARY_NODE_NAME } from "./extensions/nodes/BookmarkBoundaryExtension.js";
2
3
  import { COMMENT_REFERENCE_NODE_NAME } from "./extensions/nodes/CommentReferenceExtension.js";
3
4
  import { MOVE_RANGE_BOUNDARY_NODE_NAME } from "./extensions/nodes/MoveRangeBoundaryExtension.js";
4
5
  import { RANGE_ANCHOR_NODE_NAME } from "./extensions/nodes/RangeAnchorExtension.js";
5
6
  import { TEXT_BOX_ANCHOR_NODE_NAME } from "./extensions/nodes/TextBoxAnchorExtension.js";
6
7
  import { readMoveRangeBoundaryAttrs } from "./moveRangeBoundaryAttrs.js";
8
+ import { readRangeAnchorAttrs } from "./rangeAnchorAttrs.js";
7
9
  import { isZeroWidthAnchor } from "./zeroWidthAnchors.js";
8
10
  import { Fragment, Mark } from "prosemirror-model";
9
11
  //#region src/prosemirror/replacedAnnotations.ts
@@ -21,15 +23,12 @@ import { Fragment, Mark } from "prosemirror-model";
21
23
  * hyperlink simply stops being a link. Zero-width anchors fare worse still: a
22
24
  * plain replace deletes them outright.
23
25
  *
24
- * Word moves a range start that sits inside a replaced span to the beginning
25
- * of the replacement and lets the range keep covering the new text; it keeps
26
- * a link across a replacement of its display text. A range wholly inside the
27
- * span collapses onto the replacement, and disappears only when every
28
- * character it covered is gone. One rule per marker kind, and that is the
29
- * whole contract:
26
+ * Text replacement preserves a comment's aligned coverage. Unchanged text
27
+ * keeps its ranges; inserted text takes the comments of the replaced aligned
28
+ * span. A range with no remaining content becomes a point at the replacement
29
+ * start. The applier decides alignment once for direct and tracked edits.
30
30
  *
31
- * - `comment` mark: the replacement carries every comment the span covered, so
32
- * a range that started inside it starts at the replacement instead.
31
+ * - `comment` mark: carried over its aligned replacement span.
33
32
  * - `hyperlink` mark: the replacement carries the first link the span held, so
34
33
  * replacing a link's display text leaves the link on the new text. A span
35
34
  * crossing two links keeps the one it opens in, because the mark excludes
@@ -66,9 +65,8 @@ import { Fragment, Mark } from "prosemirror-model";
66
65
  * - every other mark: formatting, and left to boundary inheritance, which
67
66
  * already gives the replacement the formatting of the text it replaces.
68
67
  *
69
- * Deleting the whole of a comment's span leaves it no mark anywhere, so
70
- * `CommentReferenceExtension` drops the orphan reference and the save drops
71
- * the comment: the one case where a range is meant to disappear.
68
+ * Removing the owning block removes its comment. Replacing its last covered
69
+ * text within a surviving block retains a point rather than dropping it.
72
70
  */
73
71
  /**
74
72
  * What a replacement does with each mark the schema declares
@@ -90,6 +88,67 @@ const NON_INCLUSIVE_MARK_DISPOSITION = {
90
88
  /** The mark names in the schema that a replacement must not silently drop. */
91
89
  const nonInclusiveMarkNames = (schema) => Object.entries(schema.marks).filter(([, type]) => type.spec.inclusive === false).map(([name]) => name);
92
90
  const isCarried = (mark) => NON_INCLUSIVE_MARK_DISPOSITION[mark.type.name] === "carry";
91
+ /** One typed point for a comment whose aligned coverage has no content left. */
92
+ const commentPoint = (schema, id) => schema.node(RANGE_ANCHOR_NODE_NAME, {
93
+ start: {
94
+ type: "commentRangeStart",
95
+ id
96
+ },
97
+ end: {
98
+ type: "commentRangeEnd",
99
+ id
100
+ }
101
+ });
102
+ /** The same collector serves standalone commands and the existing bulk walk. */
103
+ const createResolvedCommentPointCollector = ({ removes, retiresStructure }) => {
104
+ const points = /* @__PURE__ */ new Map();
105
+ const retained = /* @__PURE__ */ new Set();
106
+ const visit = (node, position, previous) => {
107
+ if (node.type.name === "paragraph") {
108
+ const mark = expectParagraphAttrs(node).pPrMark;
109
+ const precedingMark = previous?.type.name === "paragraph" ? expectParagraphAttrs(previous).pPrMark : null;
110
+ if ((mark?.kind === "del" || mark?.kind === "moveFrom") && retiresStructure(node, position) || (precedingMark?.kind === "del" || precedingMark?.kind === "moveFrom") && previous !== void 0 && retiresStructure(previous, position - previous.nodeSize)) {
111
+ let survivingContent = false;
112
+ node.descendants((inline, offset) => {
113
+ if (inline.isInline && !isZeroWidthAnchor(inline) && !removes(inline, position + 1 + offset)) survivingContent = true;
114
+ return !survivingContent;
115
+ });
116
+ if (!survivingContent) return false;
117
+ }
118
+ }
119
+ if (node.type.name === "tableRow" && expectTableRowAttrs(node).trDel && retiresStructure(node, position)) return false;
120
+ if ((node.type.name === "tableCell" || node.type.name === "tableHeader") && expectTableCellAttrs(node).cellMarker?.kind === "del" && retiresStructure(node, position)) return false;
121
+ if (node.type.name === "rangeAnchor") {
122
+ const attrs = readRangeAnchorAttrs(node);
123
+ if (attrs.ok && attrs.value.start.type === "commentRangeStart") retained.add(attrs.value.start.id);
124
+ }
125
+ if (!node.isInline || isZeroWidthAnchor(node)) return true;
126
+ for (const mark of node.marks) {
127
+ if (mark.type.name !== "comment") continue;
128
+ const id = mark.attrs["commentId"];
129
+ if (!removes(node, position)) retained.add(id);
130
+ else if (!points.has(id)) points.set(id, position);
131
+ }
132
+ return true;
133
+ };
134
+ const finish = () => {
135
+ for (const id of retained) points.delete(id);
136
+ return points;
137
+ };
138
+ return {
139
+ visit,
140
+ finish
141
+ };
142
+ };
143
+ /** Point fallback excludes comments belonging to retired blocks and table cells. */
144
+ const resolvedCommentPoints = ({ doc, removes, retiresStructure }) => {
145
+ const collector = createResolvedCommentPointCollector({
146
+ removes,
147
+ retiresStructure
148
+ });
149
+ doc.descendants((node, position, parent, index) => collector.visit(node, position, index > 0 ? parent?.maybeChild(index - 1) ?? void 0 : void 0));
150
+ return collector.finish();
151
+ };
93
152
  /** Anchors an edit must put back, and where relative to the replacement. */
94
153
  const ANCHOR_PLACEMENT = {
95
154
  [COMMENT_REFERENCE_NODE_NAME]: "after",
@@ -349,4 +408,4 @@ const inheritedReplacementMarks = (doc, from, to) => {
349
408
  return (to === from ? $from.marks() : $from.marksAcross(doc.resolve(to))) ?? Mark.none;
350
409
  };
351
410
  //#endregion
352
- export { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, surveyReplacedAnnotations };
411
+ export { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentPoint, commentsAcrossBlockBoundary, commentsForTrackedInsertion, createResolvedCommentPointCollector, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, resolvedCommentPoints, surveyReplacedAnnotations };
@@ -9,6 +9,8 @@ type ReadAuthoredRunFormattingOptions = {
9
9
  };
10
10
  /** Read authored run properties without confusing inherited visuals for direct formatting. */
11
11
  declare const readAuthoredRunFormatting: ({ context, marks, styleResolver }: ReadAuthoredRunFormattingOptions) => document_d_exports.TextFormatting;
12
+ /** Preserve the authored distinction between an absent and empty property set in revisions. */
13
+ declare const readAuthoredRunPropertySnapshot: (options: ReadAuthoredRunFormattingOptions) => document_d_exports.TextFormatting | undefined;
12
14
  type ReconcileRunFormattingMarksOptions = {
13
15
  authoredFormatting: document_d_exports.TextFormatting;
14
16
  context: ParagraphRunStyleContext;
@@ -22,4 +24,4 @@ type ReconcileRunFormattingMarksOptions = {
22
24
  */
23
25
  declare const reconcileRunFormattingMarks: ({ authoredFormatting, context, node, styleResolver }: ReconcileRunFormattingMarksOptions) => readonly Mark[];
24
26
  //#endregion
25
- export { readAuthoredRunFormatting, reconcileRunFormattingMarks };
27
+ export { readAuthoredRunFormatting, readAuthoredRunPropertySnapshot, reconcileRunFormattingMarks };
@@ -1,8 +1,9 @@
1
1
  import { mergeTextFormatting } from "../utils/textFormattingMerge.js";
2
- import { expectCharacterStyleMarkAttrs } from "./attrs/index.js";
2
+ import { expectCharacterStyleMarkAttrs, expectRunIdentityMarkAttrs } from "./attrs/index.js";
3
3
  import { textFormattingToMarks } from "./extensions/marks/markUtils.js";
4
4
  import { marksToTextFormatting } from "./runFormattingFromMarks.js";
5
5
  import { RUN_FORMATTING_MARK_NAMES } from "./runFormattingMarkNames.js";
6
+ import { RUN_IDENTITY_MARK_NAME } from "./runIdentity.js";
6
7
  import { getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, isParagraphMarkSuppressionEligible, paragraphFormattingForRun, resolveEffectiveRunStyleFormatting } from "./runStyleFormatting.js";
7
8
  import { Mark } from "prosemirror-model";
8
9
  //#region src/prosemirror/runFormattingReconciliation.ts
@@ -14,6 +15,13 @@ const readAuthoredRunFormatting = ({ context, marks, styleResolver }) => marksTo
14
15
  paragraphMarkPrecedesStyle: context.paragraphMarkPrecedesStyle,
15
16
  ...styleResolver !== void 0 ? { styleResolver } : {}
16
17
  });
18
+ /** Preserve the authored distinction between an absent and empty property set in revisions. */
19
+ const readAuthoredRunPropertySnapshot = (options) => {
20
+ const formatting = readAuthoredRunFormatting(options);
21
+ if (Object.keys(formatting).length > 0) return formatting;
22
+ const identity = options.marks.find(({ type }) => type.name === RUN_IDENTITY_MARK_NAME);
23
+ return identity && expectRunIdentityMarkAttrs(identity).emptyFormatting ? formatting : void 0;
24
+ };
17
25
  /**
18
26
  * Rebuild a run's physical formatting marks from its authored properties and
19
27
  * current style context. This is the inverse of {@link readAuthoredRunFormatting}:
@@ -54,4 +62,4 @@ const reconcileRunFormattingMarks = ({ authoredFormatting, context, node, styleR
54
62
  ]);
55
63
  };
56
64
  //#endregion
57
- export { readAuthoredRunFormatting, reconcileRunFormattingMarks };
65
+ export { readAuthoredRunFormatting, readAuthoredRunPropertySnapshot, reconcileRunFormattingMarks };
@@ -1,10 +1,8 @@
1
1
  import { Node } from "prosemirror-model";
2
2
  //#region src/prosemirror/runIdentityAcrossRevisions.d.ts
3
- /**
4
- * `nodes`, one inline container's content, with each run a revision cut given
5
- * the identity of the run it was cut from. A control is a container of its
6
- * own. `nodes` itself comes back when nothing changes.
7
- */
8
- declare const continueRunIdentitiesAcrossRevisions: (nodes: readonly Node[]) => readonly Node[];
3
+ /** The continuation policy covers every declared source-carrier mark. */
4
+ declare const SOURCE_CARRIER_MARK_NAMES: ("hyperlink" | "runIdentity")[];
5
+ /** Every source-carrier family uses the same revision-boundary continuation policy. */
6
+ declare const continueSourceCarrierIdentitiesAcrossRevisions: (nodes: readonly Node[]) => readonly Node[];
9
7
  //#endregion
10
- export { continueRunIdentitiesAcrossRevisions };
8
+ export { SOURCE_CARRIER_MARK_NAMES, continueSourceCarrierIdentitiesAcrossRevisions };
@@ -1,52 +1,73 @@
1
- import { expectRunIdentityMarkAttrs } from "./attrs/index.js";
1
+ import { expectHyperlinkMarkAttrs, expectRunIdentityMarkAttrs } from "./attrs/index.js";
2
2
  import "./extensions/nodes/SdtExtension.js";
3
3
  import { RUN_IDENTITY_MARK_NAME } from "./runIdentity.js";
4
4
  import { Fragment, Mark } from "prosemirror-model";
5
5
  //#region src/prosemirror/runIdentityAcrossRevisions.ts
6
6
  /**
7
- * One run identity for the pieces of a `w:r` a tracked change cut.
8
- *
9
- * A revision inside a run splits it: `<w:r a>Sel</w:r><w:del><w:r a>ler</w:r></w:del>`,
10
- * `<w:r a>Sel</w:r><w:ins>…</w:ins><w:r a>ler</w:r>`, or a `w:rPrChange` on
11
- * one piece of the run only. Read back, each
12
- * `w:r` is its own authored run, so resolving the revision would leave two
13
- * runs where the source had one. The package cannot say whether the two were
14
- * one run, but runs that carry the same attributes and that only revised
15
- * content holds apart are what such a cut writes, so the later piece takes the
16
- * earlier one's identity. The pieces stay separate runs while the revision
17
- * holds them apart, and become one run once it is resolved and they state the
18
- * same properties again (a run joins its owner only then, see `fromProseDoc`).
19
- *
20
- * Runs the package writes side by side under the same revision state keep
21
- * their own identities.
7
+ * One source identity for matching runs and hyperlinks a revision cut apart.
8
+ * Serialized fragments mint separate identities on reopen. Matching fragments
9
+ * across a revision boundary or revised-only bridge take the earlier identity,
10
+ * so resolution can rejoin them. Every non-identity attribute must match.
11
+ * Adjacent carriers under the same revision state keep their source identities.
22
12
  */
23
13
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set([
24
14
  "insertion",
25
15
  "deletion",
26
16
  "runPropertyChange"
27
17
  ]);
28
- const identityOf = (node) => node.marks.find((mark) => mark.type.name === RUN_IDENTITY_MARK_NAME);
18
+ const CARRIER_OWNERS = [{
19
+ markName: RUN_IDENTITY_MARK_NAME,
20
+ read: (mark) => {
21
+ const attrs = expectRunIdentityMarkAttrs(mark);
22
+ return {
23
+ id: attrs.id,
24
+ payloadKey: JSON.stringify({
25
+ ...attrs,
26
+ id: 0
27
+ })
28
+ };
29
+ }
30
+ }, {
31
+ markName: "hyperlink",
32
+ read: (mark) => {
33
+ const attrs = expectHyperlinkMarkAttrs(mark);
34
+ if (typeof attrs._docxHyperlinkIndex !== "number") return void 0;
35
+ return {
36
+ id: attrs._docxHyperlinkIndex,
37
+ payloadKey: JSON.stringify({
38
+ ...attrs,
39
+ _docxHyperlinkIndex: 0
40
+ })
41
+ };
42
+ }
43
+ }];
44
+ /** The continuation policy covers every declared source-carrier mark. */
45
+ const SOURCE_CARRIER_MARK_NAMES = CARRIER_OWNERS.map(({ markName }) => markName);
46
+ const identityOf = (node, owner) => {
47
+ const mark = node.marks.find((candidate) => candidate.type.name === owner.markName);
48
+ if (!mark) return void 0;
49
+ const identity = owner.read(mark);
50
+ return identity === void 0 ? void 0 : {
51
+ mark,
52
+ ...identity
53
+ };
54
+ };
29
55
  const revisionMarksOf = (node) => node.marks.filter((mark) => REVISION_MARK_NAMES.has(mark.type.name));
30
- /** The identity's attrs without the id: what the run carries. */
31
- const payloadKey = (identity) => JSON.stringify({
32
- ...expectRunIdentityMarkAttrs(identity),
33
- id: 0
34
- });
35
56
  /** Whether `right` continues `left`'s run across a revision boundary. */
36
- const continuesRun = ({ left, right, bridged }) => {
37
- const leftIdentity = identityOf(left);
38
- const rightIdentity = identityOf(right);
39
- if (!leftIdentity || !rightIdentity || leftIdentity.eq(rightIdentity)) return false;
40
- return (bridged || !Mark.sameSet(revisionMarksOf(left), revisionMarksOf(right))) && payloadKey(leftIdentity) === payloadKey(rightIdentity);
57
+ const continuesCarrier = ({ left, right, bridged, owner }) => {
58
+ const leftIdentity = identityOf(left, owner);
59
+ const rightIdentity = identityOf(right, owner);
60
+ if (!leftIdentity || !rightIdentity || leftIdentity.mark.eq(rightIdentity.mark)) return false;
61
+ return (bridged || !Mark.sameSet(revisionMarksOf(left), revisionMarksOf(right))) && leftIdentity.payloadKey === rightIdentity.payloadKey;
41
62
  };
42
63
  /**
43
- * `nodes`, one inline container's content, with each run a revision cut given
44
- * the identity of the run it was cut from. A control is a container of its
64
+ * `nodes`, one inline container's content, with each revision fragment given
65
+ * the identity of its matching source carrier. A control is a container of its
45
66
  * own. `nodes` itself comes back when nothing changes.
46
67
  */
47
- const continueRunIdentitiesAcrossRevisions = (nodes) => {
68
+ const continueIdentities = (nodes, owner) => {
48
69
  const renamed = /* @__PURE__ */ new Map();
49
- /** The last run with an identity, while only revised content follows it. */
70
+ /** The last carrier with an identity, while only revised content follows it. */
50
71
  let anchor;
51
72
  let bridged = false;
52
73
  let changed = false;
@@ -57,25 +78,26 @@ const continueRunIdentitiesAcrossRevisions = (nodes) => {
57
78
  node.forEach((child) => {
58
79
  children.push(child);
59
80
  });
60
- const continued = continueRunIdentitiesAcrossRevisions(children);
81
+ const continued = continueIdentities(children, owner);
61
82
  if (continued === children) return node;
62
83
  changed = true;
63
84
  return node.copy(Fragment.fromArray([...continued]));
64
85
  }
65
- const identity = identityOf(node);
86
+ const identity = identityOf(node, owner);
66
87
  if (!identity) {
67
88
  if (anchor && revisionMarksOf(node).length > 0) bridged = true;
68
89
  else anchor = void 0;
69
90
  return node;
70
91
  }
71
- const id = expectRunIdentityMarkAttrs(identity).id;
92
+ const id = identity.id;
72
93
  let replacement = renamed.get(id);
73
- if (!replacement && anchor && continuesRun({
94
+ if (!replacement && anchor && continuesCarrier({
74
95
  left: anchor,
75
96
  right: node,
76
- bridged
97
+ bridged,
98
+ owner
77
99
  })) {
78
- replacement = identityOf(anchor);
100
+ replacement = identityOf(anchor, owner)?.mark;
79
101
  if (replacement) renamed.set(id, replacement);
80
102
  }
81
103
  const next = replacement ? node.mark(replacement.addToSet(node.marks)) : node;
@@ -86,5 +108,11 @@ const continueRunIdentitiesAcrossRevisions = (nodes) => {
86
108
  });
87
109
  return changed ? result : nodes;
88
110
  };
111
+ /** Every source-carrier family uses the same revision-boundary continuation policy. */
112
+ const continueSourceCarrierIdentitiesAcrossRevisions = (nodes) => {
113
+ let continued = nodes;
114
+ for (const owner of CARRIER_OWNERS) continued = continueIdentities(continued, owner);
115
+ return continued;
116
+ };
89
117
  //#endregion
90
- export { continueRunIdentitiesAcrossRevisions };
118
+ export { SOURCE_CARRIER_MARK_NAMES, continueSourceCarrierIdentitiesAcrossRevisions };
@@ -1,6 +1,8 @@
1
+ import { expectRunIdentityMarkAttrs } from "./attrs/index.js";
1
2
  import { runFormattingInlineAtomDisposition } from "./runFormattingInlineCarriers.js";
2
3
  import { RUN_FORMATTING_MARK_NAMES } from "./runFormattingMarkNames.js";
3
4
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "./runFormattingReconciliation.js";
5
+ import { RUN_IDENTITY_MARK_NAME, runIdentityAttrs } from "./runIdentity.js";
4
6
  //#region src/prosemirror/runPropertyChangeResolution.ts
5
7
  /**
6
8
  * Rebuild the pre-change run marks against the paragraph's live style cascade.
@@ -8,12 +10,22 @@ import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "./runFor
8
10
  * owner so their reject semantics cannot diverge at the bulk threshold.
9
11
  */
10
12
  const reconstructRejectedRunFormattingMarks = ({ node, paragraphContext, previousFormatting, styleResolver }) => {
11
- return reconcileRunFormattingMarks({
13
+ const formatting = reconcileRunFormattingMarks({
12
14
  authoredFormatting: previousFormatting ?? {},
13
15
  context: paragraphContext,
14
16
  node,
15
17
  ...styleResolver !== void 0 ? { styleResolver } : {}
16
18
  }).filter(({ type }) => RUN_FORMATTING_MARK_NAMES.has(type.name));
19
+ if (previousFormatting === void 0 || Object.keys(previousFormatting).length > 0) return formatting;
20
+ const identityType = node.type.schema.marks[RUN_IDENTITY_MARK_NAME];
21
+ if (!identityType) return formatting;
22
+ const identity = node.marks.find(({ type }) => type === identityType);
23
+ const attrs = identity ? expectRunIdentityMarkAttrs(identity) : void 0;
24
+ return [...formatting, identityType.create(runIdentityAttrs(attrs?.id ?? 0, {
25
+ preservedAttributes: attrs?.preservedAttributes ?? void 0,
26
+ preserved: attrs?.preserved ?? void 0,
27
+ emptyFormatting: true
28
+ }))];
17
29
  };
18
30
  /** Restored deletions inherit the live paragraph cascade without authoring their historical visuals. */
19
31
  const restoreHistoricalRunFormatting = ({ node, paragraphContext, styleResolver }) => {
@@ -1,35 +1,5 @@
1
+ import { bytesToBase64, encodeBase64Portable } from "@stll/utf8-boundaries";
1
2
  //#region src/utils/base64.d.ts
2
- /**
3
- * The package's one bytes-to-base64 encoder.
4
- *
5
- * Every call site that spelled this itself got a different answer to the same
6
- * two questions, and one of them got the first question wrong:
7
- *
8
- * - `btoa(new TextDecoder("latin1").decode(bytes))` is not a binary string.
9
- * The Encoding standard maps the `latin1` label to windows-1252, so bytes
10
- * 0x80-0x9F decode to characters outside Latin-1 and `btoa` throws
11
- * `InvalidCharacterError` on them. Ordinary image bytes contain those values,
12
- * so the browser path of that spelling fails on real input.
13
- * - `String.fromCharCode(...chunk)` is correct but spreads a chunk's worth of
14
- * arguments per call, and the intermediate binary string is as large as the
15
- * input.
16
- *
17
- * Neither question has to be asked per call site, so it is asked here once.
18
- * The runtime's own encoder answers it when the runtime has one, and the
19
- * portable encoder below answers it when not; both produce canonical base64,
20
- * so the choice is invisible in the output.
21
- */
22
- /**
23
- * Encode into exactly one intermediate buffer: ASCII codes are written into a
24
- * single array and decoded once, so a megabyte-scale input leaves the output
25
- * string and nothing else.
26
- *
27
- * Exported because the runtimes the tests run on all carry a native encoder,
28
- * and the fallback is the branch the browsers that lack one take.
29
- */
30
- declare const encodeBase64Portable: (bytes: Uint8Array) => string;
31
- /** Encode bytes as canonical, padded, standard-alphabet base64. */
32
- declare const bytesToBase64: (bytes: Uint8Array) => string;
33
3
  /** Encode bytes as a `data:` URL with the given media type. */
34
4
  declare const bytesToDataUrl: (bytes: Uint8Array, mimeType: string) => string;
35
5
  //#endregion