@stll/folio-core 0.55.0 → 0.56.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 (119) hide show
  1. package/dist/ai-edits/apply.js +169 -38
  2. package/dist/ai-edits/headless.js +2 -2
  3. package/dist/ai-edits/types.d.ts +7 -1
  4. package/dist/compare/compare.js +1 -0
  5. package/dist/compare/reproducible-package.d.ts +0 -17
  6. package/dist/compare/reproducible-package.js +12 -18
  7. package/dist/controller/canonicalClipboard.d.ts +23 -0
  8. package/dist/controller/canonicalClipboard.js +476 -0
  9. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  10. package/dist/controller/canonicalClipboardResources.js +382 -0
  11. package/dist/controller/canonicalComposition.d.ts +20 -3
  12. package/dist/controller/canonicalComposition.js +166 -38
  13. package/dist/controller/canonicalInlineProjection.d.ts +113 -0
  14. package/dist/controller/canonicalInlineProjection.js +393 -0
  15. package/dist/controller/canonicalInput.d.ts +13 -4
  16. package/dist/controller/canonicalInput.js +52 -13
  17. package/dist/controller/canonicalInputTimer.d.ts +7 -0
  18. package/dist/controller/canonicalInputTimer.js +11 -0
  19. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  20. package/dist/controller/canonicalPublicOperations.js +556 -0
  21. package/dist/controller/canonicalSession.d.ts +24 -6
  22. package/dist/controller/canonicalSession.js +259 -111
  23. package/dist/controller/canonicalStoryEditor.d.ts +5 -3
  24. package/dist/controller/canonicalStoryEditor.js +7 -2
  25. package/dist/controller/canonicalStructure.js +57 -4
  26. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  27. package/dist/controller/canonicalTocSelection.js +43 -0
  28. package/dist/controller/folioEditor.d.ts +2 -9
  29. package/dist/controller/folioEditor.js +5 -4
  30. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  31. package/dist/controller/headerFooterEditorManager.js +8 -7
  32. package/dist/controller/hiddenEditorApi.d.ts +12 -2
  33. package/dist/controller/hiddenEditorApi.js +4 -0
  34. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  35. package/dist/controller/hiddenEditorManager.js +216 -34
  36. package/dist/controller/noteEditorManager.d.ts +2 -1
  37. package/dist/controller/noteEditorManager.js +8 -7
  38. package/dist/document-operations.d.ts +8 -1
  39. package/dist/document-operations.js +6 -4
  40. package/dist/docx/blockPlainText.d.ts +1 -1
  41. package/dist/docx/blockPlainText.js +11 -3
  42. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  43. package/dist/docx/canonicalResourceSave.js +27 -0
  44. package/dist/docx/canonicalSave.d.ts +29 -0
  45. package/dist/docx/canonicalSave.js +101 -0
  46. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  47. package/dist/docx/canonicalStoryRepack.js +38 -5
  48. package/dist/docx/documentParser.d.ts +1 -13
  49. package/dist/docx/documentParser.js +1 -47
  50. package/dist/docx/ensureParaIds.js +10 -4
  51. package/dist/docx/footnoteParser.d.ts +1 -11
  52. package/dist/docx/footnoteParser.js +1 -16
  53. package/dist/docx/headerFooterParser.d.ts +1 -9
  54. package/dist/docx/headerFooterParser.js +1 -12
  55. package/dist/docx/headerFooterVerbatim.d.ts +26 -1
  56. package/dist/docx/headerFooterVerbatim.js +81 -3
  57. package/dist/docx/imageParser.d.ts +13 -1
  58. package/dist/docx/imageParser.js +20 -11
  59. package/dist/docx/metadataPrivacy.js +6 -1
  60. package/dist/docx/packageParts.js +9 -3
  61. package/dist/docx/paragraphPropertySource.js +2 -1
  62. package/dist/docx/parser.js +2 -1
  63. package/dist/docx/removeHeaderFooterParts.js +17 -6
  64. package/dist/docx/rezip.d.ts +49 -5
  65. package/dist/docx/rezip.js +745 -229
  66. package/dist/docx/saveDiagnostics.d.ts +11 -0
  67. package/dist/docx/selectiveSave.d.ts +2 -1
  68. package/dist/docx/selectiveSave.js +32 -3
  69. package/dist/docx/selectiveXmlPatch.js +7 -5
  70. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  71. package/dist/docx/serializer/documentSerializer.js +29 -10
  72. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  73. package/dist/docx/storyBlockReplay.d.ts +21 -1
  74. package/dist/docx/storyBlockReplay.js +113 -9
  75. package/dist/docx/storyPlainText.d.ts +20 -0
  76. package/dist/docx/storyPlainText.js +23 -0
  77. package/dist/docx/structuralXmlPatch.js +10 -22
  78. package/dist/docx/styleParser.d.ts +5 -1
  79. package/dist/docx/styleParser.js +1 -1
  80. package/dist/docx/tableParser.d.ts +1 -8
  81. package/dist/docx/tableParser.js +1 -28
  82. package/dist/docx/textBoxParser.d.ts +1 -5
  83. package/dist/docx/textBoxParser.js +1 -20
  84. package/dist/docx/unzip.js +6 -1
  85. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  86. package/dist/internal/acceptedBlockProjection.js +45 -0
  87. package/dist/managers/DocumentLoaderManager.js +2 -1
  88. package/dist/markdown/escape.d.ts +3 -1
  89. package/dist/markdown/escape.js +9 -2
  90. package/dist/markdown/renderBlock.js +5 -120
  91. package/dist/markdown/renderParagraph.js +4 -2
  92. package/dist/markdown/renderRuns.d.ts +7 -1
  93. package/dist/markdown/renderRuns.js +32 -13
  94. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  95. package/dist/prosemirror/canonicalCommands.js +11 -4
  96. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  97. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  98. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  99. package/dist/prosemirror/clipboardIntent.js +9 -0
  100. package/dist/prosemirror/commands/pageBreak.js +2 -2
  101. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  102. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  103. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  104. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  105. package/dist/prosemirror/conversion/toProseDoc.js +5 -4
  106. package/dist/prosemirror/extensions/core/ParagraphExtension.js +39 -30
  107. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
  108. package/dist/prosemirror/extensions/marks/markUtils.js +3 -1
  109. package/dist/prosemirror/insertOperations.js +2 -1
  110. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  111. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  112. package/dist/prosemirror/plugins/suggestionMode.js +7 -5
  113. package/dist/types/canonicalCapabilities.d.ts +215 -0
  114. package/dist/types/canonicalCapabilities.js +212 -0
  115. package/dist/types/canonicalSave.d.ts +11 -0
  116. package/dist/types/canonicalSave.js +0 -0
  117. package/dist/types/docxSerialization.d.ts +14 -0
  118. package/dist/types/docxSerialization.js +7 -0
  119. package/package.json +4 -4
@@ -1,20 +1,26 @@
1
1
  import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
2
  import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
3
- import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
3
+ import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
4
+ import { collectPairedBookmarkIds, footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
4
5
  import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
5
6
  import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
7
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
8
+ import { canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline } from "./canonicalInlineProjection.js";
9
+ import { mapTocSelection } from "./canonicalTocSelection.js";
6
10
  import { Result, TaggedError, panic } from "better-result";
7
11
  import { hasIllegalXmlCharacters } from "@stll/docx-core";
8
12
  import { Fragment } from "prosemirror-model";
9
13
  import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
10
- import { TextSelection } from "prosemirror-state";
14
+ import { AllSelection, TextSelection } from "prosemirror-state";
11
15
  //#region src/controller/canonicalSession.ts
12
16
  var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
13
17
  const refuse = (message) => Result.err(new CanonicalSessionError({
18
+ gap: CANONICAL_GAP.dispatch,
14
19
  message,
15
20
  reason: "refused"
16
21
  }));
17
22
  const noChange = (message) => Result.err(new CanonicalSessionError({
23
+ gap: CANONICAL_GAP.dispatch,
18
24
  message,
19
25
  reason: "noChange"
20
26
  }));
@@ -26,6 +32,7 @@ const operationChangesPackage = (op) => {
26
32
  case DOCUMENT_OP_TYPES.REMOVE_NOTE:
27
33
  case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
28
34
  case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
35
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
29
36
  case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
30
37
  case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
31
38
  case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
@@ -36,6 +43,51 @@ const operationChangesPackage = (op) => {
36
43
  return op;
37
44
  }
38
45
  };
46
+ const operationChangesStructure = (op) => {
47
+ switch (op.type) {
48
+ case DOCUMENT_OP_TYPES.DELETE_BLOCKS:
49
+ case DOCUMENT_OP_TYPES.INSERT_BLOCKS:
50
+ case DOCUMENT_OP_TYPES.SPLIT_BLOCK:
51
+ case DOCUMENT_OP_TYPES.JOIN_BLOCKS:
52
+ case DOCUMENT_OP_TYPES.REPLACE_BLOCKS:
53
+ case DOCUMENT_OP_TYPES.INSERT_TABLE:
54
+ case DOCUMENT_OP_TYPES.DELETE_TABLE:
55
+ case DOCUMENT_OP_TYPES.SET_CONTAINER_BLOCKS:
56
+ case DOCUMENT_OP_TYPES.INSERT_ROW:
57
+ case DOCUMENT_OP_TYPES.DELETE_ROW:
58
+ case DOCUMENT_OP_TYPES.SET_TABLE_ROWS:
59
+ case DOCUMENT_OP_TYPES.INSERT_COLUMN:
60
+ case DOCUMENT_OP_TYPES.DELETE_COLUMN:
61
+ case DOCUMENT_OP_TYPES.MERGE_CELLS:
62
+ case DOCUMENT_OP_TYPES.SPLIT_CELL:
63
+ case DOCUMENT_OP_TYPES.SET_TABLE:
64
+ case DOCUMENT_OP_TYPES.RESOLVE_REVISION: return true;
65
+ case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
66
+ case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
67
+ case DOCUMENT_OP_TYPES.ADD_NOTE:
68
+ case DOCUMENT_OP_TYPES.REMOVE_NOTE:
69
+ case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
70
+ case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
71
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
72
+ case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
73
+ case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
74
+ case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT:
75
+ case DOCUMENT_OP_TYPES.INSERT_TEXT:
76
+ case DOCUMENT_OP_TYPES.INSERT_CONTENT:
77
+ case DOCUMENT_OP_TYPES.DELETE_RANGE:
78
+ case DOCUMENT_OP_TYPES.SPLIT_INLINE:
79
+ case DOCUMENT_OP_TYPES.JOIN_INLINE:
80
+ case DOCUMENT_OP_TYPES.SET_RUN_PROPS:
81
+ case DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS:
82
+ case DOCUMENT_OP_TYPES.SET_PARAGRAPH_REVIEW:
83
+ case DOCUMENT_OP_TYPES.REPLACE_INLINE:
84
+ case DOCUMENT_OP_TYPES.SET_TABLE_GRID:
85
+ case DOCUMENT_OP_TYPES.SET_CELL_PROPS:
86
+ case DOCUMENT_OP_TYPES.SET_ROW_PROPS:
87
+ case DOCUMENT_OP_TYPES.SET_TABLE_PROPS: return false;
88
+ default: return op;
89
+ }
90
+ };
39
91
  const operationChangesBodyProjection = (op) => {
40
92
  switch (op.type) {
41
93
  case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
@@ -44,6 +96,7 @@ const operationChangesBodyProjection = (op) => {
44
96
  case DOCUMENT_OP_TYPES.REMOVE_NOTE:
45
97
  case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
46
98
  case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
99
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
47
100
  case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
48
101
  case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
49
102
  case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
@@ -55,7 +108,7 @@ const operationChangesBodyProjection = (op) => {
55
108
  }
56
109
  };
57
110
  const CANONICAL_PROJECTION_META = "folioCanonicalProjection";
58
- /** Paragraph gaps map logical UTF-16 units and zero-width note marks to PM positions. */
111
+ /** Paragraph gaps bind logical units and source zero-width ordinals to PM positions. */
59
112
  var CanonicalProjection = class {
60
113
  doc;
61
114
  story;
@@ -72,15 +125,16 @@ var CanonicalProjection = class {
72
125
  const paragraph = this.paragraphs.find(({ start, node }) => position >= start && position <= start + node.content.size);
73
126
  if (paragraph === void 0) return refuse("The input is outside a plain paragraph.");
74
127
  const relative = position - paragraph.start;
75
- const offset = paragraph.boundaries.findIndex((gaps) => gaps.includes(relative));
76
- if (offset < 0) return refuse("The input splits a note reference.");
128
+ const offset = paragraph.boundaries.findIndex((gaps) => gaps.some((gap) => gap.position === relative));
129
+ if (offset < 0) return refuse("The input splits an indivisible inline element.");
77
130
  if (splitsSurrogatePair(paragraph.text, offset)) return refuse("The input would split a surrogate pair.");
78
131
  const gaps = paragraph.boundaries.at(offset) ?? panic("Missing canonical boundary");
132
+ const gap = gaps.find((entry) => entry.position === relative) ?? panic("Missing canonical native gap");
79
133
  return Result.ok({
80
134
  story: this.story,
81
135
  blockId: paragraph.blockId,
82
136
  offset,
83
- ...gaps.length > 1 ? { zeroWidthBefore: gaps.indexOf(relative) } : {}
137
+ ...gaps.length > 1 || gap.zeroWidthBefore > 0 ? { zeroWidthBefore: gap.zeroWidthBefore } : {}
84
138
  });
85
139
  }
86
140
  inputAddressAt(position) {
@@ -88,30 +142,32 @@ var CanonicalProjection = class {
88
142
  if (address.isErr()) return address;
89
143
  const paragraph = this.paragraph(address.value.blockId);
90
144
  if (paragraph === void 0) panic("Input lost its paragraph.");
91
- let offset = address.value.offset;
145
+ let destination = position;
92
146
  paragraph.node.forEach((node, start) => {
93
- if (node.marks.some((mark) => mark.type.name === "deletion") && offset > start && offset < start + node.nodeSize) offset = start + node.nodeSize;
94
- });
95
- return Result.ok({
96
- ...address.value,
97
- offset
147
+ if (node.marks.some((mark) => mark.type.name === "deletion") && position > paragraph.start + start && position < paragraph.start + start + node.nodeSize) destination = paragraph.start + start + node.nodeSize;
98
148
  });
149
+ return destination === position ? address : this.addressAt(destination);
99
150
  }
100
151
  positionAt(address) {
101
152
  const paragraph = this.paragraph(address.blockId);
102
153
  if (!sameStory(address.story, this.story) || paragraph === void 0 || !Number.isInteger(address.offset) || address.offset < 0 || address.offset > paragraph.text.length || splitsSurrogatePair(paragraph.text, address.offset)) return refuse("The canonical selection is outside a plain paragraph.");
103
154
  const gaps = paragraph.boundaries.at(address.offset) ?? panic("Missing canonical boundary");
104
- const gapIndex = address.zeroWidthBefore ?? gaps.length - 1;
105
- if (!Number.isInteger(gapIndex) || gapIndex < 0 || gapIndex >= gaps.length) return refuse("The canonical selection is outside a paragraph gap.");
106
- return Result.ok(paragraph.start + (gaps.at(gapIndex) ?? panic("Missing canonical gap")));
155
+ const ordinal = address.zeroWidthBefore ?? (gaps.at(-1) ?? panic("Missing canonical gap")).zeroWidthBefore;
156
+ const gap = gaps.find(({ zeroWidthBefore }) => zeroWidthBefore === ordinal);
157
+ if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal > (gaps.at(-1)?.zeroWidthBefore ?? 0)) return refuse("The canonical selection is outside a paragraph gap.");
158
+ if (gap === void 0) return refuse("This source gap is inside a collapsed or erased inline boundary and has no editor position.");
159
+ return Result.ok(paragraph.start + gap.position);
107
160
  }
108
161
  selectionAt(state) {
109
- if (!(state.selection instanceof TextSelection)) return refuse("Canonical input requires a text selection.");
110
- const anchor = this.addressAt(state.selection.anchor);
162
+ if (!(state.selection instanceof TextSelection) && !(state.selection instanceof AllSelection)) return refuse("Canonical input requires a text selection.");
163
+ const range = canonicalSelectionRange(state);
164
+ const type = state.selection instanceof AllSelection ? "all" : "text";
165
+ const anchor = this.addressAt(type === "all" ? range.from : state.selection.anchor);
111
166
  if (anchor.isErr()) return anchor;
112
- const head = this.addressAt(state.selection.head);
167
+ const head = this.addressAt(type === "all" ? range.to : state.selection.head);
113
168
  if (head.isErr()) return head;
114
169
  return Result.ok({
170
+ type,
115
171
  anchor: anchor.value,
116
172
  head: head.value
117
173
  });
@@ -162,31 +218,29 @@ const preservePropertySources = ({ target, source, stories = documentStories(sou
162
218
  }
163
219
  copyDocumentParagraphPropertySourceContract(target, source);
164
220
  };
165
- const supportsTextContent = (content) => content.every((item) => {
166
- switch (item.type) {
167
- case "run": return item.content.every((child) => child.type === "text" && !hasIllegalXmlCharacters(child.text) && !/[\t\r\n]/u.test(child.text) || child.type === "noteMarker" || child.type === "footnoteRef" || child.type === "endnoteRef" || child.type === "tab" || child.type === "break");
168
- case "insertion":
169
- case "deletion": return supportsTextContent(item.content);
170
- default: return false;
171
- }
172
- });
173
- const supportsSeed = (document, stories = documentStories(document)) => {
174
- if ((document.package.document.comments?.length ?? 0) > 0) return false;
175
- return stories.every((story) => {
221
+ const unsupportedSeedReason = (document, stories = documentStories(document)) => {
222
+ for (const story of stories) {
176
223
  const body = findStoryBody(document, story);
177
224
  if (story !== OP_STORIES.MAIN && (story.kind === "footnote" || story.kind === "endnote")) {
178
225
  const note = (story.kind === "footnote" ? document.package.footnotes : document.package.endnotes)?.find(({ id }) => id === story.id);
179
- if (note?.noteType !== void 0 && note.noteType !== "normal") return true;
226
+ if (note?.noteType !== void 0 && note.noteType !== "normal") continue;
180
227
  }
181
228
  const content = body?.content;
182
- return content !== void 0 && content.length > 0 && content.every((paragraph) => paragraph.type === "paragraph" && paragraph.reviewCarrier === void 0 && supportsTextContent(paragraph.content));
183
- });
229
+ if (!content || content.length === 0) return "Canonical sessions require a nonempty paragraph story.";
230
+ for (const paragraph of content) {
231
+ if (paragraph.type !== "paragraph" || paragraph.reviewCarrier !== void 0) return "Canonical sessions currently require plain paragraphs and supported inline atoms.";
232
+ const issue = canonicalInlineSourceIssue(paragraph.content);
233
+ if (issue !== null) return issue;
234
+ }
235
+ }
236
+ return null;
184
237
  };
185
238
  const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
186
239
  const body = findStoryBody(document, story);
187
240
  if (!body) return refuse("The canonical story no longer exists.");
241
+ const currentStyles = Object.hasOwn(document.package, "styles") ? document.package.styles : styles;
188
242
  const options = {
189
- ...styles == null ? {} : { styles },
243
+ ...currentStyles == null ? {} : { styles: currentStyles },
190
244
  ...document.package.theme === void 0 ? {} : { theme: document.package.theme }
191
245
  };
192
246
  const converted = Result.try({
@@ -196,67 +250,50 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
196
250
  return footnoteToProseDoc(body.content, options);
197
251
  },
198
252
  catch: (cause) => new CanonicalSessionError({
253
+ gap: CANONICAL_GAP.dispatch,
199
254
  reason: "refused",
200
255
  message: `Canonical projection failed: ${cause instanceof Error ? cause.message : String(cause)}`
201
256
  })
202
257
  });
203
258
  if (converted.isErr()) return converted;
259
+ const pairedBookmarkIds = collectPairedBookmarkIds(body.content);
204
260
  const paragraphs = [];
205
261
  let failure;
206
262
  converted.value.forEach((node, offset, index) => {
207
263
  const source = body.content.at(index);
208
264
  if (source?.type !== "paragraph" || source.paraId === void 0) {
209
265
  failure = new CanonicalSessionError({
266
+ gap: CANONICAL_GAP.dispatch,
210
267
  reason: "refused",
211
268
  message: "The projection changed paragraph structure."
212
269
  });
213
270
  return;
214
271
  }
215
- let text = "";
216
- let renderedText = "";
217
- let renderedSize = 0;
218
- const boundaries = [[0]];
219
- const appendContent = (content) => {
220
- for (const run of content) {
221
- if (run.type === "insertion" || run.type === "deletion") {
222
- appendContent(run.content);
223
- continue;
224
- }
225
- if (run.type !== "run") panic("Canonical projection encountered unsupported content");
226
- for (const child of run.content) if (child.type === "text") for (const unit of child.text.split("")) {
227
- text += unit;
228
- renderedText += unit;
229
- renderedSize += 1;
230
- boundaries.push([renderedSize]);
231
- }
232
- else if (child.type === "footnoteRef" || child.type === "endnoteRef") {
233
- text += "";
234
- renderedText += String(child.id);
235
- renderedSize += String(child.id).length;
236
- boundaries.push([renderedSize]);
237
- } else if (child.type === "tab" || child.type === "break") {
238
- text += "";
239
- renderedSize += 1;
240
- boundaries.push([renderedSize]);
241
- } else if (child.type === "noteMarker") {
242
- renderedSize += 1;
243
- (boundaries.at(-1) ?? panic("Missing canonical note gap")).push(renderedSize);
244
- }
245
- }
246
- };
247
- appendContent(source.content);
248
- if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId || node.textContent !== renderedText || node.content.size !== renderedSize) {
272
+ if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId) {
273
+ failure = new CanonicalSessionError({
274
+ gap: CANONICAL_GAP.dispatch,
275
+ reason: "refused",
276
+ message: "The projection changed paragraph structure."
277
+ });
278
+ return;
279
+ }
280
+ const inline = projectCanonicalInline({
281
+ source,
282
+ paragraph: node,
283
+ pairedBookmarkIds
284
+ });
285
+ if (inline.isErr()) {
249
286
  failure = new CanonicalSessionError({
287
+ gap: CANONICAL_GAP.dispatch,
250
288
  reason: "refused",
251
- message: "The paragraph cannot be projected as plain text."
289
+ message: inline.error.message
252
290
  });
253
291
  return;
254
292
  }
255
293
  paragraphs.push({
256
294
  blockId: source.paraId,
257
295
  start: offset + 1,
258
- text,
259
- boundaries,
296
+ ...inline.value,
260
297
  node,
261
298
  source
262
299
  });
@@ -271,9 +308,15 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
271
308
  };
272
309
  const intentStory = (intent) => {
273
310
  switch (intent.type) {
311
+ case "setHyperlink":
312
+ case "removeHyperlink":
313
+ case "insertHyperlink":
314
+ case "replaceFragment":
315
+ case "moveFragment":
274
316
  case "replaceText":
275
317
  case "formatRun":
276
318
  case "insertAtom": return intent.from.story;
319
+ case "generateTOC":
277
320
  case "splitParagraph":
278
321
  case "formatParagraph": return intent.at.story;
279
322
  case "setList": return intent.items.at(0)?.at.story ?? panic("A list intent requires a paragraph.");
@@ -284,14 +327,14 @@ const intentStory = (intent) => {
284
327
  };
285
328
  const UNDO_GROUP_WINDOW_MS = 500;
286
329
  const samePosition = (left, right) => sameStory(left.story, right.story) && left.blockId === right.blockId && left.offset === right.offset && (left.zeroWidthBefore ?? 0) === (right.zeroWidthBefore ?? 0);
287
- const sameSelection = (left, right) => samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
330
+ const sameSelection = (left, right) => left.type === right.type && samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
288
331
  const continuesGroup = (previous, next) => {
289
332
  switch (next.semantic) {
290
333
  case "typing":
291
334
  case "deleteBackward":
292
335
  case "deleteForward": return previous.grouping === "run" && next.grouping === "run" && previous.semantic === next.semantic && previous.boundary === next.boundary && next.time >= previous.time && next.time - previous.time <= UNDO_GROUP_WINDOW_MS && sameSelection(previous.postSelection, next.preSelection) && samePosition(next.preSelection.anchor, next.preSelection.head);
336
+ case "composition": return next.grouping === "compositionCorrection" && previous.semantic === "composition" && previous.boundary === next.boundary && sameSelection(previous.postSelection, next.preSelection);
293
337
  case "paste":
294
- case "composition":
295
338
  case "replacement":
296
339
  case "structure": return false;
297
340
  default: {
@@ -312,6 +355,7 @@ const publishCanonicalProjection = ({ state, commit, session }) => {
312
355
  const applied = Result.try({
313
356
  try: () => state.applyTransaction(commit.transaction),
314
357
  catch: (cause) => new CanonicalSessionError({
358
+ gap: CANONICAL_GAP.dispatch,
315
359
  reason: "refused",
316
360
  message: `Canonical plugin staging failed: ${cause instanceof Error ? cause.message : String(cause)}`
317
361
  })
@@ -336,7 +380,10 @@ const storyProjectionKey = (story) => {
336
380
  var CanonicalSession = class {
337
381
  currentDocument;
338
382
  currentProjection;
383
+ sourceBlockIds;
339
384
  currentVersion = 0;
385
+ saveTouched = /* @__PURE__ */ new Set();
386
+ saveStructure = "stable";
340
387
  allocateIntentIds = createEditorIntentIdAllocator();
341
388
  mode = { type: "editing" };
342
389
  allocatedBlockIds = /* @__PURE__ */ new Set();
@@ -352,6 +399,7 @@ var CanonicalSession = class {
352
399
  constructor({ document, projection, styles }) {
353
400
  this.currentDocument = document;
354
401
  this.currentProjection = projection;
402
+ this.sourceBlockIds = new Set(packageParagraphIds(document.package));
355
403
  for (const blockId of packageParagraphIds(document.package)) this.allocatedBlockIds.add(blockId.toUpperCase());
356
404
  this.advanceBlockId();
357
405
  this.styles = styles == null ? styles : structuredClone(styles);
@@ -375,13 +423,18 @@ var CanonicalSession = class {
375
423
  return Result.ok(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase());
376
424
  }
377
425
  intentMode(document, intent) {
378
- if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return { type: "editing" };
426
+ if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return {
427
+ type: "editing",
428
+ reservedBlockIds: this.allocatedBlockIds
429
+ };
379
430
  const ids = this.allocateIntentIds(document, intent);
380
431
  return this.mode.type === "editing" ? {
381
432
  type: "editing",
382
- newIds: ids.newIds
433
+ newIds: ids.newIds,
434
+ reservedBlockIds: this.allocatedBlockIds
383
435
  } : {
384
436
  type: "suggesting",
437
+ reservedBlockIds: this.allocatedBlockIds,
385
438
  revision: {
386
439
  id: ids.revisionId,
387
440
  author: this.mode.author,
@@ -394,8 +447,14 @@ var CanonicalSession = class {
394
447
  let blockIds;
395
448
  switch (intent.type) {
396
449
  case "table": return true;
450
+ case "generateTOC":
397
451
  case "setList":
398
452
  case "formatParagraph": return false;
453
+ case "setHyperlink":
454
+ case "removeHyperlink":
455
+ case "insertHyperlink":
456
+ case "replaceFragment":
457
+ case "moveFragment": return true;
399
458
  case "replaceText":
400
459
  case "insertAtom":
401
460
  case "formatRun":
@@ -428,11 +487,26 @@ var CanonicalSession = class {
428
487
  }
429
488
  get document() {
430
489
  if (this.isComposing) throw new CanonicalSessionError({
490
+ gap: CANONICAL_GAP.dispatch,
431
491
  reason: "refused",
432
492
  message: "Composition must finish before taking a snapshot."
433
493
  });
434
494
  return this.currentDocument;
435
495
  }
496
+ captureSaveSnapshot() {
497
+ if (this.isComposing) throw new CanonicalSessionError({
498
+ gap: CANONICAL_GAP.save,
499
+ reason: "refused",
500
+ message: "Composition must finish before saving the canonical document."
501
+ });
502
+ const retainedBlockIdKeys = new Set([...this.sourceBlockIds, ...packageParagraphIds(this.currentDocument.package)].map((blockId) => blockId.toUpperCase()));
503
+ return {
504
+ document: cloneDocumentWithParagraphPropertySources(this.currentDocument),
505
+ version: this.currentVersion,
506
+ changedBlockIds: [...this.saveTouched].filter((blockId) => retainedBlockIdKeys.has(blockId.toUpperCase())),
507
+ structure: this.saveStructure
508
+ };
509
+ }
436
510
  get projection() {
437
511
  return this.currentProjection;
438
512
  }
@@ -469,7 +543,7 @@ var CanonicalSession = class {
469
543
  if (!state.doc.eq(projection.doc)) return refuse("The input uses a stale canonical projection.");
470
544
  return Result.ok(void 0);
471
545
  }
472
- prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", time = Date.now() }) {
546
+ prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", compositionPhase, time = Date.now() }) {
473
547
  const projected = this.projectStory(story);
474
548
  if (projected.isErr()) return projected;
475
549
  const projection = projected.value;
@@ -498,11 +572,14 @@ var CanonicalSession = class {
498
572
  if (compiled.isErr()) return refuse(compiled.error.message);
499
573
  const { ops, selection: caret } = compiled.value;
500
574
  const postSelection = {
575
+ type: "text",
501
576
  anchor: caret,
502
577
  head: caret
503
578
  };
504
579
  const preDocument = this.currentDocument;
505
- const run = state.selection.empty && (semantic === "typing" && from === to && from === state.selection.head || semantic === "deleteBackward" && text.length === 0 && to === state.selection.head || semantic === "deleteForward" && text.length === 0 && from === state.selection.head);
580
+ const isCaret = state.selection.empty;
581
+ const deletion = isCaret && text.length === 0 && (semantic === "deleteBackward" || semantic === "deleteForward") ? deletionRange(state, semantic === "deleteBackward" ? "backward" : "forward") : null;
582
+ const grouping = isCaret && (semantic === "typing" && from === to && from === state.selection.head || deletion?.isOk() && deletion.value.from === from && deletion.value.to === to) ? "run" : "isolated";
506
583
  return this.stage({
507
584
  state,
508
585
  story,
@@ -521,7 +598,7 @@ var CanonicalSession = class {
521
598
  version,
522
599
  origin: "input",
523
600
  semantic,
524
- grouping: run ? "run" : "isolated",
601
+ grouping: compositionPhase === "correction" ? "compositionCorrection" : grouping,
525
602
  time,
526
603
  boundary: this.groupingBoundary
527
604
  };
@@ -541,9 +618,9 @@ var CanonicalSession = class {
541
618
  });
542
619
  }
543
620
  prepareIntent(state, intent) {
544
- return this.prepareIntents(state, [intent]);
621
+ return this.prepareIntents(state, { intents: [intent] });
545
622
  }
546
- prepareIntents(state, intents) {
623
+ prepareIntents(state, { intents, resourceOps = [], semantic = "structure" }) {
547
624
  const first = intents.at(0);
548
625
  if (first === void 0) return noChange("The intent batch is empty.");
549
626
  const story = intentStory(first);
@@ -556,12 +633,24 @@ var CanonicalSession = class {
556
633
  if (preSelection.isErr()) return preSelection;
557
634
  let document = this.currentDocument;
558
635
  let postSelection = preSelection.value;
559
- const ops = [];
636
+ const ops = [...resourceOps];
560
637
  const edits = [];
638
+ if (resourceOps.length > 0) {
639
+ const imported = applyDocumentOps(document, resourceOps);
640
+ if (imported.isErr()) return refuse(imported.error.message);
641
+ document = imported.value.document;
642
+ edits.push(imported.value);
643
+ }
561
644
  for (const intent of intents) {
645
+ if (this.mode.type === "suggesting" && (intent.type === "setHyperlink" || intent.type === "removeHyperlink" || intent.type === "insertHyperlink" || intent.type === "generateTOC")) return Result.err(new CanonicalSessionError({
646
+ gap: CANONICAL_GAP.trackedHyperlinkResolution,
647
+ reason: "refused",
648
+ message: "Hyperlink and TOC suggestions require serializable wrapper review provenance."
649
+ }));
562
650
  const compiled = compileEditorIntent(document, {
563
651
  intent,
564
- mode: this.intentMode(document, intent)
652
+ mode: this.intentMode(document, intent),
653
+ firstBlockId: this.nextBlockId
565
654
  });
566
655
  if (compiled.isErr()) return refuse(compiled.error.message);
567
656
  const applied = applyDocumentOps(document, compiled.value.ops);
@@ -569,7 +658,18 @@ var CanonicalSession = class {
569
658
  document = applied.value.document;
570
659
  edits.push(applied.value);
571
660
  ops.push(...compiled.value.ops);
572
- if (intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
661
+ if (intent.type === "generateTOC") {
662
+ const mapping = {
663
+ at: intent.at,
664
+ after: compiled.value.selection,
665
+ ops: compiled.value.ops
666
+ };
667
+ postSelection = Object.assign({}, postSelection, {
668
+ anchor: mapTocSelection(postSelection.anchor, mapping),
669
+ head: mapTocSelection(postSelection.head, mapping)
670
+ });
671
+ } else if (intent.type !== "setHyperlink" && intent.type !== "removeHyperlink" && intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
672
+ type: "text",
573
673
  anchor: compiled.value.selection,
574
674
  head: compiled.value.selection
575
675
  };
@@ -582,7 +682,8 @@ var CanonicalSession = class {
582
682
  state,
583
683
  ops,
584
684
  postSelection,
585
- stagedApplied
685
+ stagedApplied,
686
+ semantic
586
687
  });
587
688
  }
588
689
  prepareOps(state, ops, postSelection) {
@@ -592,7 +693,7 @@ var CanonicalSession = class {
592
693
  postSelection
593
694
  });
594
695
  }
595
- prepareJournalledOps({ state, ops, postSelection, stagedApplied }) {
696
+ prepareJournalledOps({ state, ops, postSelection, stagedApplied, semantic = "structure" }) {
596
697
  const story = postSelection.anchor.story;
597
698
  const projected = this.projectStory(story);
598
699
  if (projected.isErr()) return projected;
@@ -618,7 +719,7 @@ var CanonicalSession = class {
618
719
  postDocument: this.currentDocument,
619
720
  version,
620
721
  origin: "input",
621
- semantic: "structure",
722
+ semantic,
622
723
  grouping: "isolated",
623
724
  time: Date.now(),
624
725
  boundary: this.groupingBoundary
@@ -656,7 +757,7 @@ var CanonicalSession = class {
656
757
  },
657
758
  patch: { styleId: nextStyle }
658
759
  });
659
- return this.prepareIntents(state, intents);
760
+ return this.prepareIntents(state, { intents });
660
761
  }
661
762
  prepareJoin(state, direction) {
662
763
  const checked = this.checkState(state);
@@ -712,6 +813,7 @@ var CanonicalSession = class {
712
813
  state,
713
814
  ops,
714
815
  postSelection: {
816
+ type: "text",
715
817
  anchor: caret,
716
818
  head: caret
717
819
  },
@@ -836,7 +938,8 @@ var CanonicalSession = class {
836
938
  document: applied.value.document,
837
939
  previous: this.currentDocument
838
940
  });
839
- if (!supportsSeed(applied.value.document, changed)) return refuse("The operations produce unsupported canonical story content.");
941
+ const unsupported = unsupportedSeedReason(applied.value.document, changed);
942
+ if (unsupported !== null) return refuse(unsupported);
840
943
  preservePropertySources({
841
944
  target: applied.value.document,
842
945
  source: this.currentDocument,
@@ -894,8 +997,22 @@ var CanonicalSession = class {
894
997
  if (sameStory(selection.anchor.story, story) && sameStory(selection.head.story, story)) {
895
998
  const anchor = projected.value.positionAt(selection.anchor);
896
999
  const head = projected.value.positionAt(selection.head);
897
- if (anchor.isOk() && head.isOk()) transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
898
- else transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
1000
+ switch (selection.type) {
1001
+ case "all":
1002
+ transaction.setSelection(new AllSelection(transaction.doc));
1003
+ break;
1004
+ case "text":
1005
+ if (anchor.isOk() && head.isOk()) {
1006
+ transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
1007
+ break;
1008
+ }
1009
+ transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
1010
+ break;
1011
+ default: {
1012
+ const exhaustive = selection.type;
1013
+ return panic(`Unknown canonical selection type: ${exhaustive}`);
1014
+ }
1015
+ }
899
1016
  }
900
1017
  transaction.setMeta(CANONICAL_PROJECTION_META, {
901
1018
  type: "canonical",
@@ -939,6 +1056,12 @@ var CanonicalSession = class {
939
1056
  this.currentSelection = selection;
940
1057
  this.currentDocument = applied.value.document;
941
1058
  this.currentProjection = bodyProjection.value;
1059
+ for (const ids of [
1060
+ applied.value.touched.modified,
1061
+ applied.value.touched.inserted,
1062
+ applied.value.touched.removed
1063
+ ]) for (const id of ids) this.saveTouched.add(id);
1064
+ if (applied.value.touched.inserted.length > 0 || applied.value.touched.removed.length > 0 || ops.some(operationChangesPackage) || ops.some(operationChangesStructure)) this.saveStructure = "changed";
942
1065
  this.currentVersion = baseVersion + 1;
943
1066
  onPublish(applied.value.inverse, this.currentVersion);
944
1067
  return Result.ok(void 0);
@@ -947,7 +1070,8 @@ var CanonicalSession = class {
947
1070
  }
948
1071
  };
949
1072
  const createCanonicalSession = (document, styles) => {
950
- if (!supportsSeed(document)) return refuse("Canonical sessions currently require plain paragraphs and supported inline atoms.");
1073
+ const unsupported = unsupportedSeedReason(document);
1074
+ if (unsupported !== null) return refuse(unsupported);
951
1075
  const owned = cloneDocumentWithParagraphPropertySources(document);
952
1076
  const normalized = normalizeForOps(owned);
953
1077
  preservePropertySources({
@@ -980,31 +1104,55 @@ const visibleDeletionContext = (state) => {
980
1104
  let last = index;
981
1105
  while (last + 1 < state.doc.childCount && hasDeletedParagraphMark(state.doc.child(last))) last += 1;
982
1106
  let text = "";
983
- const physicalGaps = [];
1107
+ const physicalStarts = [];
1108
+ const physicalEnds = [];
984
1109
  let visibleOffset = 0;
1110
+ let issue;
985
1111
  state.doc.forEach((paragraph, paragraphOffset, paragraphIndex) => {
986
1112
  if (paragraphIndex < first || paragraphIndex > last) return;
987
- paragraph.forEach((node, offset) => {
988
- if (node.marks.some((mark) => mark.type.name === "deletion")) return;
989
- const value = node.isText ? node.text ?? "" : "";
1113
+ const native = canonicalNativeCells(paragraph);
1114
+ if (native.isErr()) {
1115
+ issue = new CanonicalSessionError({
1116
+ gap: CANONICAL_GAP.dispatch,
1117
+ reason: "refused",
1118
+ message: native.error.message
1119
+ });
1120
+ return;
1121
+ }
1122
+ for (const cell of native.value) {
1123
+ if (cell.type === "zeroWidth" || cell.node.marks.some((mark) => mark.type.name === "deletion")) continue;
1124
+ const value = cell.type === "text" ? cell.text : "";
1125
+ const from = paragraphOffset + 1 + cell.from;
1126
+ const to = paragraphOffset + 1 + cell.to;
1127
+ if (cell.type !== "text" && selection.from > from && selection.from < to) {
1128
+ issue = new CanonicalSessionError({
1129
+ gap: CANONICAL_GAP.dispatch,
1130
+ reason: "refused",
1131
+ message: "Character deletion cannot start inside an inline atom."
1132
+ });
1133
+ return;
1134
+ }
990
1135
  for (let unit = 0; unit < value.length; unit += 1) {
991
- const position = paragraphOffset + 1 + offset + unit;
992
- physicalGaps[text.length] = position;
1136
+ const position = from + (cell.type === "text" ? unit : 0);
1137
+ physicalStarts.push(position);
993
1138
  text += value.charAt(unit);
994
- physicalGaps.push(position + (node.isText ? 1 : node.nodeSize));
1139
+ physicalEnds.push(cell.type === "text" ? position + 1 : to);
995
1140
  if (position < selection.from) visibleOffset += 1;
996
1141
  }
997
- });
1142
+ }
998
1143
  });
999
- return {
1144
+ return issue === void 0 ? Result.ok({
1000
1145
  text,
1001
- physicalGaps,
1146
+ physicalStarts,
1147
+ physicalEnds,
1002
1148
  visibleOffset
1003
- };
1149
+ }) : Result.err(issue);
1004
1150
  };
1005
1151
  const isCanonicalJoinBoundary = (state, direction) => {
1006
1152
  if (!(state.selection instanceof TextSelection) || !state.selection.empty) return false;
1007
- const { text, visibleOffset } = visibleDeletionContext(state);
1153
+ const context = visibleDeletionContext(state);
1154
+ if (context.isErr()) return false;
1155
+ const { text, visibleOffset } = context.value;
1008
1156
  return visibleOffset === (direction === "backward" ? 0 : text.length);
1009
1157
  };
1010
1158
  /** Native character deletion consumes one whole visible grapheme. */
@@ -1018,20 +1166,20 @@ const deletionRange = (state, direction) => {
1018
1166
  const parent = selection.$from.parent;
1019
1167
  const offset = selection.$from.parentOffset;
1020
1168
  if ((direction === "backward" ? parent.childBefore(offset).node : parent.childAfter(offset).node)?.type.name === "noteMarker") return refuse("Automatic note marks cannot be deleted as characters.");
1021
- const { text, physicalGaps, visibleOffset } = visibleDeletionContext(state);
1169
+ const context = visibleDeletionContext(state);
1170
+ if (context.isErr()) return context;
1171
+ const { text, physicalStarts, physicalEnds, visibleOffset } = context.value;
1022
1172
  if (splitsSurrogatePair(text, visibleOffset)) return refuse("The deletion would split a surrogate pair.");
1023
1173
  if (splitsGraphemeCluster(text, visibleOffset)) return refuse("Character deletion requires a caret at a grapheme boundary.");
1024
1174
  if (direction === "backward" && visibleOffset === 0 || direction === "forward" && visibleOffset === text.length) return refuse("There is no visible character to delete in this paragraph.");
1025
1175
  let boundary = visibleOffset + (direction === "backward" ? -1 : 1);
1026
1176
  while (splitsGraphemeCluster(text, boundary)) boundary += direction === "backward" ? -1 : 1;
1027
- const position = physicalGaps.at(boundary);
1028
- if (position === void 0) panic("Canonical deletion lost a grapheme boundary.");
1029
- return Result.ok(direction === "backward" ? {
1030
- from: position,
1031
- to: selection.to
1032
- } : {
1033
- from: selection.from,
1034
- to: position
1177
+ const from = physicalStarts.at(direction === "backward" ? boundary : visibleOffset);
1178
+ const to = physicalEnds.at((direction === "backward" ? visibleOffset : boundary) - 1);
1179
+ if (from === void 0 || to === void 0) panic("Canonical deletion lost a grapheme boundary.");
1180
+ return Result.ok({
1181
+ from,
1182
+ to
1035
1183
  });
1036
1184
  };
1037
1185
  //#endregion