@stll/folio-core 0.56.0 → 0.58.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.
- package/dist/ai-edits/apply.js +122 -22
- package/dist/ai-edits/batch-claims.d.ts +3 -1
- package/dist/ai-edits/batch-claims.js +25 -1
- package/dist/ai-edits/table-row-column-mutations.js +17 -3
- package/dist/ai-suggestions/apply.js +20 -1
- package/dist/compare/inline-atoms.js +40 -16
- package/dist/compare/inline-provenance.js +12 -5
- package/dist/compat/eigenpal.d.ts +2 -1
- package/dist/compat/eigenpal.js +2 -1
- package/dist/controller/canonicalClipboard.d.ts +3 -1
- package/dist/controller/canonicalClipboard.js +31 -13
- package/dist/controller/canonicalComments.d.ts +41 -0
- package/dist/controller/canonicalComments.js +152 -0
- package/dist/controller/canonicalComposition.d.ts +17 -4
- package/dist/controller/canonicalComposition.js +125 -20
- package/dist/controller/canonicalInlineProjection.d.ts +6 -1
- package/dist/controller/canonicalInlineProjection.js +86 -3
- package/dist/controller/canonicalInput.d.ts +1 -1
- package/dist/controller/canonicalInput.js +14 -4
- package/dist/controller/canonicalInputTimer.d.ts +10 -1
- package/dist/controller/canonicalInputTimer.js +24 -1
- package/dist/controller/canonicalPublicOperations.d.ts +2 -2
- package/dist/controller/canonicalPublicOperations.js +107 -15
- package/dist/controller/canonicalSelection.d.ts +18 -0
- package/dist/controller/canonicalSelection.js +41 -0
- package/dist/controller/canonicalSession.d.ts +4 -1
- package/dist/controller/canonicalSession.js +106 -36
- package/dist/controller/canonicalStoryEditor.d.ts +3 -1
- package/dist/controller/canonicalStoryEditor.js +21 -1
- package/dist/controller/canonicalStructure.js +8 -3
- package/dist/controller/folioEditor.js +13 -0
- package/dist/controller/headerFooterEditorManager.d.ts +1 -1
- package/dist/controller/headerFooterEditorManager.js +25 -11
- package/dist/controller/hiddenEditorApi.d.ts +13 -2
- package/dist/controller/hiddenEditorApi.js +5 -1
- package/dist/controller/hiddenEditorManager.d.ts +3 -1
- package/dist/controller/hiddenEditorManager.js +167 -14
- package/dist/controller/noteEditorManager.d.ts +1 -1
- package/dist/controller/noteEditorManager.js +28 -13
- package/dist/docx/builtInStyles.js +13 -6
- package/dist/docx/containerChildren.gen.d.ts +3 -1
- package/dist/docx/containerChildren.gen.js +33 -1
- package/dist/docx/fieldParser.js +14 -2
- package/dist/docx/hyperlinkParser.d.ts +1 -1
- package/dist/docx/hyperlinkParser.js +20 -1
- package/dist/docx/inlineWrapperContent.d.ts +4 -4
- package/dist/docx/inlineWrapperContent.js +4 -4
- package/dist/docx/linkedSdtPreservation.d.ts +10 -0
- package/dist/docx/linkedSdtPreservation.js +37 -0
- package/dist/docx/listRendering.d.ts +18 -0
- package/dist/{prosemirror/conversion/listRenderingDefinition.js → docx/listRendering.js} +17 -3
- package/dist/docx/paragraphParser.js +88 -67
- package/dist/docx/paragraphPropertySource.d.ts +22 -1
- package/dist/docx/paragraphPropertySource.js +107 -1
- package/dist/docx/preservedRunContent.js +2 -1
- package/dist/docx/relsParser.js +26 -2
- package/dist/docx/rezip.js +9 -6
- package/dist/docx/selectiveSave.js +10 -8
- package/dist/docx/serializer/commentSerializer.d.ts +4 -1
- package/dist/docx/serializer/commentSerializer.js +7 -3
- package/dist/docx/serializer/paragraphSerializer.js +207 -94
- package/dist/docx/server/materializeYjsDocx.js +15 -7
- package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
- package/dist/i18n/messages/ar.gen.js +4 -0
- package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
- package/dist/i18n/messages/catalogs.gen.js +68 -0
- package/dist/i18n/messages/cs.gen.js +4 -0
- package/dist/i18n/messages/de.gen.js +4 -0
- package/dist/i18n/messages/en.gen.js +4 -0
- package/dist/i18n/messages/es.gen.js +4 -0
- package/dist/i18n/messages/et.gen.js +4 -0
- package/dist/i18n/messages/fr.gen.js +4 -0
- package/dist/i18n/messages/he.gen.js +4 -0
- package/dist/i18n/messages/hi.gen.js +4 -0
- package/dist/i18n/messages/hu.gen.js +4 -0
- package/dist/i18n/messages/lt.gen.js +4 -0
- package/dist/i18n/messages/lv.gen.js +4 -0
- package/dist/i18n/messages/messages.gen.d.ts +4 -0
- package/dist/i18n/messages/pl.gen.js +4 -0
- package/dist/i18n/messages/pt-BR.gen.js +4 -0
- package/dist/i18n/messages/sk.gen.js +4 -0
- package/dist/i18n/messages/tr.gen.js +4 -0
- package/dist/i18n/messages/zh-CN.gen.js +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/internal/revisionResolutionInline.js +25 -3
- package/dist/markdown/internals.js +7 -0
- package/dist/markdown/renderParagraph.js +11 -12
- package/dist/markdown/renderRuns.js +3 -13
- package/dist/markdown/types.d.ts +3 -0
- package/dist/panel-layout.d.ts +11 -9
- package/dist/panel-layout.js +13 -9
- package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
- package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
- package/dist/prosemirror/attrs/index.js +9 -0
- package/dist/prosemirror/commands/comments.js +60 -35
- package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
- package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
- package/dist/prosemirror/commentOps.js +15 -1
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +150 -30
- package/dist/prosemirror/conversion/toProseDoc.js +128 -40
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +22 -5
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
- package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +22 -14
- package/dist/prosemirror/extensions/marks/markUtils.js +8 -7
- package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
- package/dist/prosemirror/extensions/nodes/ImageExtension.js +8 -1
- package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
- package/dist/prosemirror/fieldRepresentation.js +18 -0
- package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
- package/dist/prosemirror/hyperlinkRemoval.js +28 -9
- package/dist/prosemirror/imagePictureNames.d.ts +12 -0
- package/dist/prosemirror/imagePictureNames.js +20 -0
- package/dist/prosemirror/listNumbering.js +11 -5
- package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
- package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
- package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
- package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
- package/dist/prosemirror/noteReferenceReview.js +14 -1
- package/dist/prosemirror/paragraphFormattingProvenance.js +1 -0
- package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
- package/dist/prosemirror/plugins/documentStyles.js +17 -10
- package/dist/prosemirror/plugins/suggestionMode.js +47 -49
- package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
- package/dist/prosemirror/preservedXmlReview.js +35 -0
- package/dist/prosemirror/runFormattingFromMarks.js +2 -2
- package/dist/prosemirror/runFormattingReconciliation.js +5 -4
- package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
- package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
- package/dist/prosemirror/runStyleFormatting.d.ts +74 -2
- package/dist/prosemirror/runStyleFormatting.js +20 -1
- package/dist/prosemirror/schema/marks.d.ts +1 -0
- package/dist/prosemirror/schema/nodes.d.ts +3 -0
- package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
- package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
- package/dist/prosemirror/styles/styleResolver.d.ts +4 -1
- package/dist/prosemirror/styles/styleResolver.js +32 -11
- package/dist/prosemirror/textInput.d.ts +5 -1
- package/dist/prosemirror/textInput.js +3 -3
- package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
- package/dist/prosemirror/trackedRevisionPath.js +39 -1
- package/dist/prosemirror/validation.js +3 -5
- package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
- package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
- package/dist/types/canonicalCapabilities.d.ts +18 -18
- package/dist/types/canonicalCapabilities.js +18 -18
- package/dist/types/canonicalComments.d.ts +51 -0
- package/dist/types/canonicalComments.js +0 -0
- package/dist/types/canonicalSections.d.ts +12 -0
- package/dist/types/canonicalSections.js +0 -0
- package/dist/utils/headingCollector.js +45 -4
- package/dist/utils/outlineDepth.d.ts +9 -0
- package/dist/utils/outlineDepth.js +9 -0
- package/package.json +2 -2
- package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +0 -6
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createNumberingIdAllocator } from "../docx/numberingIds.js";
|
|
2
|
+
import { copyParagraphIndentationProjection } from "../docx/paragraphPropertySource.js";
|
|
2
3
|
import { visitInlineContentSlots, visitParagraphRuns } from "../docx/paragraphTraversal.js";
|
|
3
4
|
import { captureVerbatimXml } from "../docx/verbatimCapture.js";
|
|
4
5
|
import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, OOXML_NAMESPACE_SCOPE, findAttributeByNamespaceUri, getLocalName, parseXml } from "../docx/xmlParser.js";
|
|
6
|
+
import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
|
|
5
7
|
import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
|
|
6
8
|
import { completeNumberingForDoc } from "../prosemirror/listInstanceReferences.js";
|
|
7
9
|
import { marksToTextFormatting } from "../prosemirror/runFormattingFromMarks.js";
|
|
@@ -173,6 +175,22 @@ const importNumbering = ({ destination, source, paragraphs, mode, sourceKind, ex
|
|
|
173
175
|
numIds: nums
|
|
174
176
|
});
|
|
175
177
|
};
|
|
178
|
+
/** Imported comments and notes require copying their source story parts. */
|
|
179
|
+
const canonicalClipboardStoryPartRefusal = (paragraph) => {
|
|
180
|
+
let requiresStory = false;
|
|
181
|
+
visitInlineContentSlots(paragraph, ({ item }) => {
|
|
182
|
+
if (item.type === "commentRangeStart" || item.type === "commentRangeEnd" || item.type === "commentReference") requiresStory = true;
|
|
183
|
+
});
|
|
184
|
+
visitParagraphRuns(paragraph, (run) => {
|
|
185
|
+
if (run.content.some((item) => item.type === "footnoteRef" || item.type === "endnoteRef")) requiresStory = true;
|
|
186
|
+
});
|
|
187
|
+
if (!requiresStory) return void 0;
|
|
188
|
+
return new CanonicalSessionError({
|
|
189
|
+
gap: CANONICAL_GAP.dispatch,
|
|
190
|
+
reason: "refused",
|
|
191
|
+
message: "Clipboard comments and note references require importing their source story parts."
|
|
192
|
+
});
|
|
193
|
+
};
|
|
176
194
|
const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarget, pasteTarget, sourceDocument, moveSource }) => {
|
|
177
195
|
if (session.isComposing) return refuse("Composition must finish before using the clipboard.");
|
|
178
196
|
if (slice.content.size === 0) return Result.err(new CanonicalSessionError({
|
|
@@ -203,12 +221,18 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
|
|
|
203
221
|
const paragraphs = [];
|
|
204
222
|
for (const block of converted.value) {
|
|
205
223
|
if (block.type !== "paragraph") return refuse("The clipboard contains an unsupported block.");
|
|
206
|
-
|
|
224
|
+
const paragraph = structuredClone(block);
|
|
225
|
+
copyParagraphIndentationProjection({
|
|
226
|
+
target: paragraph,
|
|
227
|
+
source: block
|
|
228
|
+
});
|
|
229
|
+
paragraphs.push(paragraph);
|
|
207
230
|
}
|
|
208
231
|
if (moveTarget === void 0 && (plain || sourceDocument === void 0)) flattenClipboardStyleReferences(paragraphs);
|
|
209
|
-
const
|
|
232
|
+
const selectionRange = canonicalSelectionRange(state);
|
|
233
|
+
const from = session.projection.addressAt(pasteTarget ?? moveSource?.from ?? selectionRange.from);
|
|
210
234
|
if (from.isErr()) return from;
|
|
211
|
-
const to = session.projection.addressAt(pasteTarget ?? moveSource?.to ??
|
|
235
|
+
const to = session.projection.addressAt(pasteTarget ?? moveSource?.to ?? selectionRange.to);
|
|
212
236
|
if (to.isErr()) return to;
|
|
213
237
|
let styles = session.document.package.styles;
|
|
214
238
|
let styleIds;
|
|
@@ -309,14 +333,8 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
|
|
|
309
333
|
};
|
|
310
334
|
for (const paragraph of paragraphs) {
|
|
311
335
|
if (moveTarget === void 0) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (item.type === "commentRangeStart" || item.type === "commentRangeEnd" || item.type === "commentReference") requiresStory = true;
|
|
315
|
-
});
|
|
316
|
-
visitParagraphRuns(paragraph, (run) => {
|
|
317
|
-
if (run.content.some((item) => item.type === "footnoteRef" || item.type === "endnoteRef")) requiresStory = true;
|
|
318
|
-
});
|
|
319
|
-
if (requiresStory) return refuse("Clipboard comments and note references require importing their source story parts.");
|
|
336
|
+
const storyPartRefusal = canonicalClipboardStoryPartRefusal(paragraph);
|
|
337
|
+
if (storyPartRefusal !== void 0) return Result.err(storyPartRefusal);
|
|
320
338
|
const links = [];
|
|
321
339
|
visitInlineContentSlots(paragraph, ({ item }) => {
|
|
322
340
|
if (item.type === "hyperlink") links.push(item);
|
|
@@ -335,7 +353,7 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
|
|
|
335
353
|
const runs = [];
|
|
336
354
|
visitParagraphRuns(paragraph, (run) => runs.push(run));
|
|
337
355
|
for (const item of runs) {
|
|
338
|
-
if (plain) item.formatting = marksToTextFormatting(state.storedMarks ?? state.doc.resolve(pasteTarget ?? moveSource?.from ??
|
|
356
|
+
if (plain) item.formatting = marksToTextFormatting(state.storedMarks ?? state.doc.resolve(pasteTarget ?? moveSource?.from ?? selectionRange.from).marks());
|
|
339
357
|
for (const child of item.content) {
|
|
340
358
|
if (child.type !== "drawing") continue;
|
|
341
359
|
if (child.rawXmlMode !== void 0) return refuse("Opaque drawings and embedded objects require their source package parts.");
|
|
@@ -473,4 +491,4 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
|
|
|
473
491
|
});
|
|
474
492
|
};
|
|
475
493
|
//#endregion
|
|
476
|
-
export { prepareCanonicalPaste };
|
|
494
|
+
export { canonicalClipboardStoryPartRefusal, prepareCanonicalPaste };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
|
|
3
|
+
import { Comment } from "../types/content.js";
|
|
4
|
+
import { CreateCommentOp, DocumentOp } from "@stll/docx-core/ops";
|
|
5
|
+
//#region src/controller/canonicalComments.d.ts
|
|
6
|
+
declare const CanonicalCommentError_base: import("better-result").TaggedErrorClass<"CanonicalCommentError">;
|
|
7
|
+
declare class CanonicalCommentError extends CanonicalCommentError_base<{
|
|
8
|
+
message: string;
|
|
9
|
+
gap: typeof CANONICAL_GAP.comments;
|
|
10
|
+
}> {}
|
|
11
|
+
type CanonicalCommentCommand = {
|
|
12
|
+
type: "create";
|
|
13
|
+
comment: CreateCommentOp["comment"];
|
|
14
|
+
anchor: CreateCommentOp["anchor"];
|
|
15
|
+
} | {
|
|
16
|
+
type: "update";
|
|
17
|
+
id: number;
|
|
18
|
+
content: Comment["content"];
|
|
19
|
+
} | {
|
|
20
|
+
type: "resolve";
|
|
21
|
+
id: number;
|
|
22
|
+
status: "open" | "resolved";
|
|
23
|
+
} | {
|
|
24
|
+
type: "delete";
|
|
25
|
+
id: number;
|
|
26
|
+
} | {
|
|
27
|
+
type: "replace";
|
|
28
|
+
comments: readonly Comment[];
|
|
29
|
+
};
|
|
30
|
+
type CompileCanonicalCommentsOptions = {
|
|
31
|
+
document: document_d_exports.Document;
|
|
32
|
+
command: CanonicalCommentCommand;
|
|
33
|
+
};
|
|
34
|
+
declare const compileCanonicalComments: ({ document, command }: CompileCanonicalCommentsOptions) => import("better-result").Err<never, CanonicalCommentError> | import("better-result").Ok<{
|
|
35
|
+
ops: DocumentOp[];
|
|
36
|
+
document: document_d_exports.Document;
|
|
37
|
+
commentId: number | undefined;
|
|
38
|
+
}, never>;
|
|
39
|
+
declare const canonicalCommentBody: (document: document_d_exports.Document, text: string) => Comment["content"];
|
|
40
|
+
//#endregion
|
|
41
|
+
export { CanonicalCommentCommand, CanonicalCommentError, canonicalCommentBody, compileCanonicalComments };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
|
|
2
|
+
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
3
|
+
import { Result, TaggedError } from "better-result";
|
|
4
|
+
import { DOCUMENT_OP_TYPES, allocateCommentAnchorIds, applyDocumentOps, packageParagraphIds } from "@stll/docx-core/ops";
|
|
5
|
+
//#region src/controller/canonicalComments.ts
|
|
6
|
+
/** The one semantic compiler for canonical sidebar, controlled and public comments. */
|
|
7
|
+
var CanonicalCommentError = class extends TaggedError("CanonicalCommentError") {};
|
|
8
|
+
const refuse = (message) => Result.err(new CanonicalCommentError({
|
|
9
|
+
message,
|
|
10
|
+
gap: CANONICAL_GAP.comments
|
|
11
|
+
}));
|
|
12
|
+
const compileCanonicalComments = ({ document, command }) => {
|
|
13
|
+
const current = document.package.document.comments ?? [];
|
|
14
|
+
const byId = new Map(current.map((comment) => [comment.id, comment]));
|
|
15
|
+
const ops = [];
|
|
16
|
+
let commentId;
|
|
17
|
+
switch (command.type) {
|
|
18
|
+
case "create":
|
|
19
|
+
ops.push({
|
|
20
|
+
type: DOCUMENT_OP_TYPES.CREATE_COMMENT,
|
|
21
|
+
comment: command.comment,
|
|
22
|
+
anchor: command.anchor
|
|
23
|
+
});
|
|
24
|
+
commentId = command.comment.id;
|
|
25
|
+
break;
|
|
26
|
+
case "update":
|
|
27
|
+
ops.push({
|
|
28
|
+
type: DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT,
|
|
29
|
+
id: command.id,
|
|
30
|
+
content: command.content
|
|
31
|
+
});
|
|
32
|
+
break;
|
|
33
|
+
case "resolve":
|
|
34
|
+
ops.push({
|
|
35
|
+
type: DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION,
|
|
36
|
+
id: command.id,
|
|
37
|
+
status: command.status
|
|
38
|
+
});
|
|
39
|
+
break;
|
|
40
|
+
case "delete": {
|
|
41
|
+
const comment = byId.get(command.id);
|
|
42
|
+
if (!comment) return refuse("The comment no longer exists.");
|
|
43
|
+
ops.push({
|
|
44
|
+
type: DOCUMENT_OP_TYPES.DELETE_COMMENT,
|
|
45
|
+
id: command.id,
|
|
46
|
+
scope: comment.parentId !== void 0 && byId.has(comment.parentId) ? "reply" : "thread"
|
|
47
|
+
});
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "replace": {
|
|
51
|
+
const nextById = new Map(command.comments.map((comment) => [comment.id, comment]));
|
|
52
|
+
if (nextById.size !== command.comments.length) return refuse("Comment ids must be unique.");
|
|
53
|
+
for (const comment of current) {
|
|
54
|
+
if (nextById.has(comment.id) && comment.parentId !== void 0 && byId.has(comment.parentId) && !nextById.has(comment.parentId)) return refuse("A retained reply requires its parent thread.");
|
|
55
|
+
if (nextById.has(comment.id)) continue;
|
|
56
|
+
if (comment.parentId !== void 0 && !nextById.has(comment.parentId) && byId.has(comment.parentId)) continue;
|
|
57
|
+
ops.push({
|
|
58
|
+
type: DOCUMENT_OP_TYPES.DELETE_COMMENT,
|
|
59
|
+
id: comment.id,
|
|
60
|
+
scope: comment.parentId !== void 0 && byId.has(comment.parentId) ? "reply" : "thread"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const pending = command.comments.filter((comment) => !byId.has(comment.id));
|
|
64
|
+
const available = new Set(current.filter((comment) => nextById.has(comment.id)).map((comment) => comment.id));
|
|
65
|
+
while (pending.length > 0) {
|
|
66
|
+
const index = pending.findIndex((comment) => comment.parentId !== void 0 && available.has(comment.parentId));
|
|
67
|
+
if (index < 0) return refuse("New controlled comments require an existing anchored parent without cycles.");
|
|
68
|
+
const next = pending.splice(index, 1).at(0);
|
|
69
|
+
if (next === void 0 || next.parentId === void 0) return refuse("The controlled reply is missing its parent.");
|
|
70
|
+
const { parentId, ...comment } = next;
|
|
71
|
+
ops.push({
|
|
72
|
+
type: DOCUMENT_OP_TYPES.CREATE_COMMENT,
|
|
73
|
+
comment,
|
|
74
|
+
anchor: {
|
|
75
|
+
kind: "reply",
|
|
76
|
+
parentId
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
available.add(next.id);
|
|
80
|
+
}
|
|
81
|
+
for (const next of command.comments) {
|
|
82
|
+
const previous = byId.get(next.id);
|
|
83
|
+
if (!previous) continue;
|
|
84
|
+
const { content: priorContent, done: priorDone, author: priorAuthor, initials: priorInitials, date: priorDate, ...priorMetadata } = previous;
|
|
85
|
+
const { content: nextContent, done: nextDone, author: nextAuthor, initials: nextInitials, date: nextDate, ...nextMetadata } = next;
|
|
86
|
+
if (canonicalJson(priorMetadata) !== canonicalJson(nextMetadata)) return refuse("Controlled comment identity and opaque metadata cannot be replaced.");
|
|
87
|
+
const patch = {
|
|
88
|
+
...priorAuthor === nextAuthor ? {} : { author: nextAuthor },
|
|
89
|
+
...priorInitials === nextInitials ? {} : { initials: Object.hasOwn(next, "initials") ? nextInitials : null },
|
|
90
|
+
...priorDate === nextDate ? {} : { date: Object.hasOwn(next, "date") ? nextDate : null }
|
|
91
|
+
};
|
|
92
|
+
if (canonicalJson(priorContent) !== canonicalJson(nextContent) || Object.keys(patch).length > 0) ops.push({
|
|
93
|
+
type: DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT,
|
|
94
|
+
id: next.id,
|
|
95
|
+
content: nextContent,
|
|
96
|
+
patch
|
|
97
|
+
});
|
|
98
|
+
if (priorDone !== nextDone) {
|
|
99
|
+
if (nextDone === void 0) return refuse("Resolution changes must name an open or resolved state.");
|
|
100
|
+
ops.push({
|
|
101
|
+
type: DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION,
|
|
102
|
+
id: next.id,
|
|
103
|
+
status: nextDone ? "resolved" : "open"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
default: return command;
|
|
110
|
+
}
|
|
111
|
+
const compiled = [];
|
|
112
|
+
let draft = document;
|
|
113
|
+
for (const op of ops) {
|
|
114
|
+
const materialized = (() => {
|
|
115
|
+
if (op.type !== DOCUMENT_OP_TYPES.CREATE_COMMENT) return Result.ok(op);
|
|
116
|
+
const allocation = allocateCommentAnchorIds(draft, op);
|
|
117
|
+
if (allocation.isErr()) return Result.err(allocation.error);
|
|
118
|
+
return Result.ok({
|
|
119
|
+
...op,
|
|
120
|
+
newIds: allocation.value
|
|
121
|
+
});
|
|
122
|
+
})();
|
|
123
|
+
if (materialized.isErr()) return refuse(materialized.error.message);
|
|
124
|
+
const applied = applyDocumentOps(draft, [materialized.value]);
|
|
125
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
126
|
+
compiled.push(materialized.value);
|
|
127
|
+
draft = applied.value.document;
|
|
128
|
+
}
|
|
129
|
+
return Result.ok({
|
|
130
|
+
ops: compiled,
|
|
131
|
+
document: draft,
|
|
132
|
+
commentId
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const canonicalCommentBody = (document, text) => {
|
|
136
|
+
const occupied = new Set(packageParagraphIds(document.package).map((id) => id.toUpperCase()));
|
|
137
|
+
let id = 1;
|
|
138
|
+
while (occupied.has(id.toString(16).padStart(8, "0").toUpperCase())) id += 1;
|
|
139
|
+
return [{
|
|
140
|
+
type: "paragraph",
|
|
141
|
+
paraId: id.toString(16).padStart(8, "0").toUpperCase(),
|
|
142
|
+
content: [{
|
|
143
|
+
type: "run",
|
|
144
|
+
content: [{
|
|
145
|
+
type: "text",
|
|
146
|
+
text
|
|
147
|
+
}]
|
|
148
|
+
}]
|
|
149
|
+
}];
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
export { CanonicalCommentError, canonicalCommentBody, compileCanonicalComments };
|
|
@@ -13,18 +13,27 @@ type CompositionOptions = {
|
|
|
13
13
|
}) => void;
|
|
14
14
|
refuse: (reason: string) => void;
|
|
15
15
|
};
|
|
16
|
+
type CompositionCompletionEvidence = "provisional" | "explicitFinal";
|
|
17
|
+
type CompositionPhase = {
|
|
18
|
+
type: "active";
|
|
19
|
+
completion: CompositionCompletionEvidence;
|
|
20
|
+
} | {
|
|
21
|
+
type: "ended";
|
|
22
|
+
data: string | null;
|
|
23
|
+
completion: CompositionCompletionEvidence;
|
|
24
|
+
};
|
|
16
25
|
type CompositionState = {
|
|
17
26
|
type: "committed";
|
|
18
27
|
} | {
|
|
19
28
|
type: "provisional";
|
|
20
29
|
baseline: EditorState;
|
|
21
30
|
view: EditorView;
|
|
22
|
-
phase:
|
|
31
|
+
phase: CompositionPhase;
|
|
23
32
|
} | {
|
|
24
33
|
type: "refused";
|
|
25
34
|
baseline: EditorState;
|
|
26
35
|
view: EditorView;
|
|
27
|
-
phase:
|
|
36
|
+
phase: CompositionPhase;
|
|
28
37
|
text: string | null;
|
|
29
38
|
};
|
|
30
39
|
type CompositionFinish = {
|
|
@@ -37,12 +46,16 @@ type CompositionFinish = {
|
|
|
37
46
|
declare const createCanonicalComposition: (options: CompositionOptions) => {
|
|
38
47
|
readonly status: CompositionState["type"];
|
|
39
48
|
readonly active: boolean;
|
|
49
|
+
matchesCancellationRange: (view: EditorView, range: {
|
|
50
|
+
from: number;
|
|
51
|
+
to: number;
|
|
52
|
+
}) => boolean;
|
|
40
53
|
start: (view: EditorView) => void;
|
|
41
|
-
ended: (view: EditorView) => void;
|
|
54
|
+
ended: (view: EditorView, data: string | null) => void;
|
|
42
55
|
forgetCompleted: () => void;
|
|
43
56
|
readonly pendingFinal: boolean;
|
|
44
57
|
authorizeLateFinal: (view: EditorView, text: string | null) => boolean;
|
|
45
|
-
authorizeNative: (view: EditorView) => void;
|
|
58
|
+
authorizeNative: (view: EditorView, completion: CompositionCompletionEvidence) => void;
|
|
46
59
|
flushed: (view: EditorView) => void;
|
|
47
60
|
recover: (view: EditorView) => CompositionFinish;
|
|
48
61
|
cancel: (view: EditorView) => CompositionFinish;
|
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
import { splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
|
|
2
|
+
import { RUN_IDENTITY_MARK_NAME } from "../prosemirror/runIdentity.js";
|
|
2
3
|
import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
|
|
3
4
|
import { setCanonicalInputTimer } from "./canonicalInputTimer.js";
|
|
4
5
|
import { CanonicalSessionError } from "./canonicalSession.js";
|
|
5
6
|
import { Result } from "better-result";
|
|
6
7
|
import { EditorState, TextSelection } from "prosemirror-state";
|
|
7
8
|
//#region src/controller/canonicalComposition.ts
|
|
8
|
-
const
|
|
9
|
+
const matchesNativeReplacement = ({ baseline, proposed, from, to, text }) => {
|
|
10
|
+
const expected = baseline.tr.insertText(text, from, to);
|
|
11
|
+
if (expected.doc.eq(proposed.doc)) return true;
|
|
12
|
+
if (!baseline.doc.resolve(from).sameParent(baseline.doc.resolve(to))) return false;
|
|
13
|
+
if (expected.doc.content.size !== proposed.doc.content.size) return false;
|
|
14
|
+
const markType = baseline.schema.marks[RUN_IDENTITY_MARK_NAME];
|
|
15
|
+
if (markType === void 0) return false;
|
|
16
|
+
const position = expected.doc.resolve(from);
|
|
17
|
+
const paragraph = position.parent;
|
|
18
|
+
const start = position.start();
|
|
19
|
+
const native = proposed.doc.nodeAt(start - 1);
|
|
20
|
+
if (native === null || native.type !== paragraph.type) return false;
|
|
21
|
+
let index = 0;
|
|
22
|
+
let offset = 0;
|
|
23
|
+
const missing = [];
|
|
24
|
+
native.forEach((node, nodeOffset) => {
|
|
25
|
+
const end = nodeOffset + node.nodeSize;
|
|
26
|
+
let cursor = nodeOffset;
|
|
27
|
+
while (cursor < end && index < paragraph.childCount) {
|
|
28
|
+
const authored = paragraph.child(index);
|
|
29
|
+
const authoredEnd = offset + authored.nodeSize;
|
|
30
|
+
const overlapEnd = Math.min(end, authoredEnd);
|
|
31
|
+
if (node.isText && authored.isText && markType.isInSet(authored.marks) !== void 0 && markType.isInSet(node.marks) === void 0) {
|
|
32
|
+
const previous = missing.at(-1);
|
|
33
|
+
if (previous?.to === start + cursor) previous.to = start + overlapEnd;
|
|
34
|
+
else missing.push({
|
|
35
|
+
type: "omittedRunIdentity",
|
|
36
|
+
from: start + cursor,
|
|
37
|
+
to: start + overlapEnd
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
cursor = overlapEnd;
|
|
41
|
+
if (cursor === authoredEnd) {
|
|
42
|
+
offset = authoredEnd;
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
for (const range of missing) expected.removeMark(range.from, range.to, markType);
|
|
48
|
+
return expected.doc.eq(proposed.doc);
|
|
49
|
+
};
|
|
9
50
|
/** Native IME owns the provisional view; the captured canonical projection stays unchanged. */
|
|
10
51
|
const createCanonicalComposition = (options) => {
|
|
11
52
|
let state = { type: "committed" };
|
|
@@ -26,7 +67,13 @@ const createCanonicalComposition = (options) => {
|
|
|
26
67
|
const selectedReplacement = (baseline, proposed) => {
|
|
27
68
|
const { from, to } = baseline.selection;
|
|
28
69
|
const text = selectedText(baseline, proposed);
|
|
29
|
-
if (text === null || !
|
|
70
|
+
if (text === null || !matchesNativeReplacement({
|
|
71
|
+
baseline,
|
|
72
|
+
proposed,
|
|
73
|
+
from,
|
|
74
|
+
to,
|
|
75
|
+
text
|
|
76
|
+
})) return null;
|
|
30
77
|
return {
|
|
31
78
|
from,
|
|
32
79
|
to,
|
|
@@ -50,24 +97,51 @@ const createCanonicalComposition = (options) => {
|
|
|
50
97
|
if (!$from.sameParent($to)) return null;
|
|
51
98
|
const start = $from.start();
|
|
52
99
|
const next = proposed.doc.nodeAt(start - 1);
|
|
53
|
-
if (!next || next.type !== $from.parent.type
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
if (!next || next.type !== $from.parent.type) return null;
|
|
101
|
+
const previous = $from.parent.content;
|
|
102
|
+
const changedFrom = previous.findDiffStart(next.content);
|
|
103
|
+
if (changedFrom === null) {
|
|
104
|
+
if (!baseline.doc.eq(proposed.doc)) return null;
|
|
105
|
+
return {
|
|
106
|
+
from: baseline.selection.from,
|
|
107
|
+
to: baseline.selection.from,
|
|
108
|
+
text: "",
|
|
109
|
+
semantic: "composition"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const changedEnd = previous.findDiffEnd(next.content);
|
|
113
|
+
if (changedEnd === null) return null;
|
|
114
|
+
let from = changedFrom;
|
|
115
|
+
let oldEnd = changedEnd.a;
|
|
116
|
+
let newEnd = changedEnd.b;
|
|
117
|
+
const overlap = from - Math.min(oldEnd, newEnd);
|
|
118
|
+
if (overlap > 0) {
|
|
119
|
+
oldEnd += overlap;
|
|
120
|
+
newEnd += overlap;
|
|
121
|
+
}
|
|
122
|
+
const before = previous.textBetween(0, previous.size, "", "");
|
|
123
|
+
const after = next.content.textBetween(0, next.content.size, "", "");
|
|
124
|
+
while (from < oldEnd && from < newEnd && before[from] === after[from]) from++;
|
|
61
125
|
while (oldEnd > from && newEnd > from && before[oldEnd - 1] === after[newEnd - 1]) {
|
|
62
126
|
oldEnd--;
|
|
63
127
|
newEnd--;
|
|
64
128
|
}
|
|
129
|
+
while (splitsSurrogatePair(before, from) || splitsSurrogatePair(after, from)) from--;
|
|
65
130
|
while (splitsSurrogatePair(before, oldEnd) || splitsSurrogatePair(after, newEnd)) {
|
|
66
131
|
oldEnd++;
|
|
67
132
|
newEnd++;
|
|
68
133
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
134
|
+
const removed = previous.cut(from, oldEnd);
|
|
135
|
+
const inserted = next.content.cut(from, newEnd);
|
|
136
|
+
const text = inserted.textBetween(0, inserted.size, "", "");
|
|
137
|
+
if (removed.textBetween(0, removed.size, "", "").length !== removed.size || text.length !== inserted.size) return null;
|
|
138
|
+
if (!matchesNativeReplacement({
|
|
139
|
+
baseline,
|
|
140
|
+
proposed,
|
|
141
|
+
from: start + from,
|
|
142
|
+
to: start + oldEnd,
|
|
143
|
+
text
|
|
144
|
+
})) return null;
|
|
71
145
|
return {
|
|
72
146
|
from: start + from,
|
|
73
147
|
to: start + oldEnd,
|
|
@@ -99,7 +173,7 @@ const createCanonicalComposition = (options) => {
|
|
|
99
173
|
view.updateState(pending.baseline);
|
|
100
174
|
}
|
|
101
175
|
options.end();
|
|
102
|
-
if (!commit || view.isDestroyed) return { type: "cancelled" };
|
|
176
|
+
if (!commit || view.isDestroyed || pending.phase.type === "ended" && pending.phase.data === "" && pending.phase.completion === "provisional" && committedText === "") return { type: "cancelled" };
|
|
103
177
|
if (pending.type === "refused") return {
|
|
104
178
|
type: "refused",
|
|
105
179
|
text: pending.text
|
|
@@ -130,7 +204,7 @@ const createCanonicalComposition = (options) => {
|
|
|
130
204
|
};
|
|
131
205
|
const schedule = (view) => {
|
|
132
206
|
clearTimer();
|
|
133
|
-
timer = setCanonicalInputTimer(() => finish(view, true),
|
|
207
|
+
timer = setCanonicalInputTimer(() => finish(view, true), 25);
|
|
134
208
|
};
|
|
135
209
|
return {
|
|
136
210
|
get status() {
|
|
@@ -139,6 +213,18 @@ const createCanonicalComposition = (options) => {
|
|
|
139
213
|
get active() {
|
|
140
214
|
return state.type !== "committed";
|
|
141
215
|
},
|
|
216
|
+
matchesCancellationRange: (view, range) => {
|
|
217
|
+
if (state.type === "committed" || state.view !== view) return false;
|
|
218
|
+
const text = selectedText(state.baseline, view.state);
|
|
219
|
+
if (text === null || text.length === 0) return false;
|
|
220
|
+
const from = state.baseline.selection.from;
|
|
221
|
+
if (range.from !== from || range.to !== from + text.length) return false;
|
|
222
|
+
const selection = view.dom.ownerDocument.getSelection();
|
|
223
|
+
if (!selection || selection.rangeCount !== 1) return false;
|
|
224
|
+
const selected = selection.getRangeAt(0);
|
|
225
|
+
if (!view.dom.contains(selected.startContainer) || !view.dom.contains(selected.endContainer)) return false;
|
|
226
|
+
return view.posAtDOM(selected.startContainer, selected.startOffset) === range.from && view.posAtDOM(selected.endContainer, selected.endOffset) === range.to;
|
|
227
|
+
},
|
|
142
228
|
start: (view) => {
|
|
143
229
|
if (state.type !== "committed") return;
|
|
144
230
|
if (!options.begin()) return;
|
|
@@ -146,10 +232,13 @@ const createCanonicalComposition = (options) => {
|
|
|
146
232
|
type: "provisional",
|
|
147
233
|
baseline: view.state,
|
|
148
234
|
view,
|
|
149
|
-
phase:
|
|
235
|
+
phase: {
|
|
236
|
+
type: "active",
|
|
237
|
+
completion: "provisional"
|
|
238
|
+
}
|
|
150
239
|
};
|
|
151
240
|
},
|
|
152
|
-
ended: (view) => {
|
|
241
|
+
ended: (view, data) => {
|
|
153
242
|
if (state.type === "committed") return;
|
|
154
243
|
switch (state.type) {
|
|
155
244
|
case "provisional":
|
|
@@ -157,7 +246,11 @@ const createCanonicalComposition = (options) => {
|
|
|
157
246
|
type: "provisional",
|
|
158
247
|
baseline: state.baseline,
|
|
159
248
|
view: state.view,
|
|
160
|
-
phase:
|
|
249
|
+
phase: {
|
|
250
|
+
type: "ended",
|
|
251
|
+
data,
|
|
252
|
+
completion: state.phase.completion
|
|
253
|
+
}
|
|
161
254
|
};
|
|
162
255
|
break;
|
|
163
256
|
case "refused":
|
|
@@ -165,7 +258,11 @@ const createCanonicalComposition = (options) => {
|
|
|
165
258
|
type: "refused",
|
|
166
259
|
baseline: state.baseline,
|
|
167
260
|
view: state.view,
|
|
168
|
-
phase:
|
|
261
|
+
phase: {
|
|
262
|
+
type: "ended",
|
|
263
|
+
data,
|
|
264
|
+
completion: state.phase.completion
|
|
265
|
+
},
|
|
169
266
|
text: state.text
|
|
170
267
|
};
|
|
171
268
|
break;
|
|
@@ -193,15 +290,23 @@ const createCanonicalComposition = (options) => {
|
|
|
193
290
|
};
|
|
194
291
|
return true;
|
|
195
292
|
},
|
|
196
|
-
authorizeNative: (view) => {
|
|
293
|
+
authorizeNative: (view, completion) => {
|
|
197
294
|
authorizedNativeState = view.state;
|
|
295
|
+
if (state.type !== "committed" && completion === "explicitFinal") state.phase = state.phase.type === "active" ? {
|
|
296
|
+
type: "active",
|
|
297
|
+
completion
|
|
298
|
+
} : {
|
|
299
|
+
type: "ended",
|
|
300
|
+
data: state.phase.data,
|
|
301
|
+
completion
|
|
302
|
+
};
|
|
198
303
|
},
|
|
199
304
|
flushed: (view) => {
|
|
200
305
|
const authorized = authorizedNativeState;
|
|
201
306
|
queueMicrotask(() => {
|
|
202
307
|
if (authorizedNativeState === authorized) authorizedNativeState = null;
|
|
203
308
|
});
|
|
204
|
-
if (state.type !== "committed" && state.phase === "ended") schedule(view);
|
|
309
|
+
if (state.type !== "committed" && state.phase.type === "ended") schedule(view);
|
|
205
310
|
},
|
|
206
311
|
recover: (view) => finish(view, true),
|
|
207
312
|
cancel: (view) => finish(view, false),
|
|
@@ -106,8 +106,13 @@ type CanonicalInlineProjectionArgs = {
|
|
|
106
106
|
source: document_d_exports.Paragraph;
|
|
107
107
|
paragraph: Node;
|
|
108
108
|
pairedBookmarkIds: ReadonlySet<number>;
|
|
109
|
+
commentContext?: {
|
|
110
|
+
rangedIds: ReadonlySet<number>;
|
|
111
|
+
pointIds: ReadonlySet<number>;
|
|
112
|
+
openIds: ReadonlySet<number>;
|
|
113
|
+
};
|
|
109
114
|
};
|
|
110
115
|
/** Bind rendered gaps to the operation owner's source leaf ordinals, including collapsed pairs. */
|
|
111
|
-
declare const projectCanonicalInline: ({ source, paragraph, pairedBookmarkIds }: CanonicalInlineProjectionArgs) => Result<InlineProjection, InlineProjectionError>;
|
|
116
|
+
declare const projectCanonicalInline: ({ source, paragraph, pairedBookmarkIds, commentContext }: CanonicalInlineProjectionArgs) => Result<InlineProjection, InlineProjectionError>;
|
|
112
117
|
//#endregion
|
|
113
118
|
export { CANONICAL_NATIVE_INLINE_POLICIES, CANONICAL_SOURCE_INLINE_POLICIES, CanonicalInlineGap, canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline };
|