@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.
- package/dist/ai-edits/apply.js +169 -38
- package/dist/ai-edits/headless.js +2 -2
- package/dist/ai-edits/types.d.ts +7 -1
- package/dist/compare/compare.js +1 -0
- package/dist/compare/reproducible-package.d.ts +0 -17
- package/dist/compare/reproducible-package.js +12 -18
- package/dist/controller/canonicalClipboard.d.ts +23 -0
- package/dist/controller/canonicalClipboard.js +476 -0
- package/dist/controller/canonicalClipboardResources.d.ts +21 -0
- package/dist/controller/canonicalClipboardResources.js +382 -0
- package/dist/controller/canonicalComposition.d.ts +20 -3
- package/dist/controller/canonicalComposition.js +166 -38
- package/dist/controller/canonicalInlineProjection.d.ts +113 -0
- package/dist/controller/canonicalInlineProjection.js +393 -0
- package/dist/controller/canonicalInput.d.ts +13 -4
- package/dist/controller/canonicalInput.js +52 -13
- package/dist/controller/canonicalInputTimer.d.ts +7 -0
- package/dist/controller/canonicalInputTimer.js +11 -0
- package/dist/controller/canonicalPublicOperations.d.ts +48 -0
- package/dist/controller/canonicalPublicOperations.js +556 -0
- package/dist/controller/canonicalSession.d.ts +24 -6
- package/dist/controller/canonicalSession.js +259 -111
- package/dist/controller/canonicalStoryEditor.d.ts +5 -3
- package/dist/controller/canonicalStoryEditor.js +7 -2
- package/dist/controller/canonicalStructure.js +57 -4
- package/dist/controller/canonicalTocSelection.d.ts +11 -0
- package/dist/controller/canonicalTocSelection.js +43 -0
- package/dist/controller/folioEditor.d.ts +2 -9
- package/dist/controller/folioEditor.js +5 -4
- package/dist/controller/headerFooterEditorManager.d.ts +2 -1
- package/dist/controller/headerFooterEditorManager.js +8 -7
- package/dist/controller/hiddenEditorApi.d.ts +12 -2
- package/dist/controller/hiddenEditorApi.js +4 -0
- package/dist/controller/hiddenEditorManager.d.ts +3 -1
- package/dist/controller/hiddenEditorManager.js +216 -34
- package/dist/controller/noteEditorManager.d.ts +2 -1
- package/dist/controller/noteEditorManager.js +8 -7
- package/dist/document-operations.d.ts +8 -1
- package/dist/document-operations.js +6 -4
- package/dist/docx/blockPlainText.d.ts +1 -1
- package/dist/docx/blockPlainText.js +11 -3
- package/dist/docx/canonicalResourceSave.d.ts +16 -0
- package/dist/docx/canonicalResourceSave.js +27 -0
- package/dist/docx/canonicalSave.d.ts +29 -0
- package/dist/docx/canonicalSave.js +101 -0
- package/dist/docx/canonicalStoryRepack.d.ts +1 -1
- package/dist/docx/canonicalStoryRepack.js +38 -5
- package/dist/docx/documentParser.d.ts +1 -13
- package/dist/docx/documentParser.js +1 -47
- package/dist/docx/ensureParaIds.js +10 -4
- package/dist/docx/footnoteParser.d.ts +1 -11
- package/dist/docx/footnoteParser.js +1 -16
- package/dist/docx/headerFooterParser.d.ts +1 -9
- package/dist/docx/headerFooterParser.js +1 -12
- package/dist/docx/headerFooterVerbatim.d.ts +26 -1
- package/dist/docx/headerFooterVerbatim.js +81 -3
- package/dist/docx/imageParser.d.ts +13 -1
- package/dist/docx/imageParser.js +20 -11
- package/dist/docx/metadataPrivacy.js +6 -1
- package/dist/docx/packageParts.js +9 -3
- package/dist/docx/paragraphPropertySource.js +2 -1
- package/dist/docx/parser.js +2 -1
- package/dist/docx/removeHeaderFooterParts.js +17 -6
- package/dist/docx/rezip.d.ts +49 -5
- package/dist/docx/rezip.js +745 -229
- package/dist/docx/saveDiagnostics.d.ts +11 -0
- package/dist/docx/selectiveSave.d.ts +2 -1
- package/dist/docx/selectiveSave.js +32 -3
- package/dist/docx/selectiveXmlPatch.js +7 -5
- package/dist/docx/serializer/documentSerializer.d.ts +9 -2
- package/dist/docx/serializer/documentSerializer.js +29 -10
- package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
- package/dist/docx/storyBlockReplay.d.ts +21 -1
- package/dist/docx/storyBlockReplay.js +113 -9
- package/dist/docx/storyPlainText.d.ts +20 -0
- package/dist/docx/storyPlainText.js +23 -0
- package/dist/docx/structuralXmlPatch.js +10 -22
- package/dist/docx/styleParser.d.ts +5 -1
- package/dist/docx/styleParser.js +1 -1
- package/dist/docx/tableParser.d.ts +1 -8
- package/dist/docx/tableParser.js +1 -28
- package/dist/docx/textBoxParser.d.ts +1 -5
- package/dist/docx/textBoxParser.js +1 -20
- package/dist/docx/unzip.js +6 -1
- package/dist/internal/acceptedBlockProjection.d.ts +12 -0
- package/dist/internal/acceptedBlockProjection.js +45 -0
- package/dist/managers/DocumentLoaderManager.js +2 -1
- package/dist/markdown/escape.d.ts +3 -1
- package/dist/markdown/escape.js +9 -2
- package/dist/markdown/renderBlock.js +5 -120
- package/dist/markdown/renderParagraph.js +4 -2
- package/dist/markdown/renderRuns.d.ts +7 -1
- package/dist/markdown/renderRuns.js +32 -13
- package/dist/prosemirror/canonicalCommands.d.ts +28 -4
- package/dist/prosemirror/canonicalCommands.js +11 -4
- package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
- package/dist/prosemirror/canonicalSelectionRange.js +15 -0
- package/dist/prosemirror/clipboardIntent.d.ts +7 -0
- package/dist/prosemirror/clipboardIntent.js +9 -0
- package/dist/prosemirror/commands/pageBreak.js +2 -2
- package/dist/prosemirror/commands/pastePlainText.js +2 -1
- package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
- package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
- package/dist/prosemirror/conversion/toProseDoc.js +5 -4
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +39 -30
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
- package/dist/prosemirror/extensions/marks/markUtils.js +3 -1
- package/dist/prosemirror/insertOperations.js +2 -1
- package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
- package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
- package/dist/prosemirror/plugins/suggestionMode.js +7 -5
- package/dist/types/canonicalCapabilities.d.ts +215 -0
- package/dist/types/canonicalCapabilities.js +212 -0
- package/dist/types/canonicalSave.d.ts +11 -0
- package/dist/types/canonicalSave.js +0 -0
- package/dist/types/docxSerialization.d.ts +14 -0
- package/dist/types/docxSerialization.js +7 -0
- package/package.json +4 -4
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { readNoteMarkerAttrs } from "../internal/noteMarkerAttrs.js";
|
|
2
|
+
import { readFootnoteRefMarkAttrs, readPreservedXmlAttrs } from "../prosemirror/attrs/index.js";
|
|
3
|
+
import { bookmarkMarkerFromAttrs, readBookmarkBoundaryAttrs } from "../prosemirror/bookmarkBoundaryAttrs.js";
|
|
4
|
+
import { HYPHEN_TEXT_CARRIERS } from "../prosemirror/conversion/hyphenTextCarriers.js";
|
|
5
|
+
import { readMoveRangeBoundaryAttrs } from "../prosemirror/moveRangeBoundaryAttrs.js";
|
|
6
|
+
import { readRangeAnchorAttrs } from "../prosemirror/rangeAnchorAttrs.js";
|
|
7
|
+
import { PRESERVED_XML_LEVELS } from "../prosemirror/schema/nodes.js";
|
|
8
|
+
import { Result, TaggedError, panic } from "better-result";
|
|
9
|
+
import { hasIllegalXmlCharacters } from "@stll/docx-core";
|
|
10
|
+
import { inlineLeafSpans, paragraphLogicalText } from "@stll/docx-core/ops";
|
|
11
|
+
//#region src/controller/canonicalInlineProjection.ts
|
|
12
|
+
const CANONICAL_SOURCE_INLINE_POLICIES = {
|
|
13
|
+
run: "container",
|
|
14
|
+
hyperlink: "container",
|
|
15
|
+
insertion: "container",
|
|
16
|
+
deletion: "container",
|
|
17
|
+
moveFrom: "container",
|
|
18
|
+
moveTo: "container",
|
|
19
|
+
inlineWrapper: "container",
|
|
20
|
+
inlineSdt: "control",
|
|
21
|
+
simpleField: "leaf",
|
|
22
|
+
complexField: "leaf",
|
|
23
|
+
mathEquation: "leaf",
|
|
24
|
+
preservedInline: "leaf",
|
|
25
|
+
bookmarkStart: "leaf",
|
|
26
|
+
bookmarkEnd: "leaf",
|
|
27
|
+
commentRangeStart: "leaf",
|
|
28
|
+
commentRangeEnd: "leaf",
|
|
29
|
+
commentReference: "leaf",
|
|
30
|
+
moveFromRangeStart: "leaf",
|
|
31
|
+
moveFromRangeEnd: "leaf",
|
|
32
|
+
moveToRangeStart: "leaf",
|
|
33
|
+
moveToRangeEnd: "leaf",
|
|
34
|
+
text: "text",
|
|
35
|
+
tab: "leaf",
|
|
36
|
+
break: "leaf",
|
|
37
|
+
symbol: "leaf",
|
|
38
|
+
footnoteRef: "leaf",
|
|
39
|
+
endnoteRef: "leaf",
|
|
40
|
+
noteMarker: "leaf",
|
|
41
|
+
fieldChar: "rawField",
|
|
42
|
+
instrText: "rawField",
|
|
43
|
+
softHyphen: "leaf",
|
|
44
|
+
noBreakHyphen: "leaf",
|
|
45
|
+
renderedPageBreak: "layout",
|
|
46
|
+
preservedXml: "leaf",
|
|
47
|
+
drawing: "leaf",
|
|
48
|
+
shape: "leaf"
|
|
49
|
+
};
|
|
50
|
+
const CANONICAL_NATIVE_INLINE_POLICIES = {
|
|
51
|
+
text: "text",
|
|
52
|
+
bookmarkBoundary: "markers",
|
|
53
|
+
moveRangeBoundary: "markers",
|
|
54
|
+
rangeAnchor: "markers",
|
|
55
|
+
noteMarker: "noteMarker",
|
|
56
|
+
preservedXml: "capture",
|
|
57
|
+
field: "unit",
|
|
58
|
+
structuredField: "unit",
|
|
59
|
+
hardBreak: "unit",
|
|
60
|
+
image: "unit",
|
|
61
|
+
math: "unit",
|
|
62
|
+
pageBreakRun: "unit",
|
|
63
|
+
shape: "unit",
|
|
64
|
+
symbol: "unit",
|
|
65
|
+
tab: "unit",
|
|
66
|
+
textBoxAnchor: "unit",
|
|
67
|
+
commentReference: "unit",
|
|
68
|
+
renderedPageBreak: "refused",
|
|
69
|
+
sdt: "refused"
|
|
70
|
+
};
|
|
71
|
+
const nativePolicies = new Map(Object.entries(CANONICAL_NATIVE_INLINE_POLICIES));
|
|
72
|
+
var InlineProjectionError = class extends TaggedError("InlineProjectionError") {};
|
|
73
|
+
const refuse = (message) => Result.err(new InlineProjectionError({ message }));
|
|
74
|
+
/** Every source variant makes a decision before conversion can discard its positions. */
|
|
75
|
+
const canonicalInlineSourceIssue = (items, context = "paragraph") => {
|
|
76
|
+
for (const item of items) switch (CANONICAL_SOURCE_INLINE_POLICIES[item.type]) {
|
|
77
|
+
case "control":
|
|
78
|
+
if (context === "fieldPayload" && item.type === "inlineSdt") {
|
|
79
|
+
const issue = canonicalInlineSourceIssue(item.content, context);
|
|
80
|
+
if (issue !== null) return issue;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return "Canonical editing cannot preserve inline content-control boundary positions.";
|
|
84
|
+
case "rawField":
|
|
85
|
+
if (context === "fieldPayload") break;
|
|
86
|
+
return "Canonical editing cannot preserve unstructured field instructions; a structured field is required.";
|
|
87
|
+
case "layout": return "Canonical editing cannot preserve cached rendered page-break positions.";
|
|
88
|
+
case "text":
|
|
89
|
+
if (item.type === "text" && (hasIllegalXmlCharacters(item.text) || /[\t\r\n]/u.test(item.text))) return "Canonical text requires valid XML characters and explicit tab or break elements.";
|
|
90
|
+
break;
|
|
91
|
+
case "container": {
|
|
92
|
+
if ((item.type === "insertion" || item.type === "deletion") && item.resolutionJoins !== void 0 && item.content.some((child) => child.type === "hyperlink")) return "Canonical editing cannot preserve hyperlink wrapper review provenance.";
|
|
93
|
+
const children = sourceContainerChildren(item);
|
|
94
|
+
const issue = canonicalInlineSourceIssue(children, context);
|
|
95
|
+
if (issue !== null) return issue;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "leaf": {
|
|
99
|
+
let children = [];
|
|
100
|
+
if (item.type === "simpleField") children = item.content;
|
|
101
|
+
if (item.type === "complexField") children = [...item.fieldCode, ...item.fieldResult];
|
|
102
|
+
const issue = canonicalInlineSourceIssue(children, "fieldPayload");
|
|
103
|
+
if (issue !== null) return issue;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
};
|
|
110
|
+
const sourceContainerChildren = (item) => {
|
|
111
|
+
switch (item.type) {
|
|
112
|
+
case "hyperlink": return item.children;
|
|
113
|
+
case "run":
|
|
114
|
+
case "insertion":
|
|
115
|
+
case "deletion":
|
|
116
|
+
case "moveFrom":
|
|
117
|
+
case "moveTo":
|
|
118
|
+
case "inlineWrapper": return item.content;
|
|
119
|
+
default: return panic(`Source policy incorrectly classified ${item.type} as a container.`);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/** Native positions and deletion geometry read the same inline-cell classification. */
|
|
123
|
+
const canonicalNativeCells = (paragraph) => {
|
|
124
|
+
const cells = [];
|
|
125
|
+
let issue;
|
|
126
|
+
paragraph.forEach((node, from) => {
|
|
127
|
+
if (issue !== void 0) return;
|
|
128
|
+
const to = from + node.nodeSize;
|
|
129
|
+
switch (nativePolicies.get(node.type.name)) {
|
|
130
|
+
case "text": {
|
|
131
|
+
const reference = node.marks.find(({ type }) => type.name === "footnoteRef");
|
|
132
|
+
if (!reference) {
|
|
133
|
+
cells.push({
|
|
134
|
+
type: "text",
|
|
135
|
+
text: node.text ?? "",
|
|
136
|
+
from,
|
|
137
|
+
to,
|
|
138
|
+
node
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const attrs = readFootnoteRefMarkAttrs(reference);
|
|
143
|
+
if (!attrs.ok) {
|
|
144
|
+
issue = "Canonical editing requires valid note-reference mark attributes.";
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const label = String(attrs.value.id);
|
|
148
|
+
const text = node.text ?? "";
|
|
149
|
+
if (label.length === 0 || text.length % label.length !== 0 || text !== label.repeat(text.length / label.length)) {
|
|
150
|
+
issue = "Canonical editing cannot map the rendered note-reference labels.";
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (let offset = 0; offset < text.length; offset += label.length) cells.push({
|
|
154
|
+
type: "markedUnit",
|
|
155
|
+
sourceType: attrs.value.noteType === "endnote" ? "endnoteRef" : "footnoteRef",
|
|
156
|
+
label,
|
|
157
|
+
from: from + offset,
|
|
158
|
+
to: from + offset + label.length,
|
|
159
|
+
node
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
case "unit":
|
|
164
|
+
cells.push({
|
|
165
|
+
type: "unit",
|
|
166
|
+
from,
|
|
167
|
+
to,
|
|
168
|
+
node
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
case "markers":
|
|
172
|
+
if (node.type.name === "bookmarkBoundary") {
|
|
173
|
+
const attrs = readBookmarkBoundaryAttrs(node);
|
|
174
|
+
if (!attrs.ok) {
|
|
175
|
+
issue = "Canonical editing requires valid bookmark boundary attributes.";
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
cells.push({
|
|
179
|
+
type: "zeroWidth",
|
|
180
|
+
sources: [bookmarkMarkerFromAttrs(attrs.value)],
|
|
181
|
+
from,
|
|
182
|
+
to,
|
|
183
|
+
node
|
|
184
|
+
});
|
|
185
|
+
} else if (node.type.name === "moveRangeBoundary") {
|
|
186
|
+
const attrs = readMoveRangeBoundaryAttrs(node);
|
|
187
|
+
if (!attrs.ok) {
|
|
188
|
+
issue = "Canonical editing requires valid move-range boundary attributes.";
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
cells.push({
|
|
192
|
+
type: "zeroWidth",
|
|
193
|
+
sources: [attrs.value],
|
|
194
|
+
from,
|
|
195
|
+
to,
|
|
196
|
+
node
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
const attrs = readRangeAnchorAttrs(node);
|
|
200
|
+
if (!attrs.ok) {
|
|
201
|
+
issue = "Canonical editing requires a valid collapsed range anchor.";
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
cells.push({
|
|
205
|
+
type: "zeroWidth",
|
|
206
|
+
sources: [attrs.value.start, attrs.value.end],
|
|
207
|
+
from,
|
|
208
|
+
to,
|
|
209
|
+
node
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
case "noteMarker": {
|
|
214
|
+
const attrs = readNoteMarkerAttrs(node);
|
|
215
|
+
if (!attrs.ok) {
|
|
216
|
+
issue = "Canonical editing requires a valid automatic note marker.";
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
cells.push({
|
|
220
|
+
type: "zeroWidth",
|
|
221
|
+
sources: [{
|
|
222
|
+
type: "noteMarker",
|
|
223
|
+
kind: attrs.value.kind
|
|
224
|
+
}],
|
|
225
|
+
from,
|
|
226
|
+
to,
|
|
227
|
+
node
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
case "capture": {
|
|
232
|
+
const attrs = readPreservedXmlAttrs(node);
|
|
233
|
+
if (!attrs.ok) {
|
|
234
|
+
issue = "Canonical editing requires valid preserved inline markup.";
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (attrs.value.level === PRESERVED_XML_LEVELS.inline && attrs.value.text === "") cells.push({
|
|
238
|
+
type: "zeroWidth",
|
|
239
|
+
sources: [{
|
|
240
|
+
type: "preservedInline",
|
|
241
|
+
xml: attrs.value.xml,
|
|
242
|
+
text: ""
|
|
243
|
+
}],
|
|
244
|
+
from,
|
|
245
|
+
to,
|
|
246
|
+
node
|
|
247
|
+
});
|
|
248
|
+
else cells.push({
|
|
249
|
+
type: "unit",
|
|
250
|
+
from,
|
|
251
|
+
to,
|
|
252
|
+
node
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case "refused":
|
|
257
|
+
issue = `Canonical editing cannot preserve ${node.type.name} boundary positions.`;
|
|
258
|
+
return;
|
|
259
|
+
case void 0:
|
|
260
|
+
issue = `Canonical editing has no position mapping for inline ${node.type.name}.`;
|
|
261
|
+
return;
|
|
262
|
+
default:
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return issue === void 0 ? Result.ok(cells) : refuse(issue);
|
|
266
|
+
};
|
|
267
|
+
const sameSourceMarker = (left, right) => {
|
|
268
|
+
switch (left.type) {
|
|
269
|
+
case "bookmarkStart":
|
|
270
|
+
case "bookmarkEnd":
|
|
271
|
+
case "commentRangeStart":
|
|
272
|
+
case "commentRangeEnd":
|
|
273
|
+
case "moveFromRangeStart":
|
|
274
|
+
case "moveFromRangeEnd":
|
|
275
|
+
case "moveToRangeStart":
|
|
276
|
+
case "moveToRangeEnd": return right.type === left.type && left.id === right.id;
|
|
277
|
+
case "noteMarker": return right.type === "noteMarker" && left.kind === right.kind;
|
|
278
|
+
case "preservedInline": return right.type === "preservedInline" && left.xml === right.xml && left.text === right.text;
|
|
279
|
+
default: return false;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/** Bind rendered gaps to the operation owner's source leaf ordinals, including collapsed pairs. */
|
|
283
|
+
const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds }) => {
|
|
284
|
+
const native = canonicalNativeCells(paragraph);
|
|
285
|
+
if (native.isErr()) return native;
|
|
286
|
+
const spans = inlineLeafSpans(source.content);
|
|
287
|
+
const erasedBookmarks = /* @__PURE__ */ new Set();
|
|
288
|
+
const collectErasedBookmarks = (items, context) => {
|
|
289
|
+
for (const item of items) {
|
|
290
|
+
if (item.type === "bookmarkStart" || item.type === "bookmarkEnd") {
|
|
291
|
+
if (context === "paragraph" && !pairedBookmarkIds.has(item.id)) erasedBookmarks.add(item);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (CANONICAL_SOURCE_INLINE_POLICIES[item.type] !== "container") continue;
|
|
295
|
+
collectErasedBookmarks(sourceContainerChildren(item), item.type === "run" || item.type === "inlineWrapper" ? context : "nested");
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
collectErasedBookmarks(source.content, "paragraph");
|
|
299
|
+
const text = paragraphLogicalText(source);
|
|
300
|
+
const boundaries = Array.from({ length: text.length + 1 }, () => []);
|
|
301
|
+
let index = 0;
|
|
302
|
+
let consumed = 0;
|
|
303
|
+
const addGap = (position, gap) => {
|
|
304
|
+
const gaps = boundaries.at(gap.offset);
|
|
305
|
+
if (!gaps) panic(`Source leaf offset ${gap.offset} exceeds paragraph ${source.paraId}.`);
|
|
306
|
+
const existing = gaps.findIndex((entry) => entry.position === position);
|
|
307
|
+
const entry = {
|
|
308
|
+
position,
|
|
309
|
+
zeroWidthBefore: gap.zeroWidthBefore
|
|
310
|
+
};
|
|
311
|
+
if (existing < 0) gaps.push(entry);
|
|
312
|
+
else gaps[existing] = entry;
|
|
313
|
+
};
|
|
314
|
+
const consumeErasedLeaves = (position) => {
|
|
315
|
+
while (consumed === 0) {
|
|
316
|
+
const span = spans.at(index);
|
|
317
|
+
if (!span || span.before.offset !== span.after.offset || !(erasedBookmarks.has(span.node) || CANONICAL_SOURCE_INLINE_POLICIES[span.node.type] === "container" && sourceContainerChildren(span.node).length === 0)) break;
|
|
318
|
+
index += 1;
|
|
319
|
+
if (position !== null) addGap(position, span.after);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
addGap(0, {
|
|
323
|
+
offset: 0,
|
|
324
|
+
zeroWidthBefore: 0
|
|
325
|
+
});
|
|
326
|
+
for (const cell of native.value) {
|
|
327
|
+
consumeErasedLeaves(cell.from);
|
|
328
|
+
let span = spans.at(index);
|
|
329
|
+
if (!span) return refuse(`Canonical editing cannot map native ${cell.node.type.name} to a source leaf.`);
|
|
330
|
+
addGap(cell.from, consumed === 0 ? span.before : {
|
|
331
|
+
offset: span.before.offset + consumed,
|
|
332
|
+
zeroWidthBefore: 0
|
|
333
|
+
});
|
|
334
|
+
switch (cell.type) {
|
|
335
|
+
case "zeroWidth":
|
|
336
|
+
for (const marker of cell.sources) {
|
|
337
|
+
consumeErasedLeaves(null);
|
|
338
|
+
span = spans.at(index);
|
|
339
|
+
if (!span || consumed !== 0 || span.before.offset !== span.after.offset || !sameSourceMarker(span.node, marker)) return refuse(`Canonical editing cannot preserve ${marker.type} source boundary positions.`);
|
|
340
|
+
index += 1;
|
|
341
|
+
}
|
|
342
|
+
if (!span) return refuse("Canonical editing lost a source range boundary.");
|
|
343
|
+
addGap(cell.to, span.after);
|
|
344
|
+
break;
|
|
345
|
+
case "unit":
|
|
346
|
+
case "markedUnit":
|
|
347
|
+
if (cell.type === "markedUnit" && (span.node.type !== "footnoteRef" && span.node.type !== "endnoteRef" || span.node.type !== cell.sourceType || String(span.node.id) !== cell.label)) return refuse("Canonical editing cannot preserve note-reference identity.");
|
|
348
|
+
if (consumed !== 0 || span.node.type === "text" || span.after.offset - span.before.offset !== 1) return refuse(`Canonical editing cannot map ${span.node.type} to one native inline unit.`);
|
|
349
|
+
index += 1;
|
|
350
|
+
addGap(cell.to, span.after);
|
|
351
|
+
break;
|
|
352
|
+
case "text":
|
|
353
|
+
for (let unit = 0; unit < cell.text.length; unit += 1) {
|
|
354
|
+
consumeErasedLeaves(cell.from + unit);
|
|
355
|
+
span = spans.at(index);
|
|
356
|
+
if (!span) return refuse("Canonical editing cannot map missing source text to native text positions.");
|
|
357
|
+
let value;
|
|
358
|
+
switch (span.node.type) {
|
|
359
|
+
case "text":
|
|
360
|
+
value = span.node.text;
|
|
361
|
+
break;
|
|
362
|
+
case "softHyphen":
|
|
363
|
+
case "noBreakHyphen":
|
|
364
|
+
value = HYPHEN_TEXT_CARRIERS[span.node.type];
|
|
365
|
+
break;
|
|
366
|
+
default: return refuse(`Canonical editing cannot map ${span.node.type} to native text positions.`);
|
|
367
|
+
}
|
|
368
|
+
if (value.charAt(consumed) !== cell.text.charAt(unit)) return refuse("Canonical editing cannot preserve the source text carrier.");
|
|
369
|
+
consumed += 1;
|
|
370
|
+
addGap(cell.from + unit + 1, {
|
|
371
|
+
offset: span.before.offset + consumed,
|
|
372
|
+
zeroWidthBefore: 0
|
|
373
|
+
});
|
|
374
|
+
if (consumed === value.length) {
|
|
375
|
+
index += 1;
|
|
376
|
+
consumed = 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
default:
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
consumeErasedLeaves(paragraph.content.size);
|
|
384
|
+
const missing = spans.at(index);
|
|
385
|
+
if (missing || consumed !== 0) return refuse(`Canonical editing cannot display the source positions of ${missing?.node.type ?? "incomplete text"}.`);
|
|
386
|
+
if (boundaries.some((gaps) => gaps.length === 0)) return refuse("Canonical editing lost a source character boundary.");
|
|
387
|
+
return Result.ok({
|
|
388
|
+
text,
|
|
389
|
+
boundaries
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
//#endregion
|
|
393
|
+
export { CANONICAL_NATIVE_INLINE_POLICIES, CANONICAL_SOURCE_INLINE_POLICIES, canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline };
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { EditorState, Transaction } from "prosemirror-state";
|
|
2
2
|
import { EditorView } from "prosemirror-view";
|
|
3
3
|
//#region src/controller/canonicalInput.d.ts
|
|
4
|
+
declare const CANONICAL_COMPOSITION_INPUT_TYPES: readonly ["insertCompositionText", "insertFromComposition", "deleteCompositionText", "deleteByComposition"];
|
|
4
5
|
type ReplaceTextInput = {
|
|
5
6
|
from: number;
|
|
6
7
|
to: number;
|
|
7
8
|
text: string;
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
} & ({
|
|
10
|
+
semantic: "composition";
|
|
11
|
+
compositionPhase?: "correction";
|
|
12
|
+
} | {
|
|
13
|
+
semantic?: "typing" | "deleteBackward" | "deleteForward";
|
|
14
|
+
compositionPhase?: never;
|
|
15
|
+
});
|
|
10
16
|
type CanonicalInputOptions = {
|
|
11
17
|
replace: (input: ReplaceTextInput) => void;
|
|
12
18
|
structure?: (type: "split" | "joinBackward" | "joinForward" | "lineBreak" | "pageBreak" | "tab" | "indent" | "outdent") => void;
|
|
@@ -14,6 +20,8 @@ type CanonicalInputOptions = {
|
|
|
14
20
|
breakUndoGroup?: () => void;
|
|
15
21
|
beginComposition?: () => boolean;
|
|
16
22
|
endComposition?: () => void;
|
|
23
|
+
pastePlainText?: (view: EditorView) => void;
|
|
24
|
+
cut?: (view: EditorView, event: ClipboardEvent) => boolean;
|
|
17
25
|
undo: () => boolean;
|
|
18
26
|
redo: () => boolean;
|
|
19
27
|
refuse: (reason: string) => void;
|
|
@@ -26,6 +34,7 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
|
|
|
26
34
|
handleTextInput: (view: EditorView, from: number, to: number, text: string) => boolean;
|
|
27
35
|
handleKeyDown: (view: EditorView, event: KeyboardEvent) => boolean;
|
|
28
36
|
handleDOMEvents: {
|
|
37
|
+
keydown: (view: EditorView, event: KeyboardEvent) => boolean;
|
|
29
38
|
compositionstart: (view: EditorView) => boolean;
|
|
30
39
|
compositionend: (view: EditorView) => boolean;
|
|
31
40
|
mousedown: (view: EditorView) => boolean;
|
|
@@ -33,7 +42,7 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
|
|
|
33
42
|
beforeinput: (view: EditorView, event: InputEvent) => boolean;
|
|
34
43
|
input: (view: EditorView) => boolean;
|
|
35
44
|
paste: (_view: EditorView, event: Event) => boolean;
|
|
36
|
-
cut: (
|
|
45
|
+
cut: (view: EditorView, event: ClipboardEvent) => boolean;
|
|
37
46
|
drop: (_view: EditorView, event: Event) => boolean;
|
|
38
47
|
};
|
|
39
48
|
/** Returns a classified intent only; the proposed PM document is never authoritative. */
|
|
@@ -42,4 +51,4 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
|
|
|
42
51
|
commitNativeProposal: (view: EditorView, transaction: Transaction) => boolean;
|
|
43
52
|
};
|
|
44
53
|
//#endregion
|
|
45
|
-
export { createCanonicalInputBoundary };
|
|
54
|
+
export { CANONICAL_COMPOSITION_INPUT_TYPES, createCanonicalInputBoundary };
|
|
@@ -4,6 +4,12 @@ import { deletionRange, isCanonicalJoinBoundary } from "./canonicalSession.js";
|
|
|
4
4
|
import { ReplaceStep } from "prosemirror-transform";
|
|
5
5
|
import { Selection, TextSelection } from "prosemirror-state";
|
|
6
6
|
//#region src/controller/canonicalInput.ts
|
|
7
|
+
const CANONICAL_COMPOSITION_INPUT_TYPES = [
|
|
8
|
+
"insertCompositionText",
|
|
9
|
+
"insertFromComposition",
|
|
10
|
+
"deleteCompositionText",
|
|
11
|
+
"deleteByComposition"
|
|
12
|
+
];
|
|
7
13
|
/** Native changes require a preceding, classified input event at the same state. */
|
|
8
14
|
const createCanonicalInputBoundary = (options) => {
|
|
9
15
|
const composition = createCanonicalComposition({
|
|
@@ -15,7 +21,8 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
15
21
|
let proposal = { type: "idle" };
|
|
16
22
|
let authorizedInput = null;
|
|
17
23
|
let groupClosed = false;
|
|
18
|
-
const beginGesture = () => {
|
|
24
|
+
const beginGesture = (lifecycle = "new") => {
|
|
25
|
+
if (lifecycle === "new") composition.forgetCompleted();
|
|
19
26
|
proposal = { type: "idle" };
|
|
20
27
|
authorizedInput = null;
|
|
21
28
|
groupClosed = false;
|
|
@@ -37,6 +44,12 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
37
44
|
if (!composition.active) proposal = { type: "refused" };
|
|
38
45
|
return true;
|
|
39
46
|
};
|
|
47
|
+
const refuseClipboardGesture = (event, reason) => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
closeGroup();
|
|
50
|
+
options.refuse(reason);
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
40
53
|
const refuseNativeMutation = (view) => {
|
|
41
54
|
if (!composition.active && proposal.type !== "refused") {
|
|
42
55
|
options.refuse("Unclassified native text is unavailable in this session.");
|
|
@@ -72,7 +85,7 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
72
85
|
},
|
|
73
86
|
acceptComposition: composition.accept,
|
|
74
87
|
handleTextInput: (view, from, to, text) => {
|
|
75
|
-
if (composition.active) return false;
|
|
88
|
+
if (composition.active || composition.pendingFinal) return false;
|
|
76
89
|
const pending = proposal;
|
|
77
90
|
if (authorizedInput?.view === view && authorizedInput.state === view.state) {
|
|
78
91
|
options.replace({
|
|
@@ -92,6 +105,7 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
92
105
|
return true;
|
|
93
106
|
},
|
|
94
107
|
handleKeyDown: (view, event) => {
|
|
108
|
+
if (event.isComposing || event.keyCode === 229) return false;
|
|
95
109
|
beginGesture();
|
|
96
110
|
if (composition.active) {
|
|
97
111
|
if (event.key === "Escape") {
|
|
@@ -138,6 +152,12 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
138
152
|
}
|
|
139
153
|
return true;
|
|
140
154
|
}
|
|
155
|
+
if (modifier && event.altKey && event.key.toLowerCase() === "v" && options.pastePlainText) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
closeGroup();
|
|
158
|
+
options.pastePlainText(view);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
141
161
|
if (event.key === "Enter") {
|
|
142
162
|
if (view.composing) return refuseEvent(event, "Paragraph structure edits are unavailable during composition.");
|
|
143
163
|
event.preventDefault();
|
|
@@ -177,6 +197,13 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
177
197
|
return true;
|
|
178
198
|
},
|
|
179
199
|
handleDOMEvents: {
|
|
200
|
+
keydown: (view, event) => {
|
|
201
|
+
if (event.isComposing || event.keyCode === 229) return true;
|
|
202
|
+
if (!composition.active || event.key !== "Escape") return false;
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
composition.cancel(view);
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
180
207
|
compositionstart: (view) => {
|
|
181
208
|
beginGesture();
|
|
182
209
|
composition.start(view);
|
|
@@ -200,19 +227,22 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
200
227
|
return false;
|
|
201
228
|
},
|
|
202
229
|
beforeinput: (view, event) => {
|
|
203
|
-
beginGesture();
|
|
204
|
-
if (event.isComposing ||
|
|
230
|
+
beginGesture(event.inputType === "insertFromComposition" ? "compositionFinal" : "new");
|
|
231
|
+
if (event.isComposing || CANONICAL_COMPOSITION_INPUT_TYPES.some((inputType) => inputType === event.inputType)) {
|
|
205
232
|
if (composition.active) {
|
|
206
233
|
composition.authorizeNative(view);
|
|
207
234
|
return false;
|
|
208
235
|
}
|
|
236
|
+
if (event.inputType === "insertFromComposition" && composition.authorizeLateFinal(view, event.data)) return false;
|
|
209
237
|
return refuseEvent(event, "Composition has no captured canonical baseline.");
|
|
210
238
|
}
|
|
211
239
|
if (composition.active) {
|
|
212
|
-
const
|
|
213
|
-
|
|
240
|
+
const nativeFinal = view.composing && (event.inputType === "insertText" || event.inputType === "insertReplacementText");
|
|
241
|
+
const nativeStillComposing = view.composing;
|
|
242
|
+
const recovered = composition.recover(view);
|
|
243
|
+
composition.forgetCompleted();
|
|
214
244
|
proposal = { type: "idle" };
|
|
215
|
-
if (
|
|
245
|
+
if (nativeFinal && (recovered.type === "refused" && nativeStillComposing || (recovered.type === "committed" || recovered.type === "refused") && recovered.text !== null && recovered.text === event.data)) {
|
|
216
246
|
event.preventDefault();
|
|
217
247
|
return true;
|
|
218
248
|
}
|
|
@@ -298,7 +328,7 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
298
328
|
return refuseEvent(event, `Input ${event.inputType} is unavailable in this session.`);
|
|
299
329
|
},
|
|
300
330
|
input: (view) => {
|
|
301
|
-
if (composition.active) {
|
|
331
|
+
if (composition.active || composition.pendingFinal) {
|
|
302
332
|
composition.flushed(view);
|
|
303
333
|
return false;
|
|
304
334
|
}
|
|
@@ -312,15 +342,24 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
312
342
|
},
|
|
313
343
|
paste: (_view, event) => {
|
|
314
344
|
beginGesture();
|
|
315
|
-
|
|
345
|
+
closeGroup();
|
|
346
|
+
if (composition.active) return refuseClipboardGesture(event, "Composition must finish before pasting.");
|
|
347
|
+
return false;
|
|
316
348
|
},
|
|
317
|
-
cut: (
|
|
349
|
+
cut: (view, event) => {
|
|
318
350
|
beginGesture();
|
|
319
|
-
return
|
|
351
|
+
if (composition.active) return refuseClipboardGesture(event, "Composition must finish before cutting.");
|
|
352
|
+
if (options.cut !== void 0) {
|
|
353
|
+
closeGroup();
|
|
354
|
+
return options.cut(view, event);
|
|
355
|
+
}
|
|
356
|
+
return refuseEvent(event, "The canonical clipboard owner is unavailable.");
|
|
320
357
|
},
|
|
321
358
|
drop: (_view, event) => {
|
|
322
359
|
beginGesture();
|
|
323
|
-
|
|
360
|
+
closeGroup();
|
|
361
|
+
if (composition.active) return refuseClipboardGesture(event, "Composition must finish before dropping.");
|
|
362
|
+
return false;
|
|
324
363
|
}
|
|
325
364
|
},
|
|
326
365
|
/** Returns a classified intent only; the proposed PM document is never authoritative. */
|
|
@@ -335,4 +374,4 @@ const createCanonicalInputBoundary = (options) => {
|
|
|
335
374
|
};
|
|
336
375
|
};
|
|
337
376
|
//#endregion
|
|
338
|
-
export { createCanonicalInputBoundary };
|
|
377
|
+
export { CANONICAL_COMPOSITION_INPUT_TYPES, createCanonicalInputBoundary };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/controller/canonicalInputTimer.d.ts
|
|
2
|
+
/** Shared by timer producers and browser diagnostics, regardless of bundling. */
|
|
3
|
+
declare const CANONICAL_INPUT_TIMER_OWNER = "folio.canonical-input-timer";
|
|
4
|
+
/** Mark the scheduled wrapper, never the caller's potentially shared callback. */
|
|
5
|
+
declare const setCanonicalInputTimer: (callback: () => void, delay: number) => NodeJS.Timeout;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { CANONICAL_INPUT_TIMER_OWNER, setCanonicalInputTimer };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/controller/canonicalInputTimer.ts
|
|
2
|
+
/** Shared by timer producers and browser diagnostics, regardless of bundling. */
|
|
3
|
+
const CANONICAL_INPUT_TIMER_OWNER = "folio.canonical-input-timer";
|
|
4
|
+
/** Mark the scheduled wrapper, never the caller's potentially shared callback. */
|
|
5
|
+
const setCanonicalInputTimer = (callback, delay) => {
|
|
6
|
+
const ownedCallback = () => callback();
|
|
7
|
+
Object.defineProperty(ownedCallback, Symbol.for(CANONICAL_INPUT_TIMER_OWNER), { value: true });
|
|
8
|
+
return setTimeout(ownedCallback, delay);
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { CANONICAL_INPUT_TIMER_OWNER, setCanonicalInputTimer };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ApplyFolioDocumentOperationsOptions, FolioDocumentOperationResult, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult } from "../document-operations.js";
|
|
2
|
+
import { CanonicalCommit, CanonicalSession } from "./canonicalSession.js";
|
|
3
|
+
import { EditorState } from "prosemirror-state";
|
|
4
|
+
import { OpStory } from "@stll/docx-core/ops";
|
|
5
|
+
//#region src/controller/canonicalPublicOperations.d.ts
|
|
6
|
+
/** A new public kind cannot bypass a canonical compiler/refusal decision. */
|
|
7
|
+
declare const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS: {
|
|
8
|
+
readonly replaceInBlock: "compile";
|
|
9
|
+
readonly replaceRange: "compile";
|
|
10
|
+
readonly replaceBlock: "compile";
|
|
11
|
+
readonly splitBlock: "compile";
|
|
12
|
+
readonly formatRange: "compile";
|
|
13
|
+
readonly mergeBlockWithNext: "compile";
|
|
14
|
+
readonly setBlockParagraphProperties: "publicOps.unsupportedInline";
|
|
15
|
+
readonly insertAfterBlock: "publicOps.unsupportedInline";
|
|
16
|
+
readonly insertBeforeBlock: "publicOps.unsupportedInline";
|
|
17
|
+
readonly deleteBlock: "publicOps.unsupportedInline";
|
|
18
|
+
readonly commentOnBlock: "publicOps.comments";
|
|
19
|
+
readonly commentOnRange: "publicOps.comments";
|
|
20
|
+
readonly insertTable: "publicOps.tableProjection";
|
|
21
|
+
readonly insertSignatureTable: "publicOps.tableProjection";
|
|
22
|
+
readonly deleteTable: "publicOps.tableProjection";
|
|
23
|
+
readonly insertTableRow: "publicOps.tableProjection";
|
|
24
|
+
readonly deleteTableRow: "publicOps.tableProjection";
|
|
25
|
+
readonly insertTableColumn: "publicOps.tableProjection";
|
|
26
|
+
readonly deleteTableColumn: "publicOps.tableProjection";
|
|
27
|
+
readonly mergeTableCells: "publicOps.tableProjection";
|
|
28
|
+
readonly splitTableCell: "publicOps.tableProjection";
|
|
29
|
+
};
|
|
30
|
+
type CanonicalPublicOperationOptions = Omit<ApplyFolioDocumentOperationsOptions, "view" | "createUndoHandle" | "createCommentId">;
|
|
31
|
+
type CanonicalPublicOperationsOptions = {
|
|
32
|
+
session: CanonicalSession;
|
|
33
|
+
getState: (story: OpStory) => EditorState | null;
|
|
34
|
+
publish: (commit: CanonicalCommit) => boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Handles address isolated journal groups; no second document or PM history is retained. */
|
|
37
|
+
declare class CanonicalPublicOperations {
|
|
38
|
+
private readonly options;
|
|
39
|
+
private readonly handles;
|
|
40
|
+
private nextHandle;
|
|
41
|
+
private readonly handleNamespace;
|
|
42
|
+
constructor(options: CanonicalPublicOperationsOptions);
|
|
43
|
+
apply(options: CanonicalPublicOperationOptions): FolioDocumentOperationResult;
|
|
44
|
+
undo(handle: FolioDocumentOperationUndoHandle): FolioDocumentOperationUndoResult;
|
|
45
|
+
private resolve;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { CANONICAL_PUBLIC_OPERATION_DISPOSITIONS, CanonicalPublicOperationOptions, CanonicalPublicOperations };
|