wordinweb 0.2.5 → 0.3.1
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/README.md +5 -1
- package/dist/{chunk-ALLQ5KWP.js → chunk-FFQBNFND.js} +24797 -7481
- package/dist/chunk-FFQBNFND.js.map +1 -0
- package/dist/collab.d.ts +109 -4
- package/dist/collab.js +572 -160
- package/dist/collab.js.map +1 -1
- package/dist/{index-CGp6gVjn.d.ts → index-C_oyKJu0.d.ts} +2529 -281
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -3
- package/package.json +1 -1
- package/dist/chunk-ALLQ5KWP.js.map +0 -1
package/dist/collab.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ADDRESS_WIRE_FIELD,
|
|
2
3
|
DocxDocument,
|
|
3
4
|
DocxToolbar,
|
|
4
5
|
DocxView,
|
|
6
|
+
REGISTERED_OPERATION_KINDS,
|
|
5
7
|
acceptAllRevisions,
|
|
6
8
|
acceptRevision,
|
|
7
9
|
addComment,
|
|
8
10
|
adjustIndent,
|
|
9
11
|
applyDeleteRange,
|
|
12
|
+
applyDeleteSeparator,
|
|
13
|
+
applyInsertSeparator,
|
|
10
14
|
applyInsertText,
|
|
15
|
+
applyRegisteredOperation,
|
|
11
16
|
applyRunFormat,
|
|
12
17
|
applySplitParagraph,
|
|
13
18
|
applyTableOp,
|
|
14
|
-
checkboxStateElement,
|
|
15
19
|
collectRevisions,
|
|
16
20
|
deleteComment,
|
|
17
21
|
deleteMath,
|
|
18
22
|
deleteSuggestedRange,
|
|
23
|
+
editCommentText,
|
|
19
24
|
insertBlankPageAt,
|
|
20
25
|
insertBookmarkAroundSelection,
|
|
21
26
|
insertBookmarkAt,
|
|
@@ -32,9 +37,11 @@ import {
|
|
|
32
37
|
insertSectionBreak,
|
|
33
38
|
insertShapeAt,
|
|
34
39
|
insertSmartArtAt,
|
|
35
|
-
insertTableAfter,
|
|
36
40
|
insertWordArtAt,
|
|
41
|
+
isInsertableFieldInstruction,
|
|
42
|
+
isRegisteredOperationKind,
|
|
37
43
|
isSafeUrl,
|
|
44
|
+
isValidShapePreset,
|
|
38
45
|
localName,
|
|
39
46
|
markParagraphGlyph,
|
|
40
47
|
mergeParagraphBackward,
|
|
@@ -42,6 +49,7 @@ import {
|
|
|
42
49
|
moveTableTo,
|
|
43
50
|
parseXml,
|
|
44
51
|
recordedProvenance,
|
|
52
|
+
registeredOperation,
|
|
45
53
|
rejectAllRevisions,
|
|
46
54
|
rejectRevision,
|
|
47
55
|
removeDrawingRun,
|
|
@@ -49,15 +57,17 @@ import {
|
|
|
49
57
|
replyToComment,
|
|
50
58
|
resizeDrawing,
|
|
51
59
|
resizeTableColumn,
|
|
52
|
-
resizeTableRow,
|
|
53
60
|
resolveRunOffset,
|
|
54
61
|
runWireLength,
|
|
62
|
+
separatorAtWireOffset,
|
|
55
63
|
serializeXml,
|
|
56
64
|
setChartData,
|
|
65
|
+
setCommentResolved,
|
|
57
66
|
setDrawingFill,
|
|
58
67
|
setDrawingLineStyle,
|
|
59
68
|
setDrawingOrder,
|
|
60
69
|
setDrawingRotation,
|
|
70
|
+
setDrawingWordArtStyle,
|
|
61
71
|
setDrawingWordArtText,
|
|
62
72
|
setDropCapAt,
|
|
63
73
|
setFloatingPagePosition,
|
|
@@ -66,7 +76,6 @@ import {
|
|
|
66
76
|
setLineNumbering,
|
|
67
77
|
setLink,
|
|
68
78
|
setListLevel,
|
|
69
|
-
setListType,
|
|
70
79
|
setMathLinear,
|
|
71
80
|
setPageLayout,
|
|
72
81
|
setParagraphAlignment,
|
|
@@ -77,24 +86,109 @@ import {
|
|
|
77
86
|
setSmartArtFill,
|
|
78
87
|
setSmartArtNodeText,
|
|
79
88
|
setSmartArtTextFormat,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
siblingParagraph,
|
|
90
|
+
suggestMeta,
|
|
91
|
+
validatePastedOoxml,
|
|
92
|
+
validateRegisteredOperation
|
|
93
|
+
} from "./chunk-FFQBNFND.js";
|
|
83
94
|
import "./chunk-7D4SUZUM.js";
|
|
84
95
|
|
|
85
96
|
// src/collab.tsx
|
|
86
97
|
import { useEffect, useMemo, useRef, useState, createElement } from "react";
|
|
87
98
|
|
|
88
99
|
// ../collab/src/intents.ts
|
|
100
|
+
function isRegisteredIntent(intent) {
|
|
101
|
+
return isRegisteredOperationKind(intent.kind);
|
|
102
|
+
}
|
|
103
|
+
var HAND_WRITTEN_INTENT_KIND_MAP = {
|
|
104
|
+
insertText: true,
|
|
105
|
+
insertSeparator: true,
|
|
106
|
+
deleteSeparator: true,
|
|
107
|
+
deleteText: true,
|
|
108
|
+
splitParagraph: true,
|
|
109
|
+
formatRun: true,
|
|
110
|
+
formatParagraph: true,
|
|
111
|
+
formatRange: true,
|
|
112
|
+
tableOp: true,
|
|
113
|
+
mergeParagraph: true,
|
|
114
|
+
commentRun: true,
|
|
115
|
+
pasteBlocks: true,
|
|
116
|
+
insertImage: true,
|
|
117
|
+
suggestRevision: true,
|
|
118
|
+
insertBreak: true,
|
|
119
|
+
insertMath: true,
|
|
120
|
+
insertShape: true,
|
|
121
|
+
replyComment: true,
|
|
122
|
+
adjustIndent: true,
|
|
123
|
+
setSpacing: true,
|
|
124
|
+
insertPageField: true,
|
|
125
|
+
setLink: true,
|
|
126
|
+
insertFootnote: true,
|
|
127
|
+
setDropCap: true,
|
|
128
|
+
setDivider: true,
|
|
129
|
+
insertBookmark: true,
|
|
130
|
+
insertBlankPage: true,
|
|
131
|
+
insertSectionBreak: true,
|
|
132
|
+
insertCrossRef: true,
|
|
133
|
+
insertCoverPage: true,
|
|
134
|
+
setPageLayout: true,
|
|
135
|
+
setListLevel: true,
|
|
136
|
+
insertWordArt: true,
|
|
137
|
+
insertChart: true,
|
|
138
|
+
insertSmartArt: true,
|
|
139
|
+
setLineNumbering: true,
|
|
140
|
+
insertDateTimeField: true,
|
|
141
|
+
insertField: true,
|
|
142
|
+
setDrawingRotation: true,
|
|
143
|
+
setDrawingFill: true,
|
|
144
|
+
setChartData: true,
|
|
145
|
+
setSmartArtNodeText: true,
|
|
146
|
+
setDrawingWordArtText: true,
|
|
147
|
+
setDrawingWordArtStyle: true,
|
|
148
|
+
setDrawingLineStyle: true,
|
|
149
|
+
setImageAltText: true,
|
|
150
|
+
removeLink: true,
|
|
151
|
+
setImageWrap: true,
|
|
152
|
+
setDrawingOrder: true,
|
|
153
|
+
setSmartArtData: true,
|
|
154
|
+
setSmartArtFill: true,
|
|
155
|
+
setSmartArtTextFormat: true,
|
|
156
|
+
setFloatingPagePosition: true,
|
|
157
|
+
resizeDrawing: true,
|
|
158
|
+
resizeTableColumn: true,
|
|
159
|
+
moveTable: true,
|
|
160
|
+
removeDrawing: true,
|
|
161
|
+
setMathLinear: true,
|
|
162
|
+
deleteMath: true,
|
|
163
|
+
moveMath: true,
|
|
164
|
+
ensureHeaderFooter: true,
|
|
165
|
+
deleteComment: true,
|
|
166
|
+
resolveComment: true,
|
|
167
|
+
editComment: true,
|
|
168
|
+
insertBookmarkRange: true,
|
|
169
|
+
acceptRevision: true,
|
|
170
|
+
rejectRevision: true,
|
|
171
|
+
acceptAllRevisions: true,
|
|
172
|
+
rejectAllRevisions: true
|
|
173
|
+
};
|
|
174
|
+
var INTENT_KINDS = Object.freeze([
|
|
175
|
+
...Object.keys(HAND_WRITTEN_INTENT_KIND_MAP),
|
|
176
|
+
...REGISTERED_OPERATION_KINDS
|
|
177
|
+
]);
|
|
89
178
|
function idempotencyKey(i) {
|
|
90
179
|
return `${i.clientId}:${i.clientSeq}`;
|
|
91
180
|
}
|
|
92
181
|
|
|
93
182
|
// ../collab/src/transform.ts
|
|
94
183
|
function runEditsOf(intent) {
|
|
184
|
+
if (isRegisteredIntent(intent)) return [];
|
|
95
185
|
switch (intent.kind) {
|
|
96
186
|
case "insertText":
|
|
97
187
|
return [{ runId: intent.at.runId, at: intent.at.offset, del: 0, ins: intent.text.length }];
|
|
188
|
+
case "insertSeparator":
|
|
189
|
+
return [{ runId: intent.at.runId, at: intent.at.offset, del: 0, ins: 1 }];
|
|
190
|
+
case "deleteSeparator":
|
|
191
|
+
return intent.suggest ? [] : [{ runId: intent.at.runId, at: intent.at.offset, del: 1, ins: 0 }];
|
|
98
192
|
case "deleteText":
|
|
99
193
|
return [{ runId: intent.runId, at: intent.start, del: intent.end - intent.start, ins: 0 }];
|
|
100
194
|
case "splitParagraph":
|
|
@@ -103,8 +197,6 @@ function runEditsOf(intent) {
|
|
|
103
197
|
return [];
|
|
104
198
|
case "formatParagraph":
|
|
105
199
|
return [];
|
|
106
|
-
case "setListType":
|
|
107
|
-
return [];
|
|
108
200
|
case "formatRange":
|
|
109
201
|
return [{
|
|
110
202
|
runId: intent.runId,
|
|
@@ -159,6 +251,7 @@ function runEditsOf(intent) {
|
|
|
159
251
|
case "setChartData":
|
|
160
252
|
case "setSmartArtNodeText":
|
|
161
253
|
case "setDrawingWordArtText":
|
|
254
|
+
case "setDrawingWordArtStyle":
|
|
162
255
|
case "setDrawingLineStyle":
|
|
163
256
|
case "setImageAltText":
|
|
164
257
|
case "removeLink":
|
|
@@ -170,19 +263,18 @@ function runEditsOf(intent) {
|
|
|
170
263
|
case "setFloatingPagePosition":
|
|
171
264
|
case "resizeDrawing":
|
|
172
265
|
case "resizeTableColumn":
|
|
173
|
-
case "resizeTableRow":
|
|
174
266
|
case "moveTable":
|
|
175
267
|
case "removeDrawing":
|
|
176
268
|
case "setMathLinear":
|
|
177
269
|
case "deleteMath":
|
|
178
270
|
case "ensureHeaderFooter":
|
|
179
271
|
case "deleteComment":
|
|
272
|
+
case "resolveComment":
|
|
273
|
+
case "editComment":
|
|
180
274
|
case "insertBookmarkRange":
|
|
181
|
-
case "toggleCheckbox":
|
|
182
275
|
return [];
|
|
183
276
|
case "moveMath":
|
|
184
277
|
return [];
|
|
185
|
-
case "insertTable":
|
|
186
278
|
case "acceptRevision":
|
|
187
279
|
case "rejectRevision":
|
|
188
280
|
case "acceptAllRevisions":
|
|
@@ -234,17 +326,20 @@ function transformPosition(pos, ahead, leftGravity = false) {
|
|
|
234
326
|
}
|
|
235
327
|
function transformIntent(intent, ahead) {
|
|
236
328
|
const newBase = intent.base + ahead.length;
|
|
329
|
+
if (isRegisteredIntent(intent)) return { ...intent, base: newBase };
|
|
237
330
|
switch (intent.kind) {
|
|
238
331
|
case "insertText":
|
|
239
332
|
return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
|
|
333
|
+
case "insertSeparator":
|
|
334
|
+
return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
|
|
335
|
+
case "deleteSeparator":
|
|
336
|
+
return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
|
|
240
337
|
case "splitParagraph":
|
|
241
338
|
return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
|
|
242
339
|
case "formatRun":
|
|
243
340
|
return { ...intent, base: newBase };
|
|
244
341
|
case "formatParagraph":
|
|
245
342
|
return { ...intent, base: newBase };
|
|
246
|
-
case "setListType":
|
|
247
|
-
return { ...intent, base: newBase };
|
|
248
343
|
case "formatRange": {
|
|
249
344
|
const s = transformPosition({ blockId: intent.blockId, runId: intent.runId, offset: intent.start }, ahead);
|
|
250
345
|
const e = transformPosition({ blockId: intent.blockId, runId: intent.runId, offset: intent.end }, ahead);
|
|
@@ -305,6 +400,7 @@ function transformIntent(intent, ahead) {
|
|
|
305
400
|
case "setChartData":
|
|
306
401
|
case "setSmartArtNodeText":
|
|
307
402
|
case "setDrawingWordArtText":
|
|
403
|
+
case "setDrawingWordArtStyle":
|
|
308
404
|
case "setDrawingLineStyle":
|
|
309
405
|
case "setImageAltText":
|
|
310
406
|
case "removeLink":
|
|
@@ -316,16 +412,15 @@ function transformIntent(intent, ahead) {
|
|
|
316
412
|
case "setFloatingPagePosition":
|
|
317
413
|
case "resizeDrawing":
|
|
318
414
|
case "resizeTableColumn":
|
|
319
|
-
case "resizeTableRow":
|
|
320
415
|
case "moveTable":
|
|
321
416
|
case "removeDrawing":
|
|
322
417
|
case "setMathLinear":
|
|
323
418
|
case "deleteMath":
|
|
324
419
|
case "ensureHeaderFooter":
|
|
325
420
|
case "deleteComment":
|
|
421
|
+
case "resolveComment":
|
|
422
|
+
case "editComment":
|
|
326
423
|
case "insertBookmarkRange":
|
|
327
|
-
case "toggleCheckbox":
|
|
328
|
-
case "insertTable":
|
|
329
424
|
case "acceptRevision":
|
|
330
425
|
case "rejectRevision":
|
|
331
426
|
case "acceptAllRevisions":
|
|
@@ -387,6 +482,7 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
387
482
|
const ctx = { suggesting: false, revMeta: () => {
|
|
388
483
|
throw new Error("suggesting mode unsupported headlessly");
|
|
389
484
|
} };
|
|
485
|
+
if (isRegisteredIntent(intent)) return applyRegistered(doc, ids, runOf, intent);
|
|
390
486
|
switch (intent.kind) {
|
|
391
487
|
case "insertText": {
|
|
392
488
|
const caret = resolveCaret(ids, runOf, intent.at);
|
|
@@ -404,6 +500,43 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
404
500
|
out.scope = blockScope(ids, intent.at.blockId, intent.at.runId);
|
|
405
501
|
return true;
|
|
406
502
|
}
|
|
503
|
+
case "insertSeparator": {
|
|
504
|
+
const caret = resolveCaret(ids, runOf, intent.at);
|
|
505
|
+
if (!caret) return false;
|
|
506
|
+
if (intent.suggest) {
|
|
507
|
+
const s = intent.suggest;
|
|
508
|
+
const suggestCtx = { suggesting: true, revMeta: () => ({ author: s.author, date: s.date, nextId: () => doc.nextRevisionId() }) };
|
|
509
|
+
const scope = blockScope(ids, intent.at.blockId, intent.at.runId);
|
|
510
|
+
if (!applyInsertSeparator(doc, caret, intent.separator, suggestCtx)) return false;
|
|
511
|
+
if (doc.stableIds) resyncScope(doc, ids, scope);
|
|
512
|
+
out.scope = scope;
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
if (!applyInsertSeparator(doc, caret, intent.separator, ctx)) return false;
|
|
516
|
+
out.scope = blockScope(ids, intent.at.blockId, intent.at.runId);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
case "deleteSeparator": {
|
|
520
|
+
const runEl = ids.elOf(intent.at.runId);
|
|
521
|
+
if (!runEl) return false;
|
|
522
|
+
const sepEl = separatorAtWireOffset(runEl, intent.at.offset);
|
|
523
|
+
if (!sepEl) return false;
|
|
524
|
+
if (intent.suggest) {
|
|
525
|
+
const s = intent.suggest;
|
|
526
|
+
const suggestCtx = { suggesting: true, revMeta: () => ({ author: s.author, date: s.date, nextId: () => doc.nextRevisionId() }) };
|
|
527
|
+
const scope2 = blockScope(ids, intent.at.blockId, intent.at.runId);
|
|
528
|
+
if (!applyDeleteSeparator(doc, sepEl, suggestCtx)) return false;
|
|
529
|
+
if (doc.stableIds) resyncScope(doc, ids, scope2);
|
|
530
|
+
out.scope = scope2;
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
const scope = blockScope(ids, intent.at.blockId, intent.at.runId);
|
|
534
|
+
const res = applyDeleteSeparator(doc, sepEl, ctx);
|
|
535
|
+
if (!res) return false;
|
|
536
|
+
if (res.removedRun) ids.prune(doc.editableRoots());
|
|
537
|
+
out.scope = scope;
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
407
540
|
case "deleteText": {
|
|
408
541
|
if (intent.end <= intent.start) return false;
|
|
409
542
|
const caret = resolveCaret(ids, runOf, { blockId: intent.blockId, runId: intent.runId, offset: intent.start });
|
|
@@ -439,24 +572,19 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
439
572
|
const entry = runOf(runEl);
|
|
440
573
|
if (!entry) return false;
|
|
441
574
|
const seg = { run: entry.run, t: null, start: 0, end: 0, props: entry.run.props };
|
|
442
|
-
applyRunFormat(doc, [seg], intent.patch);
|
|
575
|
+
applyRunFormat(doc, [seg], intent.patch, suggestMeta(doc, intent.suggest));
|
|
443
576
|
return true;
|
|
444
577
|
}
|
|
445
578
|
case "formatParagraph": {
|
|
446
579
|
const blockEl = ids.elOf(intent.blockId);
|
|
447
580
|
if (!blockEl) return false;
|
|
448
581
|
const target = firstTextDescendant(blockEl) ?? blockEl;
|
|
582
|
+
const meta = suggestMeta(doc, intent.suggest);
|
|
449
583
|
let changed = false;
|
|
450
|
-
if (intent.align) changed = setParagraphAlignment(doc, [target], intent.align) || changed;
|
|
451
|
-
if (intent.styleId !== void 0) changed = setParagraphStyle(doc, [target], intent.styleId) || changed;
|
|
584
|
+
if (intent.align) changed = setParagraphAlignment(doc, [target], intent.align, meta) || changed;
|
|
585
|
+
if (intent.styleId !== void 0) changed = setParagraphStyle(doc, [target], intent.styleId, meta) || changed;
|
|
452
586
|
return changed;
|
|
453
587
|
}
|
|
454
|
-
case "setListType": {
|
|
455
|
-
const blockEl = ids.elOf(intent.blockId);
|
|
456
|
-
if (!blockEl) return false;
|
|
457
|
-
const target = firstTextDescendant(blockEl) ?? blockEl;
|
|
458
|
-
return setListType(doc, [target], intent.listKind);
|
|
459
|
-
}
|
|
460
588
|
case "formatRange": {
|
|
461
589
|
if (intent.end <= intent.start) return false;
|
|
462
590
|
const runEl = ids.elOf(intent.runId);
|
|
@@ -470,7 +598,7 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
470
598
|
const localEnd = hit.offset + (intent.end - intent.start);
|
|
471
599
|
if (localEnd > hit.t.text.length) return false;
|
|
472
600
|
const seg = { run: entry.run, t: hit.t, start: hit.offset, end: localEnd, props: entry.run.props };
|
|
473
|
-
const formatted = applyRunFormat(doc, [seg], intent.patch);
|
|
601
|
+
const formatted = applyRunFormat(doc, [seg], intent.patch, suggestMeta(doc, intent.suggest));
|
|
474
602
|
if (formatted.length === 0) return false;
|
|
475
603
|
const middleT = formatted[0].t;
|
|
476
604
|
const middleRun = doc.findParentOf(middleT);
|
|
@@ -494,10 +622,11 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
494
622
|
if (!paraEl) return false;
|
|
495
623
|
const target = firstTextDescendant(paraEl) ?? paraEl;
|
|
496
624
|
const isInsert = intent.op === "rowAbove" || intent.op === "rowBelow" || intent.op === "colLeft" || intent.op === "colRight";
|
|
497
|
-
const
|
|
498
|
-
const
|
|
625
|
+
const createsNodes = isInsert || intent.op === "mergeDown" || intent.op === "splitCell";
|
|
626
|
+
const before = createsNodes ? trackedSet(ids, doc) : null;
|
|
627
|
+
const ok = applyTableOp(doc, target, intent.op, suggestMeta(doc, intent.suggest));
|
|
499
628
|
if (!ok) return false;
|
|
500
|
-
if (
|
|
629
|
+
if (before && intent.nodeIds) {
|
|
501
630
|
const fresh = [];
|
|
502
631
|
walkTracked(doc, (el) => {
|
|
503
632
|
if (!before.has(el)) fresh.push(el);
|
|
@@ -505,7 +634,8 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
505
634
|
for (let k = 0; k < fresh.length && k < intent.nodeIds.length; k++) {
|
|
506
635
|
ids.reassign(fresh[k], intent.nodeIds[k]);
|
|
507
636
|
}
|
|
508
|
-
}
|
|
637
|
+
}
|
|
638
|
+
if (!isInsert) {
|
|
509
639
|
ids.prune(doc.editableRoots());
|
|
510
640
|
}
|
|
511
641
|
return true;
|
|
@@ -513,6 +643,14 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
513
643
|
case "mergeParagraph": {
|
|
514
644
|
const pEl = ids.elOf(intent.blockId);
|
|
515
645
|
if (!pEl) return false;
|
|
646
|
+
const meta = suggestMeta(doc, intent.suggest);
|
|
647
|
+
if (meta) {
|
|
648
|
+
const previous = siblingParagraph(doc, pEl, -1);
|
|
649
|
+
if (!previous) return false;
|
|
650
|
+
markParagraphGlyph(previous, "del", meta);
|
|
651
|
+
doc.refresh();
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
516
654
|
const ok = mergeParagraphBackward(doc, pEl);
|
|
517
655
|
if (ok) ids.prune(doc.editableRoots());
|
|
518
656
|
return ok;
|
|
@@ -599,7 +737,7 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
599
737
|
if (!runEl) return false;
|
|
600
738
|
const entry = runOf(runEl);
|
|
601
739
|
if (!entry || !entry.firstT) return false;
|
|
602
|
-
const relId = doc.registerPendingImage(intent.blobSha, intent.ext, { iv: intent.iv });
|
|
740
|
+
const relId = doc.registerPendingImage(intent.blobSha, intent.ext, { iv: intent.iv }, runEl);
|
|
603
741
|
const before = trackedSet(ids, doc);
|
|
604
742
|
const newRun = insertImageAt(doc, entry.firstT, relId, intent.widthPx, intent.heightPx);
|
|
605
743
|
if (!newRun) return false;
|
|
@@ -663,7 +801,7 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
663
801
|
const entry = runOf(runEl);
|
|
664
802
|
if (!entry || !entry.firstT) return false;
|
|
665
803
|
const before = trackedSet(ids, doc);
|
|
666
|
-
const drawing = insertWordArtAt(doc, entry.firstT, intent.text, intent.preset);
|
|
804
|
+
const drawing = insertWordArtAt(doc, entry.firstT, intent.text, intent.preset, intent.style);
|
|
667
805
|
if (!drawing) return false;
|
|
668
806
|
assignFreshTracked(ids, doc, before, intent.nodeIds);
|
|
669
807
|
return true;
|
|
@@ -715,49 +853,56 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
715
853
|
case "setDrawingRotation": {
|
|
716
854
|
const runEl = ids.elOf(intent.runId);
|
|
717
855
|
if (!runEl) return false;
|
|
718
|
-
const drawing =
|
|
856
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
719
857
|
if (!drawing) return false;
|
|
720
858
|
return setDrawingRotation(doc, drawing, intent.degrees);
|
|
721
859
|
}
|
|
722
860
|
case "setDrawingFill": {
|
|
723
861
|
const runEl = ids.elOf(intent.runId);
|
|
724
862
|
if (!runEl) return false;
|
|
725
|
-
const drawing =
|
|
863
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
726
864
|
if (!drawing) return false;
|
|
727
865
|
return setDrawingFill(doc, drawing, intent.color);
|
|
728
866
|
}
|
|
729
867
|
case "setChartData": {
|
|
730
868
|
const runEl = ids.elOf(intent.runId);
|
|
731
869
|
if (!runEl) return false;
|
|
732
|
-
const drawing =
|
|
870
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
733
871
|
if (!drawing) return false;
|
|
734
872
|
return setChartData(doc, drawing, intent.chart);
|
|
735
873
|
}
|
|
736
874
|
case "setSmartArtNodeText": {
|
|
737
875
|
const runEl = ids.elOf(intent.runId);
|
|
738
876
|
if (!runEl) return false;
|
|
739
|
-
const drawing =
|
|
877
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
740
878
|
if (!drawing) return false;
|
|
741
879
|
return setSmartArtNodeText(doc, drawing, intent.index, intent.text);
|
|
742
880
|
}
|
|
743
881
|
case "setDrawingWordArtText": {
|
|
744
882
|
const runEl = ids.elOf(intent.runId);
|
|
745
883
|
if (!runEl) return false;
|
|
746
|
-
const drawing =
|
|
884
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
747
885
|
if (!drawing) return false;
|
|
748
886
|
return setDrawingWordArtText(doc, drawing, intent.text);
|
|
749
887
|
}
|
|
888
|
+
case "setDrawingWordArtStyle": {
|
|
889
|
+
const runEl = ids.elOf(intent.runId);
|
|
890
|
+
if (!runEl) return false;
|
|
891
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
892
|
+
if (!drawing) return false;
|
|
893
|
+
return setDrawingWordArtStyle(doc, drawing, intent.color, intent.opacity);
|
|
894
|
+
}
|
|
750
895
|
case "setDrawingLineStyle": {
|
|
751
896
|
const runEl = ids.elOf(intent.runId);
|
|
752
897
|
if (!runEl) return false;
|
|
753
|
-
const drawing =
|
|
898
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
754
899
|
if (!drawing) return false;
|
|
755
900
|
return setDrawingLineStyle(doc, drawing, intent.color, intent.widthPx, intent.dash);
|
|
756
901
|
}
|
|
757
902
|
case "setImageAltText": {
|
|
758
903
|
const runEl = ids.elOf(intent.runId);
|
|
759
904
|
if (!runEl) return false;
|
|
760
|
-
const drawing =
|
|
905
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
761
906
|
if (!drawing) return false;
|
|
762
907
|
return setImageAltText(doc, drawing, intent.alt);
|
|
763
908
|
}
|
|
@@ -771,49 +916,49 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
771
916
|
case "setImageWrap": {
|
|
772
917
|
const runEl = ids.elOf(intent.runId);
|
|
773
918
|
if (!runEl) return false;
|
|
774
|
-
const drawing =
|
|
919
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
775
920
|
if (!drawing) return false;
|
|
776
921
|
return setImageWrap(doc, drawing, intent.mode);
|
|
777
922
|
}
|
|
778
923
|
case "setDrawingOrder": {
|
|
779
924
|
const runEl = ids.elOf(intent.runId);
|
|
780
925
|
if (!runEl) return false;
|
|
781
|
-
const drawing =
|
|
926
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
782
927
|
if (!drawing) return false;
|
|
783
928
|
return setDrawingOrder(doc, drawing, intent.order);
|
|
784
929
|
}
|
|
785
930
|
case "setSmartArtData": {
|
|
786
931
|
const runEl = ids.elOf(intent.runId);
|
|
787
932
|
if (!runEl) return false;
|
|
788
|
-
const drawing =
|
|
933
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
789
934
|
if (!drawing) return false;
|
|
790
935
|
return setSmartArtData(doc, drawing, intent.smartArt);
|
|
791
936
|
}
|
|
792
937
|
case "setSmartArtFill": {
|
|
793
938
|
const runEl = ids.elOf(intent.runId);
|
|
794
939
|
if (!runEl) return false;
|
|
795
|
-
const drawing =
|
|
940
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
796
941
|
if (!drawing) return false;
|
|
797
942
|
return setSmartArtFill(doc, drawing, intent.color, intent.nodeIndex);
|
|
798
943
|
}
|
|
799
944
|
case "setSmartArtTextFormat": {
|
|
800
945
|
const runEl = ids.elOf(intent.runId);
|
|
801
946
|
if (!runEl) return false;
|
|
802
|
-
const drawing =
|
|
947
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
803
948
|
if (!drawing) return false;
|
|
804
949
|
return setSmartArtTextFormat(doc, drawing, intent.format, intent.nodeIndex);
|
|
805
950
|
}
|
|
806
951
|
case "setFloatingPagePosition": {
|
|
807
952
|
const runEl = ids.elOf(intent.runId);
|
|
808
953
|
if (!runEl) return false;
|
|
809
|
-
const drawing =
|
|
954
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
810
955
|
if (!drawing) return false;
|
|
811
956
|
return setFloatingPagePosition(doc, drawing, intent.xPx, intent.yPx);
|
|
812
957
|
}
|
|
813
958
|
case "resizeDrawing": {
|
|
814
959
|
const runEl = ids.elOf(intent.runId);
|
|
815
960
|
if (!runEl) return false;
|
|
816
|
-
const drawing =
|
|
961
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
817
962
|
if (!drawing) return false;
|
|
818
963
|
return resizeDrawing(doc, drawing, intent.widthPx, intent.heightPx);
|
|
819
964
|
}
|
|
@@ -822,11 +967,6 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
822
967
|
if (!tbl) return false;
|
|
823
968
|
return resizeTableColumn(doc, tbl, intent.boundary, intent.deltaPx, intent.renderedWidths);
|
|
824
969
|
}
|
|
825
|
-
case "resizeTableRow": {
|
|
826
|
-
const tbl = tableOfParagraph(doc, ids, intent.cellParagraphId);
|
|
827
|
-
if (!tbl) return false;
|
|
828
|
-
return resizeTableRow(doc, tbl, intent.rowIdx, intent.heightPx);
|
|
829
|
-
}
|
|
830
970
|
case "moveTable": {
|
|
831
971
|
const tbl = tableOfParagraph(doc, ids, intent.cellParagraphId);
|
|
832
972
|
if (!tbl) return false;
|
|
@@ -835,7 +975,7 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
835
975
|
case "removeDrawing": {
|
|
836
976
|
const runEl = ids.elOf(intent.runId);
|
|
837
977
|
if (!runEl) return false;
|
|
838
|
-
const drawing =
|
|
978
|
+
const drawing = drawingIn(runEl, intent.objectIndex, runOf);
|
|
839
979
|
if (!drawing) return false;
|
|
840
980
|
const ok = removeDrawingRun(doc, drawing);
|
|
841
981
|
if (ok) ids.prune(doc.editableRoots());
|
|
@@ -844,21 +984,21 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
844
984
|
case "setMathLinear": {
|
|
845
985
|
const blockEl = ids.elOf(intent.blockId);
|
|
846
986
|
if (!blockEl) return false;
|
|
847
|
-
const math =
|
|
987
|
+
const math = mathIn(blockEl, intent.mathIndex);
|
|
848
988
|
if (!math) return false;
|
|
849
989
|
return setMathLinear(doc, math, intent.mathText);
|
|
850
990
|
}
|
|
851
991
|
case "deleteMath": {
|
|
852
992
|
const blockEl = ids.elOf(intent.blockId);
|
|
853
993
|
if (!blockEl) return false;
|
|
854
|
-
const math =
|
|
994
|
+
const math = mathIn(blockEl, intent.mathIndex);
|
|
855
995
|
if (!math) return false;
|
|
856
996
|
return deleteMath(doc, math);
|
|
857
997
|
}
|
|
858
998
|
case "moveMath": {
|
|
859
999
|
const blockEl = ids.elOf(intent.blockId);
|
|
860
1000
|
if (!blockEl) return false;
|
|
861
|
-
const math =
|
|
1001
|
+
const math = mathIn(blockEl, intent.mathIndex);
|
|
862
1002
|
if (!math) return false;
|
|
863
1003
|
const dest = resolveCaret(ids, runOf, intent.at);
|
|
864
1004
|
if (!dest) return false;
|
|
@@ -884,6 +1024,10 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
884
1024
|
}
|
|
885
1025
|
case "deleteComment":
|
|
886
1026
|
return deleteComment(doc, intent.commentId);
|
|
1027
|
+
case "resolveComment":
|
|
1028
|
+
return setCommentResolved(doc, intent.commentId, intent.resolved, recordedProvenance({ paraIds: [intent.paraId] }));
|
|
1029
|
+
case "editComment":
|
|
1030
|
+
return editCommentText(doc, intent.commentId, intent.text);
|
|
887
1031
|
case "insertBookmarkRange": {
|
|
888
1032
|
const runEl = ids.elOf(intent.runId);
|
|
889
1033
|
if (!runEl) return false;
|
|
@@ -897,16 +1041,6 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
897
1041
|
const seg = { run: entry.run, t: hit.t, start: hit.offset, end: localEnd, props: entry.run.props };
|
|
898
1042
|
return insertBookmarkAroundSelection(doc, [seg], intent.name);
|
|
899
1043
|
}
|
|
900
|
-
case "toggleCheckbox": {
|
|
901
|
-
const runEl = ids.elOf(intent.runId);
|
|
902
|
-
if (!runEl) return false;
|
|
903
|
-
const entry = runOf(runEl);
|
|
904
|
-
if (!entry) return false;
|
|
905
|
-
const cbEl = checkboxStateElement(entry.run, entry.firstT);
|
|
906
|
-
if (!cbEl) return false;
|
|
907
|
-
toggleCheckbox(doc, cbEl);
|
|
908
|
-
return true;
|
|
909
|
-
}
|
|
910
1044
|
case "acceptRevision": {
|
|
911
1045
|
const refs = collectRevisions(doc);
|
|
912
1046
|
if (intent.index >= refs.length) return false;
|
|
@@ -918,19 +1052,9 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
918
1052
|
return rejectRevision(doc, refs[intent.index]);
|
|
919
1053
|
}
|
|
920
1054
|
case "acceptAllRevisions":
|
|
921
|
-
return acceptAllRevisions(doc) > 0;
|
|
1055
|
+
return acceptAllRevisions(doc, intent.author) > 0;
|
|
922
1056
|
case "rejectAllRevisions":
|
|
923
|
-
return rejectAllRevisions(doc) > 0;
|
|
924
|
-
case "insertTable": {
|
|
925
|
-
const runEl = ids.elOf(intent.runId);
|
|
926
|
-
if (!runEl) return false;
|
|
927
|
-
const entry = runOf(runEl);
|
|
928
|
-
if (!entry || !entry.firstT) return false;
|
|
929
|
-
const before = trackedSet(ids, doc);
|
|
930
|
-
const ok = insertTableAfter(doc, entry.firstT, intent.rows, intent.cols);
|
|
931
|
-
if (ok) assignFreshTracked(ids, doc, before, intent.nodeIds);
|
|
932
|
-
return ok;
|
|
933
|
-
}
|
|
1057
|
+
return rejectAllRevisions(doc, intent.author) > 0;
|
|
934
1058
|
case "setLink": {
|
|
935
1059
|
if (!isSafeUrl(intent.url)) return false;
|
|
936
1060
|
const runEl = ids.elOf(intent.runId);
|
|
@@ -980,13 +1104,13 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
980
1104
|
const blockEl = ids.elOf(intent.blockId);
|
|
981
1105
|
if (!blockEl) return false;
|
|
982
1106
|
const target = firstTextDescendant(blockEl) ?? blockEl;
|
|
983
|
-
return adjustIndent(doc, [target], intent.direction);
|
|
1107
|
+
return adjustIndent(doc, [target], intent.direction, suggestMeta(doc, intent.suggest));
|
|
984
1108
|
}
|
|
985
1109
|
case "setSpacing": {
|
|
986
1110
|
const blockEl = ids.elOf(intent.blockId);
|
|
987
1111
|
if (!blockEl) return false;
|
|
988
1112
|
const target = firstTextDescendant(blockEl) ?? blockEl;
|
|
989
|
-
return setParagraphSpacing(doc, [target], intent.patch);
|
|
1113
|
+
return setParagraphSpacing(doc, [target], intent.patch, suggestMeta(doc, intent.suggest));
|
|
990
1114
|
}
|
|
991
1115
|
case "insertPageField": {
|
|
992
1116
|
const runEl = ids.elOf(intent.runId);
|
|
@@ -1044,14 +1168,65 @@ function applyIntentInner(doc, ids, intent, out) {
|
|
|
1044
1168
|
assignFreshTracked(ids, doc, before, intent.nodeIds);
|
|
1045
1169
|
return true;
|
|
1046
1170
|
}
|
|
1171
|
+
default: {
|
|
1172
|
+
const exhaustive = intent;
|
|
1173
|
+
return exhaustive;
|
|
1174
|
+
}
|
|
1047
1175
|
}
|
|
1048
1176
|
}
|
|
1177
|
+
function resolveOperationTarget(doc, ids, runOf, address, intent) {
|
|
1178
|
+
const empty = { t: null, run: null, cellParagraph: null, drawing: null };
|
|
1179
|
+
if (address === "document") return { ...empty, el: doc.docRoot };
|
|
1180
|
+
const addressId = intent[ADDRESS_WIRE_FIELD[address]];
|
|
1181
|
+
switch (address) {
|
|
1182
|
+
case "run": {
|
|
1183
|
+
const el = ids.elOf(addressId);
|
|
1184
|
+
if (!el) return null;
|
|
1185
|
+
const entry = runOf(el);
|
|
1186
|
+
if (!entry) return null;
|
|
1187
|
+
return { ...empty, el, t: entry.firstT ?? null, run: entry.run };
|
|
1188
|
+
}
|
|
1189
|
+
case "block": {
|
|
1190
|
+
const el = ids.elOf(addressId);
|
|
1191
|
+
if (!el) return null;
|
|
1192
|
+
return { ...empty, el, t: firstTextDescendant(el) };
|
|
1193
|
+
}
|
|
1194
|
+
case "cell": {
|
|
1195
|
+
const paraEl = ids.elOf(addressId) ?? null;
|
|
1196
|
+
const tbl = paraEl ? tableOf(doc, paraEl) : null;
|
|
1197
|
+
return tbl ? { ...empty, el: tbl, cellParagraph: paraEl } : null;
|
|
1198
|
+
}
|
|
1199
|
+
case "object": {
|
|
1200
|
+
const el = ids.elOf(addressId);
|
|
1201
|
+
if (!el) return null;
|
|
1202
|
+
const objectIndex = intent.objectIndex;
|
|
1203
|
+
const drawing = drawingIn(el, objectIndex, runOf);
|
|
1204
|
+
return drawing ? { ...empty, el, drawing } : null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function applyRegistered(doc, ids, runOf, intent) {
|
|
1209
|
+
const definition = registeredOperation(intent.kind);
|
|
1210
|
+
if (!definition) return false;
|
|
1211
|
+
const target = resolveOperationTarget(doc, ids, runOf, definition.address, intent);
|
|
1212
|
+
if (!target) return false;
|
|
1213
|
+
const nodeIds = "nodeIds" in intent ? intent.nodeIds : void 0;
|
|
1214
|
+
const before = nodeIds ? trackedSet(ids, doc) : null;
|
|
1215
|
+
if (!applyRegisteredOperation(doc, target, intent)) return false;
|
|
1216
|
+
if (definition.prunesIds) ids.prune(doc.editableRoots());
|
|
1217
|
+
if (before && nodeIds) assignFreshTracked(ids, doc, before, nodeIds);
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1049
1220
|
function assignFreshTracked(ids, doc, before, nodeIds) {
|
|
1050
1221
|
const fresh = [];
|
|
1051
1222
|
walkTracked(doc, (el) => {
|
|
1052
1223
|
if (!before.has(el)) fresh.push(el);
|
|
1053
1224
|
});
|
|
1054
|
-
for (
|
|
1225
|
+
for (const el of fresh) ids.unassign(el);
|
|
1226
|
+
for (let k = 0; k < fresh.length; k++) {
|
|
1227
|
+
if (k < nodeIds.length) ids.reassign(fresh[k], nodeIds[k]);
|
|
1228
|
+
else ids.assign(fresh[k]);
|
|
1229
|
+
}
|
|
1055
1230
|
}
|
|
1056
1231
|
function localRun(el) {
|
|
1057
1232
|
const n = el.name;
|
|
@@ -1080,27 +1255,41 @@ function firstTextDescendant(el) {
|
|
|
1080
1255
|
}
|
|
1081
1256
|
function tableOfParagraph(doc, ids, cellParagraphId) {
|
|
1082
1257
|
const paraEl = ids.elOf(cellParagraphId);
|
|
1083
|
-
|
|
1084
|
-
|
|
1258
|
+
return paraEl ? tableOf(doc, paraEl) : null;
|
|
1259
|
+
}
|
|
1260
|
+
function tableOf(doc, el) {
|
|
1261
|
+
for (let cur = el; cur; cur = doc.findParentOf(cur) ?? null) {
|
|
1085
1262
|
if (localName(cur.name) === "tbl") return cur;
|
|
1086
1263
|
}
|
|
1087
1264
|
return null;
|
|
1088
1265
|
}
|
|
1089
1266
|
function firstDrawingIn(el) {
|
|
1090
|
-
if (["drawing", "shape", "rect"].includes(localName(el.name))) return el;
|
|
1267
|
+
if (["drawing", "shape", "rect", "line"].includes(localName(el.name))) return el;
|
|
1091
1268
|
for (const c of el.children) {
|
|
1092
1269
|
const found = firstDrawingIn(c);
|
|
1093
1270
|
if (found) return found;
|
|
1094
1271
|
}
|
|
1095
1272
|
return null;
|
|
1096
1273
|
}
|
|
1097
|
-
function
|
|
1098
|
-
if (
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
return
|
|
1274
|
+
function drawingSource(content) {
|
|
1275
|
+
if (content.kind === "image" || content.kind === "drawing") return content.srcDrawing;
|
|
1276
|
+
if (content.kind !== "anchor") return void 0;
|
|
1277
|
+
const shape = content.shape;
|
|
1278
|
+
if (shape.type === "wordart") return shape.src;
|
|
1279
|
+
if (shape.type === "line") return shape.src;
|
|
1280
|
+
return shape.srcDrawing;
|
|
1281
|
+
}
|
|
1282
|
+
function drawingIn(runEl, objectIndex, runOf) {
|
|
1283
|
+
if (objectIndex === void 0) return firstDrawingIn(runEl);
|
|
1284
|
+
const content = runOf(runEl)?.run.content[objectIndex];
|
|
1285
|
+
return content ? drawingSource(content) ?? null : null;
|
|
1286
|
+
}
|
|
1287
|
+
function mathsIn(el) {
|
|
1288
|
+
if (localName(el.name) === "oMath") return [el];
|
|
1289
|
+
return el.children.flatMap(mathsIn);
|
|
1290
|
+
}
|
|
1291
|
+
function mathIn(el, index = 0) {
|
|
1292
|
+
return mathsIn(el)[index] ?? null;
|
|
1104
1293
|
}
|
|
1105
1294
|
function runEntry(run) {
|
|
1106
1295
|
return { run, firstT: run.content.find((c) => c.kind === "text")?.srcT };
|
|
@@ -1629,6 +1818,10 @@ function toSuggestions(tail, author, date) {
|
|
|
1629
1818
|
for (const intent of tail) {
|
|
1630
1819
|
if (intent.kind === "insertText") {
|
|
1631
1820
|
suggestions.push({ kind: "insertText", at: intent.at, text: intent.text, suggest: { author, date } });
|
|
1821
|
+
} else if (intent.kind === "insertSeparator") {
|
|
1822
|
+
suggestions.push({ kind: "insertSeparator", at: intent.at, separator: intent.separator, suggest: { author, date } });
|
|
1823
|
+
} else if (intent.kind === "deleteSeparator") {
|
|
1824
|
+
suggestions.push({ kind: "deleteSeparator", at: intent.at, suggest: { author, date } });
|
|
1632
1825
|
} else if (intent.kind === "deleteText") {
|
|
1633
1826
|
suggestions.push({
|
|
1634
1827
|
kind: "suggestRevision",
|
|
@@ -1656,7 +1849,7 @@ function arrivalMode(tailLength, diverged, opts = {}) {
|
|
|
1656
1849
|
}
|
|
1657
1850
|
|
|
1658
1851
|
// ../collab/src/protocol.ts
|
|
1659
|
-
var ENGINE_VERSION = "
|
|
1852
|
+
var ENGINE_VERSION = "e19";
|
|
1660
1853
|
var PRESENCE_MAX_RANGES = 64;
|
|
1661
1854
|
function sanitizePresencePosition(pos) {
|
|
1662
1855
|
if (!pos || !pos.ranges) return pos;
|
|
@@ -1672,7 +1865,7 @@ function sanitizePresencePosition(pos) {
|
|
|
1672
1865
|
if (clean.length === pos.ranges.length) return pos;
|
|
1673
1866
|
return clean.length > 0 ? { ...pos, ranges: clean } : { ...pos, ranges: void 0 };
|
|
1674
1867
|
}
|
|
1675
|
-
var PROTOCOL_VERSION =
|
|
1868
|
+
var PROTOCOL_VERSION = 6;
|
|
1676
1869
|
|
|
1677
1870
|
// ../collab/src/id-allocator.ts
|
|
1678
1871
|
var CarriedIdAllocator = class {
|
|
@@ -2223,9 +2416,19 @@ var _CollabConnection = class _CollabConnection {
|
|
|
2223
2416
|
sinceSeq: 0,
|
|
2224
2417
|
profile: opts?.profile,
|
|
2225
2418
|
codeProof: opts?.codeProof,
|
|
2226
|
-
ownerToken: opts?.ownerToken
|
|
2419
|
+
ownerToken: opts?.ownerToken,
|
|
2420
|
+
agentInvite: opts?.agentInvite
|
|
2227
2421
|
});
|
|
2228
2422
|
}
|
|
2423
|
+
registerAgentInvite(inviteId, token, expiresAt) {
|
|
2424
|
+
this.transport.send({ t: "agent-invite", inviteId, token, expiresAt });
|
|
2425
|
+
}
|
|
2426
|
+
revokeAgentInvite(inviteId) {
|
|
2427
|
+
this.transport.send({ t: "agent-invite-revoke", inviteId });
|
|
2428
|
+
}
|
|
2429
|
+
sendAgentChat(agentClientId, messageId, iv, ciphertext) {
|
|
2430
|
+
this.transport.send({ t: "agent-chat", agentClientId, messageId, iv, ciphertext });
|
|
2431
|
+
}
|
|
2229
2432
|
/** Owner admin op (doc 14 §2.5): honored only if this connection proved
|
|
2230
2433
|
* the owner token at hello — otherwise the server refuses `not-owner`. */
|
|
2231
2434
|
admin(action) {
|
|
@@ -2491,6 +2694,22 @@ var _CollabConnection = class _CollabConnection {
|
|
|
2491
2694
|
this.cb.onRoster?.(msg.roster);
|
|
2492
2695
|
return;
|
|
2493
2696
|
}
|
|
2697
|
+
case "agent-invite-registered": {
|
|
2698
|
+
this.cb.onAgentInviteRegistered?.({ inviteId: msg.inviteId, expiresAt: msg.expiresAt });
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
case "agent-invite-refused": {
|
|
2702
|
+
this.cb.onAgentInviteRefused?.({ inviteId: msg.inviteId, reason: msg.reason });
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
case "agent-connected": {
|
|
2706
|
+
this.cb.onAgentConnected?.(msg);
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
case "agent-chat": {
|
|
2710
|
+
this.cb.onAgentChat?.(msg);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2494
2713
|
case "session-warning": {
|
|
2495
2714
|
this.cb.onSessionWarning?.({ reason: msg.reason, inMs: msg.inMs });
|
|
2496
2715
|
return;
|
|
@@ -2591,7 +2810,12 @@ function invertIntent(doc, ids, intent) {
|
|
|
2591
2810
|
}
|
|
2592
2811
|
// Formatting / table / comment / merge inverses need prior-state capture
|
|
2593
2812
|
// (was-bold?, the merged paragraph's structure, …) — a documented
|
|
2594
|
-
// extension; not undoable yet.
|
|
2813
|
+
// extension; not undoable yet. That includes pasteBlocks: a rich paste in
|
|
2814
|
+
// a ROOM is not undoable (its inverse — remove N spliced blocks plus
|
|
2815
|
+
// re-merge the split — has no wire form yet), so Cmd+Z after a room paste
|
|
2816
|
+
// stops at it as a cannot-undo marker (see submitUndoNow). The
|
|
2817
|
+
// single-process session is unaffected: it replays local history
|
|
2818
|
+
// (EditorHost.onLocalHistory) and undoes a paste as one step.
|
|
2595
2819
|
default:
|
|
2596
2820
|
return null;
|
|
2597
2821
|
}
|
|
@@ -2607,7 +2831,8 @@ var DEFAULT_INTENT_LIMITS = {
|
|
|
2607
2831
|
};
|
|
2608
2832
|
function chartError(c, who) {
|
|
2609
2833
|
if (typeof c !== "object" || c === null) return `${who}: bad chart`;
|
|
2610
|
-
if (!["column", "bar", "line", "pie"].includes(c.type)) return `${who}: bad type`;
|
|
2834
|
+
if (!["column", "bar", "line", "pie", "doughnut", "area", "scatter"].includes(c.type)) return `${who}: bad type`;
|
|
2835
|
+
if (c.grouping !== void 0 && !["clustered", "stacked", "percentStacked"].includes(c.grouping)) return `${who}: bad grouping`;
|
|
2611
2836
|
if (!Array.isArray(c.categories) || c.categories.length === 0 || c.categories.length > 100) return `${who}: bad categories`;
|
|
2612
2837
|
if (c.categories.some((x) => typeof x !== "string" || x.length > 200)) return `${who}: bad category`;
|
|
2613
2838
|
if (c.title !== void 0 && (typeof c.title !== "string" || c.title.length > 200)) return `${who}: bad title`;
|
|
@@ -2625,8 +2850,49 @@ function smartArtError(a, who) {
|
|
|
2625
2850
|
if (a.items.some((x) => typeof x !== "string" || x.length > 500)) return `${who}: bad item`;
|
|
2626
2851
|
return null;
|
|
2627
2852
|
}
|
|
2853
|
+
function badCharacterStyle(patch, who) {
|
|
2854
|
+
const id = patch?.characterStyleId;
|
|
2855
|
+
if (id === void 0 || id === null) return null;
|
|
2856
|
+
return typeof id === "string" && /^[A-Za-z0-9\-_]{1,253}$/.test(id) ? null : `${who}: bad characterStyleId`;
|
|
2857
|
+
}
|
|
2858
|
+
function badTextEffect(patch, who) {
|
|
2859
|
+
const effect = patch?.textEffect;
|
|
2860
|
+
if (effect === void 0 || effect === null) return null;
|
|
2861
|
+
if (typeof effect !== "object") return `${who}: bad textEffect`;
|
|
2862
|
+
const e = effect;
|
|
2863
|
+
if (e.shadow !== void 0 && typeof e.shadow !== "boolean") return `${who}: bad textEffect.shadow`;
|
|
2864
|
+
if (e.outline !== void 0 && e.outline !== null) {
|
|
2865
|
+
if (typeof e.outline !== "object") return `${who}: bad textEffect.outline`;
|
|
2866
|
+
const o = e.outline;
|
|
2867
|
+
if (typeof o.color !== "string" || !/^#?[0-9a-fA-F]{6}$/.test(o.color)) return `${who}: bad textEffect.outline.color`;
|
|
2868
|
+
if (typeof o.widthPt !== "number" || !Number.isFinite(o.widthPt) || o.widthPt <= 0 || o.widthPt > 50) {
|
|
2869
|
+
return `${who}: bad textEffect.outline.widthPt`;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return null;
|
|
2873
|
+
}
|
|
2628
2874
|
function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
2629
2875
|
const nonNegInt = (n) => Number.isInteger(n) && n >= 0;
|
|
2876
|
+
if (typeof intent.clientId !== "string" || intent.clientId.length === 0 || intent.clientId.length > 128) {
|
|
2877
|
+
return "intent: bad clientId";
|
|
2878
|
+
}
|
|
2879
|
+
if (!nonNegInt(intent.clientSeq)) return "intent: bad clientSeq";
|
|
2880
|
+
if (!nonNegInt(intent.base)) return "intent: bad base";
|
|
2881
|
+
const badMathIndex = (math, kind) => math.mathIndex === void 0 || nonNegInt(math.mathIndex) && math.mathIndex <= 1e3 ? null : `${kind}: bad mathIndex`;
|
|
2882
|
+
const badSuggest = (suggest) => {
|
|
2883
|
+
if (suggest === void 0) return null;
|
|
2884
|
+
if (typeof suggest.author !== "string" || suggest.author.length > 100) return `${intent.kind}: bad author`;
|
|
2885
|
+
if (typeof suggest.date !== "string" || suggest.date.length > 40) return `${intent.kind}: bad date`;
|
|
2886
|
+
return null;
|
|
2887
|
+
};
|
|
2888
|
+
if ("suggest" in intent && intent.kind !== "suggestRevision") {
|
|
2889
|
+
const bad = badSuggest(intent.suggest);
|
|
2890
|
+
if (bad) return bad;
|
|
2891
|
+
}
|
|
2892
|
+
if ("objectIndex" in intent && intent.objectIndex !== void 0 && (!Number.isInteger(intent.objectIndex) || intent.objectIndex < 0 || intent.objectIndex > 1e4)) {
|
|
2893
|
+
return `${intent.kind}: bad objectIndex`;
|
|
2894
|
+
}
|
|
2895
|
+
if (isRegisteredIntent(intent)) return validateRegisteredOperation(intent);
|
|
2630
2896
|
switch (intent.kind) {
|
|
2631
2897
|
case "insertText":
|
|
2632
2898
|
if (typeof intent.text !== "string") return "insertText: text not a string";
|
|
@@ -2634,6 +2900,13 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2634
2900
|
if (intent.text.length > limits.maxInsertLength) return "insertText: too long";
|
|
2635
2901
|
if (!nonNegInt(intent.at.offset)) return "insertText: bad offset";
|
|
2636
2902
|
return null;
|
|
2903
|
+
case "insertSeparator":
|
|
2904
|
+
if (intent.separator !== "br" && intent.separator !== "tab") return "insertSeparator: bad separator";
|
|
2905
|
+
if (!nonNegInt(intent.at.offset)) return "insertSeparator: bad offset";
|
|
2906
|
+
return null;
|
|
2907
|
+
case "deleteSeparator":
|
|
2908
|
+
if (!nonNegInt(intent.at.offset)) return "deleteSeparator: bad offset";
|
|
2909
|
+
return null;
|
|
2637
2910
|
case "suggestRevision": {
|
|
2638
2911
|
const nRanges = intent.ranges?.length ?? 0;
|
|
2639
2912
|
const nMarks = intent.marks?.length ?? 0;
|
|
@@ -2658,15 +2931,11 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2658
2931
|
case "splitParagraph":
|
|
2659
2932
|
if (!nonNegInt(intent.at.offset)) return "splitParagraph: bad offset";
|
|
2660
2933
|
if (!nonNegInt(intent.newBlockId) || !nonNegInt(intent.newRunId)) return "splitParagraph: bad ids";
|
|
2661
|
-
if (intent.suggest !== void 0) {
|
|
2662
|
-
if (typeof intent.suggest.author !== "string" || intent.suggest.author.length > 100) return "splitParagraph: bad author";
|
|
2663
|
-
if (typeof intent.suggest.date !== "string" || intent.suggest.date.length > 40) return "splitParagraph: bad date";
|
|
2664
|
-
}
|
|
2665
2934
|
return null;
|
|
2666
2935
|
case "formatRange":
|
|
2667
2936
|
if (!nonNegInt(intent.start) || !nonNegInt(intent.end) || intent.end <= intent.start) return "formatRange: bad range";
|
|
2668
2937
|
if (!nonNegInt(intent.middleId)) return "formatRange: bad middleId";
|
|
2669
|
-
return
|
|
2938
|
+
return badCharacterStyle(intent.patch, "formatRange") ?? badTextEffect(intent.patch, "formatRange");
|
|
2670
2939
|
case "commentRun":
|
|
2671
2940
|
if (typeof intent.text !== "string" || intent.text.length === 0) return "commentRun: empty";
|
|
2672
2941
|
if (intent.text.length > limits.maxCommentLength) return "commentRun: too long";
|
|
@@ -2692,11 +2961,9 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2692
2961
|
if (typeof intent.mathText !== "string" || intent.mathText.length === 0) return "insertMath: empty";
|
|
2693
2962
|
if (intent.mathText.length > 1e4) return "insertMath: too long";
|
|
2694
2963
|
return null;
|
|
2695
|
-
case "insertShape":
|
|
2696
|
-
|
|
2697
|
-
if (!presets.includes(intent.preset)) return "insertShape: bad preset";
|
|
2964
|
+
case "insertShape":
|
|
2965
|
+
if (typeof intent.preset !== "string" || !isValidShapePreset(intent.preset)) return "insertShape: bad preset";
|
|
2698
2966
|
return null;
|
|
2699
|
-
}
|
|
2700
2967
|
case "replyComment":
|
|
2701
2968
|
if (typeof intent.text !== "string" || intent.text.length === 0) return "replyComment: empty";
|
|
2702
2969
|
if (intent.text.length > 2e4) return "replyComment: too long";
|
|
@@ -2766,7 +3033,19 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2766
3033
|
case "insertWordArt": {
|
|
2767
3034
|
if (typeof intent.text !== "string" || intent.text.length === 0) return "insertWordArt: empty text";
|
|
2768
3035
|
if (intent.text.length > 500) return "insertWordArt: text too long";
|
|
2769
|
-
if (!["plain", "archUp", "archDown", "wave", "chevron"].includes(intent.preset)) return "insertWordArt: bad preset";
|
|
3036
|
+
if (!["plain", "archUp", "archDown", "wave", "chevron", "circle", "button", "chevronDown"].includes(intent.preset)) return "insertWordArt: bad preset";
|
|
3037
|
+
if (intent.style !== void 0) {
|
|
3038
|
+
const style = intent.style;
|
|
3039
|
+
if (typeof style !== "object" || style === null) return "insertWordArt: bad style";
|
|
3040
|
+
if (!/^[0-9a-fA-F]{6}$/.test(style.fill)) return "insertWordArt: bad style fill";
|
|
3041
|
+
if (style.shadow !== void 0 && typeof style.shadow !== "boolean") return "insertWordArt: bad style shadow";
|
|
3042
|
+
if (style.outline !== void 0) {
|
|
3043
|
+
if (typeof style.outline !== "object" || style.outline === null) return "insertWordArt: bad style outline";
|
|
3044
|
+
if (!/^[0-9a-fA-F]{6}$/.test(style.outline.color)) return "insertWordArt: bad outline color";
|
|
3045
|
+
const width = style.outline.widthPt;
|
|
3046
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width <= 0 || width > 50) return "insertWordArt: bad outline width";
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
2770
3049
|
return null;
|
|
2771
3050
|
}
|
|
2772
3051
|
case "insertChart":
|
|
@@ -2780,7 +3059,12 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2780
3059
|
case "setDrawingWordArtText":
|
|
2781
3060
|
if (typeof intent.text !== "string" || intent.text.length === 0 || intent.text.length > 500) return "setDrawingWordArtText: bad text";
|
|
2782
3061
|
return null;
|
|
3062
|
+
case "setDrawingWordArtStyle":
|
|
3063
|
+
if (!/^[0-9a-fA-F]{6}$/.test(intent.color)) return "setDrawingWordArtStyle: bad color";
|
|
3064
|
+
if (typeof intent.opacity !== "number" || !Number.isFinite(intent.opacity) || intent.opacity < 0 || intent.opacity > 1) return "setDrawingWordArtStyle: bad opacity";
|
|
3065
|
+
return null;
|
|
2783
3066
|
case "setDrawingLineStyle":
|
|
3067
|
+
if (intent.color === null) return null;
|
|
2784
3068
|
if (!/^[0-9a-fA-F]{6}$/.test(intent.color)) return "setDrawingLineStyle: bad color";
|
|
2785
3069
|
if (typeof intent.widthPx !== "number" || !Number.isFinite(intent.widthPx) || intent.widthPx <= 0 || intent.widthPx > 100) return "setDrawingLineStyle: bad width";
|
|
2786
3070
|
if (!["solid", "dashed", "dotted"].includes(intent.dash)) return "setDrawingLineStyle: bad dash";
|
|
@@ -2834,11 +3118,6 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2834
3118
|
}
|
|
2835
3119
|
return null;
|
|
2836
3120
|
}
|
|
2837
|
-
case "resizeTableRow": {
|
|
2838
|
-
if (!Number.isInteger(intent.rowIdx) || intent.rowIdx < 0 || intent.rowIdx > 5e3) return "resizeTableRow: bad row";
|
|
2839
|
-
if (typeof intent.heightPx !== "number" || !Number.isFinite(intent.heightPx) || intent.heightPx < 1 || intent.heightPx > 2e4) return "resizeTableRow: bad height";
|
|
2840
|
-
return null;
|
|
2841
|
-
}
|
|
2842
3121
|
case "moveTable": {
|
|
2843
3122
|
const num = (v) => typeof v === "number" && Number.isFinite(v) && v >= -5e3 && v <= 2e4;
|
|
2844
3123
|
if (!num(intent.xPx) || !num(intent.yPx)) return "moveTable: bad position";
|
|
@@ -2851,13 +3130,13 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2851
3130
|
// run-addressed only; resolution is the whole check
|
|
2852
3131
|
case "setMathLinear":
|
|
2853
3132
|
if (typeof intent.mathText !== "string" || intent.mathText.length === 0 || intent.mathText.length > 1e3) return "setMathLinear: bad mathText";
|
|
2854
|
-
return
|
|
3133
|
+
return badMathIndex(intent, "setMathLinear");
|
|
2855
3134
|
case "deleteMath":
|
|
2856
|
-
return
|
|
3135
|
+
return badMathIndex(intent, "deleteMath");
|
|
2857
3136
|
case "moveMath":
|
|
2858
3137
|
if (!nonNegInt(intent.at?.offset)) return "moveMath: bad offset";
|
|
2859
3138
|
if (!Array.isArray(intent.nodeIds) || intent.nodeIds.length > 64) return "moveMath: bad nodeIds";
|
|
2860
|
-
return
|
|
3139
|
+
return badMathIndex(intent, "moveMath");
|
|
2861
3140
|
case "ensureHeaderFooter":
|
|
2862
3141
|
if (intent.hfKind !== "header" && intent.hfKind !== "footer") return "ensureHeaderFooter: bad kind";
|
|
2863
3142
|
if (!Array.isArray(intent.nodeIds) || intent.nodeIds.length > 64) return "ensureHeaderFooter: bad nodeIds";
|
|
@@ -2865,24 +3144,30 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2865
3144
|
case "deleteComment":
|
|
2866
3145
|
if (typeof intent.commentId !== "string" || intent.commentId.length === 0 || intent.commentId.length > 64) return "deleteComment: bad id";
|
|
2867
3146
|
return null;
|
|
3147
|
+
case "resolveComment":
|
|
3148
|
+
if (typeof intent.commentId !== "string" || intent.commentId.length === 0 || intent.commentId.length > 64) return "resolveComment: bad id";
|
|
3149
|
+
if (typeof intent.resolved !== "boolean") return "resolveComment: bad resolved";
|
|
3150
|
+
if (typeof intent.paraId !== "string" || intent.paraId.length === 0 || intent.paraId.length > 16) return "resolveComment: bad paraId";
|
|
3151
|
+
return null;
|
|
3152
|
+
case "editComment":
|
|
3153
|
+
if (typeof intent.commentId !== "string" || intent.commentId.length === 0 || intent.commentId.length > 64) return "editComment: bad id";
|
|
3154
|
+
if (typeof intent.text !== "string" || intent.text.length === 0) return "editComment: empty";
|
|
3155
|
+
if (intent.text.length > limits.maxCommentLength) return "editComment: too long";
|
|
3156
|
+
return null;
|
|
2868
3157
|
case "insertBookmarkRange":
|
|
2869
3158
|
if (typeof intent.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]{0,39}$/.test(intent.name)) return "insertBookmarkRange: bad name";
|
|
2870
3159
|
if (!nonNegInt(intent.start) || !nonNegInt(intent.end) || intent.end <= intent.start) return "insertBookmarkRange: bad range";
|
|
2871
3160
|
return null;
|
|
2872
|
-
case "toggleCheckbox":
|
|
2873
|
-
return null;
|
|
2874
3161
|
case "acceptRevision":
|
|
2875
3162
|
case "rejectRevision":
|
|
2876
3163
|
if (!nonNegInt(intent.index) || intent.index > 1e5) return `${intent.kind}: bad index`;
|
|
2877
3164
|
return null;
|
|
2878
3165
|
case "acceptAllRevisions":
|
|
2879
3166
|
case "rejectAllRevisions":
|
|
3167
|
+
if (intent.author !== void 0 && (typeof intent.author !== "string" || intent.author.length > 255)) {
|
|
3168
|
+
return `${intent.kind}: bad author`;
|
|
3169
|
+
}
|
|
2880
3170
|
return null;
|
|
2881
|
-
case "insertTable": {
|
|
2882
|
-
const okDim = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 50;
|
|
2883
|
-
if (!okDim(intent.rows) || !okDim(intent.cols)) return "insertTable: bad dimensions";
|
|
2884
|
-
return null;
|
|
2885
|
-
}
|
|
2886
3171
|
case "setLineNumbering": {
|
|
2887
3172
|
const p = intent.patch;
|
|
2888
3173
|
if (typeof p !== "object" || p === null || typeof p.enabled !== "boolean") return "setLineNumbering: bad patch";
|
|
@@ -2898,38 +3183,8 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2898
3183
|
return null;
|
|
2899
3184
|
}
|
|
2900
3185
|
case "insertField": {
|
|
2901
|
-
if (typeof intent.instruction !== "string" || intent.instruction.length === 0 || intent.instruction.length > 200) return "insertField: bad instruction";
|
|
2902
3186
|
if (intent.cachedResult !== void 0 && (typeof intent.cachedResult !== "string" || intent.cachedResult.length > 1e3)) return "insertField: bad cachedResult";
|
|
2903
|
-
|
|
2904
|
-
"PAGE",
|
|
2905
|
-
"NUMPAGES",
|
|
2906
|
-
"SECTIONPAGES",
|
|
2907
|
-
"SECTION",
|
|
2908
|
-
"DATE",
|
|
2909
|
-
"TIME",
|
|
2910
|
-
"CREATEDATE",
|
|
2911
|
-
"SAVEDATE",
|
|
2912
|
-
"PRINTDATE",
|
|
2913
|
-
"AUTHOR",
|
|
2914
|
-
"TITLE",
|
|
2915
|
-
"SUBJECT",
|
|
2916
|
-
"KEYWORDS",
|
|
2917
|
-
"COMMENTS",
|
|
2918
|
-
"FILENAME",
|
|
2919
|
-
"NUMWORDS",
|
|
2920
|
-
"NUMCHARS",
|
|
2921
|
-
"PAGEREF",
|
|
2922
|
-
"REF",
|
|
2923
|
-
"SEQ",
|
|
2924
|
-
"STYLEREF",
|
|
2925
|
-
"TOC",
|
|
2926
|
-
"INDEX",
|
|
2927
|
-
"LISTNUM",
|
|
2928
|
-
"QUOTE"
|
|
2929
|
-
]);
|
|
2930
|
-
const type = intent.instruction.trim().split(/\s+/)[0]?.toUpperCase();
|
|
2931
|
-
if (!type || !SAFE_FIELDS.has(type)) return "insertField: field type not allowed";
|
|
2932
|
-
if (!/^[\x20-\x7e]+$/.test(intent.instruction)) return "insertField: illegal instruction char";
|
|
3187
|
+
if (!isInsertableFieldInstruction(intent.instruction)) return "insertField: instruction not allowed";
|
|
2933
3188
|
return null;
|
|
2934
3189
|
}
|
|
2935
3190
|
case "setDrawingRotation":
|
|
@@ -2939,14 +3194,14 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2939
3194
|
if (intent.color !== null && !/^[0-9a-fA-F]{6}$/.test(intent.color)) return "setDrawingFill: bad color";
|
|
2940
3195
|
return null;
|
|
2941
3196
|
case "formatRun":
|
|
3197
|
+
return badCharacterStyle(intent.patch, "formatRun") ?? badTextEffect(intent.patch, "formatRun");
|
|
2942
3198
|
case "formatParagraph":
|
|
2943
|
-
case "setListType":
|
|
2944
3199
|
case "mergeParagraph":
|
|
2945
3200
|
return null;
|
|
2946
3201
|
case "tableOp": {
|
|
2947
3202
|
const op = intent.op;
|
|
2948
3203
|
if (typeof op === "string") {
|
|
2949
|
-
return ["deleteRow", "deleteCol", "deleteTable", "rowAbove", "rowBelow", "colLeft", "colRight"].includes(op) ? null : "tableOp: bad operation";
|
|
3204
|
+
return ["deleteRow", "deleteCol", "deleteTable", "rowAbove", "rowBelow", "colLeft", "colRight", "mergeRight", "mergeDown", "splitCell"].includes(op) ? null : "tableOp: bad operation";
|
|
2950
3205
|
}
|
|
2951
3206
|
if (op.kind === "cellShading") {
|
|
2952
3207
|
return op.fill === null || /^[0-9a-fA-F]{6}$/.test(op.fill) ? null : "tableOp: bad shading";
|
|
@@ -2958,6 +3213,10 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
|
|
|
2958
3213
|
if (op.wrapping !== "none" && op.wrapping !== "around" || !Number.isFinite(op.xPx) || !Number.isFinite(op.yPx)) return "tableOp: bad text wrapping";
|
|
2959
3214
|
return null;
|
|
2960
3215
|
}
|
|
3216
|
+
default: {
|
|
3217
|
+
const exhaustive = intent;
|
|
3218
|
+
return exhaustive;
|
|
3219
|
+
}
|
|
2961
3220
|
}
|
|
2962
3221
|
}
|
|
2963
3222
|
|
|
@@ -3300,6 +3559,15 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3300
3559
|
this.activity = [];
|
|
3301
3560
|
/** Bundle being resumed from (consumed after the welcome replay). */
|
|
3302
3561
|
this.resuming = null;
|
|
3562
|
+
this.queuedUndos = 0;
|
|
3563
|
+
this.undoDeadlineTimer = null;
|
|
3564
|
+
/**
|
|
3565
|
+
* How long held undos wait for the pending queue to drain before they are
|
|
3566
|
+
* dropped. A few seconds: long enough to cover an ordinary round trip under
|
|
3567
|
+
* load, short enough that a dead connection tells the user quickly. Settable
|
|
3568
|
+
* for tests.
|
|
3569
|
+
*/
|
|
3570
|
+
this.undoDrainTimeoutMs = 5e3;
|
|
3303
3571
|
/**
|
|
3304
3572
|
* Throws on the ingest/submit chain — in practice a seal or transport send
|
|
3305
3573
|
* that failed AFTER the edit was accepted locally and counted as pending.
|
|
@@ -3392,6 +3660,7 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3392
3660
|
codeProof: opts?.codeProof,
|
|
3393
3661
|
ownerToken: opts?.ownerToken,
|
|
3394
3662
|
// owner-capability proof (doc 14 §2.5)
|
|
3663
|
+
agentInvite: opts?.agentInvite,
|
|
3395
3664
|
engineVersion: ENGINE_VERSION
|
|
3396
3665
|
// the fence for client-derived canon (doc 13 §2)
|
|
3397
3666
|
});
|
|
@@ -3437,6 +3706,15 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3437
3706
|
setProfile(profile) {
|
|
3438
3707
|
this.transport.send({ t: "profile", profile });
|
|
3439
3708
|
}
|
|
3709
|
+
registerAgentInvite(inviteId, token, expiresAt) {
|
|
3710
|
+
this.transport.send({ t: "agent-invite", inviteId, token, expiresAt });
|
|
3711
|
+
}
|
|
3712
|
+
revokeAgentInvite(inviteId) {
|
|
3713
|
+
this.transport.send({ t: "agent-invite-revoke", inviteId });
|
|
3714
|
+
}
|
|
3715
|
+
sendAgentChat(agentClientId, messageId, iv, ciphertext) {
|
|
3716
|
+
this.transport.send({ t: "agent-chat", agentClientId, messageId, iv, ciphertext });
|
|
3717
|
+
}
|
|
3440
3718
|
/**
|
|
3441
3719
|
* Rejoin from a persisted bundle (doc 12 §5, encrypted flavor). Identical
|
|
3442
3720
|
* contract to the plaintext connection: clientSeq watermark restored
|
|
@@ -3462,7 +3740,9 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3462
3740
|
* connection — the react layer's `as unknown as CollabConnection` cast
|
|
3463
3741
|
* hid it from the typechecker, so the demo's owner controls were silent
|
|
3464
3742
|
* TypeErrors on encrypted docs (the default), long misattributed to a
|
|
3465
|
-
* Vite prebundle race ("stale session w/o admin").
|
|
3743
|
+
* Vite prebundle race ("stale session w/o admin"). That cast is gone: this
|
|
3744
|
+
* class now `implements CollabConnectionLike`, so the same omission is a
|
|
3745
|
+
* compile error rather than a runtime silence. */
|
|
3466
3746
|
admin(action) {
|
|
3467
3747
|
this.transport.send({ t: "admin", action });
|
|
3468
3748
|
}
|
|
@@ -3509,7 +3789,87 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3509
3789
|
this.submitFull(intent, true);
|
|
3510
3790
|
}
|
|
3511
3791
|
/**
|
|
3512
|
-
* COLLABORATIVE UNDO
|
|
3792
|
+
* COLLABORATIVE UNDO, the entry point — a PENDING GATE in front of the real
|
|
3793
|
+
* work below.
|
|
3794
|
+
*
|
|
3795
|
+
* The undo stack lives on the MIRROR, which advances only by ingesting
|
|
3796
|
+
* sequenced envelopes, while this client paints its OWN edits optimistically
|
|
3797
|
+
* the moment they are made. So between a keystroke and its echo the mirror
|
|
3798
|
+
* has not seen it, and `submitUndoNow` would reverse the previous
|
|
3799
|
+
* CONFIRMED action instead — the user types, presses Ctrl+Z, and watches an
|
|
3800
|
+
* older edit disappear while the one they just made stays. Architecturally
|
|
3801
|
+
* correct and completely wrong to a person.
|
|
3802
|
+
*
|
|
3803
|
+
* The press is therefore HELD while `pendingCount > 0` and run on the drain,
|
|
3804
|
+
* against a mirror that by then holds the edit the user meant. Presses
|
|
3805
|
+
* ACCUMULATE (two quick Ctrl+Z's undo two actions), because that is what the
|
|
3806
|
+
* same two presses do outside a room.
|
|
3807
|
+
*
|
|
3808
|
+
* Held presses are not held forever: the deadline exists because a queue
|
|
3809
|
+
* that never drains means the connection stopped carrying edits to
|
|
3810
|
+
* confirmation, and running the undo then would reverse the wrong action
|
|
3811
|
+
* anyway. At the deadline the queue is DROPPED and onUndoQueue reports it,
|
|
3812
|
+
* beside the offline/writes-blocked affordances that are already up by then.
|
|
3813
|
+
* Each press restarts that deadline, so it measures the wait the user is
|
|
3814
|
+
* actually watching rather than the age of the oldest press.
|
|
3815
|
+
*
|
|
3816
|
+
* A held press targets whatever this client's newest action is WHEN IT RUNS,
|
|
3817
|
+
* so typing during the wait makes the new keystrokes the undo's target. It
|
|
3818
|
+
* cannot be otherwise — the whole reason the press is held is that the
|
|
3819
|
+
* target it would name is not in the mirror yet — and it stays "undo my
|
|
3820
|
+
* latest thing", which is what the key means.
|
|
3821
|
+
*/
|
|
3822
|
+
undoLast() {
|
|
3823
|
+
if (!this.mirror || !this.replica || !this.keys || !this.genesisId) return "unavailable";
|
|
3824
|
+
if (this.replica.pendingCount === 0) return this.submitUndoNow();
|
|
3825
|
+
this.queuedUndos++;
|
|
3826
|
+
this.armUndoDeadline();
|
|
3827
|
+
this.cb.onUndoQueue?.({ queued: this.queuedUndos, expired: false });
|
|
3828
|
+
return "queued";
|
|
3829
|
+
}
|
|
3830
|
+
/** Undos held for the drain (see undoLast) — the count onUndoQueue reports. */
|
|
3831
|
+
get undoQueued() {
|
|
3832
|
+
return this.queuedUndos;
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Run ONE held undo now that the pending queue is empty. True when it
|
|
3836
|
+
* submitted — the queue is non-empty again, so the caller has nothing left
|
|
3837
|
+
* to schedule.
|
|
3838
|
+
*/
|
|
3839
|
+
runQueuedUndo() {
|
|
3840
|
+
if (this.queuedUndos === 0) return false;
|
|
3841
|
+
while (this.queuedUndos > 0) {
|
|
3842
|
+
this.queuedUndos--;
|
|
3843
|
+
const outcome = this.submitUndoNow();
|
|
3844
|
+
if (outcome === "undone") {
|
|
3845
|
+
if (this.queuedUndos > 0) this.armUndoDeadline();
|
|
3846
|
+
else this.clearUndoDeadline();
|
|
3847
|
+
this.cb.onUndoQueue?.({ queued: this.queuedUndos, expired: false });
|
|
3848
|
+
return true;
|
|
3849
|
+
}
|
|
3850
|
+
if (outcome !== "changed-since") this.queuedUndos = 0;
|
|
3851
|
+
}
|
|
3852
|
+
this.clearUndoDeadline();
|
|
3853
|
+
this.cb.onUndoQueue?.({ queued: 0, expired: false });
|
|
3854
|
+
return false;
|
|
3855
|
+
}
|
|
3856
|
+
armUndoDeadline() {
|
|
3857
|
+
this.clearUndoDeadline();
|
|
3858
|
+
this.undoDeadlineTimer = setTimeout(() => {
|
|
3859
|
+
this.undoDeadlineTimer = null;
|
|
3860
|
+
if (this.queuedUndos === 0) return;
|
|
3861
|
+
this.queuedUndos = 0;
|
|
3862
|
+
this.cb.onUndoQueue?.({ queued: 0, expired: true });
|
|
3863
|
+
}, this.undoDrainTimeoutMs);
|
|
3864
|
+
}
|
|
3865
|
+
clearUndoDeadline() {
|
|
3866
|
+
if (this.undoDeadlineTimer !== null) clearTimeout(this.undoDeadlineTimer);
|
|
3867
|
+
this.undoDeadlineTimer = null;
|
|
3868
|
+
}
|
|
3869
|
+
/**
|
|
3870
|
+
* COLLABORATIVE UNDO (plan doc 03 Phase 8), encrypted flavour. Callers go
|
|
3871
|
+
* through `undoLast`, which holds the press until the mirror can see this
|
|
3872
|
+
* client's own edits; everything below assumes that already happened.
|
|
3513
3873
|
*
|
|
3514
3874
|
* The mirror is a real DocumentSession fed the canonical log, so it already
|
|
3515
3875
|
* knows the inverse of this client's last action — no new wire message and
|
|
@@ -3526,7 +3886,7 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3526
3886
|
* canonical id table first, so the common conflict (someone else removed
|
|
3527
3887
|
* the thing) is declined up front rather than painted and yanked back.
|
|
3528
3888
|
*/
|
|
3529
|
-
|
|
3889
|
+
submitUndoNow() {
|
|
3530
3890
|
if (!this.mirror || !this.replica || !this.keys || !this.genesisId) return "unavailable";
|
|
3531
3891
|
const candidate = this.mirror.takeUndo(this.clientId);
|
|
3532
3892
|
if (candidate.kind !== "undoable") return candidate.kind;
|
|
@@ -3743,6 +4103,22 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3743
4103
|
this.cb.onRoster?.(msg.roster);
|
|
3744
4104
|
return;
|
|
3745
4105
|
}
|
|
4106
|
+
case "agent-invite-registered": {
|
|
4107
|
+
this.cb.onAgentInviteRegistered?.({ inviteId: msg.inviteId, expiresAt: msg.expiresAt });
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
case "agent-invite-refused": {
|
|
4111
|
+
this.cb.onAgentInviteRefused?.({ inviteId: msg.inviteId, reason: msg.reason });
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
case "agent-connected": {
|
|
4115
|
+
this.cb.onAgentConnected?.(msg);
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
case "agent-chat": {
|
|
4119
|
+
this.cb.onAgentChat?.(msg);
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
3746
4122
|
// Session lifecycle warnings are PLAINTEXT bookkeeping and identical in
|
|
3747
4123
|
// both modes: the deadline is the server's own policy, not document
|
|
3748
4124
|
// content, so a blind sequencer announces it in the clear. Dispatched
|
|
@@ -3816,6 +4192,7 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
|
|
|
3816
4192
|
this.scheduleStuckCheck();
|
|
3817
4193
|
return;
|
|
3818
4194
|
}
|
|
4195
|
+
if (this.runQueuedUndo()) return;
|
|
3819
4196
|
this.redriveBackoffMs = 0;
|
|
3820
4197
|
if (this.redriveTimer !== null) {
|
|
3821
4198
|
clearTimeout(this.redriveTimer);
|
|
@@ -4305,8 +4682,12 @@ function useCollab(opts) {
|
|
|
4305
4682
|
const [notOwner, setNotOwner] = useState(false);
|
|
4306
4683
|
const [epochChanged, setEpochChanged] = useState(null);
|
|
4307
4684
|
const [roster, setRoster] = useState([]);
|
|
4685
|
+
const [agentConnections, setAgentConnections] = useState([]);
|
|
4686
|
+
const [agentChat, setAgentChat] = useState([]);
|
|
4687
|
+
const [agentInviteError, setAgentInviteError] = useState(null);
|
|
4308
4688
|
const [arrival, setArrival] = useState(null);
|
|
4309
4689
|
const [selfHeals, setSelfHeals] = useState(0);
|
|
4690
|
+
const [undoQueue, setUndoQueue] = useState({ queued: 0, expired: false });
|
|
4310
4691
|
const [droppedPreReady, setDroppedPreReady] = useState(0);
|
|
4311
4692
|
const [persistErrors, setPersistErrors] = useState(0);
|
|
4312
4693
|
const [storeSlow, setStoreSlow] = useState(false);
|
|
@@ -4640,7 +5021,17 @@ function useCollab(opts) {
|
|
|
4640
5021
|
// there is deliberately no per-reason branch here to get wrong.
|
|
4641
5022
|
onSessionWarningCleared: () => setDeadlines((d) => d.idle === null ? d : { ...d, idle: null }),
|
|
4642
5023
|
onRoster: (r) => setRoster(r),
|
|
5024
|
+
onAgentInviteRegistered: () => setAgentInviteError(null),
|
|
5025
|
+
onAgentInviteRefused: (invite) => setAgentInviteError(invite),
|
|
5026
|
+
onAgentConnected: (agent) => setAgentConnections((current) => {
|
|
5027
|
+
const next = current.filter((item) => item.inviteId !== agent.inviteId);
|
|
5028
|
+
return [...next, agent];
|
|
5029
|
+
}),
|
|
5030
|
+
onAgentChat: (message) => setAgentChat((current) => [...current.slice(-199), message]),
|
|
4643
5031
|
onSelfHeal: () => setSelfHeals((n) => n + 1),
|
|
5032
|
+
// Held undos (see CollabSession.undoQueued): the count is authoritative
|
|
5033
|
+
// in the connection, so it is copied rather than derived here.
|
|
5034
|
+
onUndoQueue: (state) => setUndoQueue(state),
|
|
4644
5035
|
// A submit that never left the client. Counted, never swallowed.
|
|
4645
5036
|
onSubmitDropped: () => setDroppedPreReady((n) => n + 1),
|
|
4646
5037
|
// Async failures with nowhere to return (seal, transport send). Surfaced
|
|
@@ -4829,7 +5220,13 @@ function useCollab(opts) {
|
|
|
4829
5220
|
// at construction — a pre-existing modelling wrinkle, not this arc's), so
|
|
4830
5221
|
// the access is structural. The TYPE is the real one now, so a change to
|
|
4831
5222
|
// UndoOutcome breaks here rather than silently disagreeing.
|
|
4832
|
-
undoLast: () =>
|
|
5223
|
+
undoLast: () => {
|
|
5224
|
+
if (connectionRef.current !== "live") return "unavailable";
|
|
5225
|
+
setUndoQueue((state) => state.expired ? { queued: state.queued, expired: false } : state);
|
|
5226
|
+
return connRef.current?.undoLast?.() ?? "unavailable";
|
|
5227
|
+
},
|
|
5228
|
+
undoQueued: undoQueue.queued,
|
|
5229
|
+
undoExpired: undoQueue.expired,
|
|
4833
5230
|
presence: connection === "live" ? presence : EMPTY_PRESENCE,
|
|
4834
5231
|
refused,
|
|
4835
5232
|
sessionWarning,
|
|
@@ -4899,6 +5296,18 @@ function useCollab(opts) {
|
|
|
4899
5296
|
retryWrites: () => setReadOnlyBlocked(false),
|
|
4900
5297
|
epochChanged,
|
|
4901
5298
|
roster,
|
|
5299
|
+
agentConnections,
|
|
5300
|
+
agentChat,
|
|
5301
|
+
agentInviteError,
|
|
5302
|
+
registerAgentInvite: (inviteId, inviteToken, expiresAt) => {
|
|
5303
|
+
if (connectionRef.current === "live") connRef.current?.registerAgentInvite(inviteId, inviteToken, expiresAt);
|
|
5304
|
+
},
|
|
5305
|
+
revokeAgentInvite: (inviteId) => {
|
|
5306
|
+
if (connectionRef.current === "live") connRef.current?.revokeAgentInvite(inviteId);
|
|
5307
|
+
},
|
|
5308
|
+
sendAgentChat: (agentClientId, messageId, iv, ciphertext) => {
|
|
5309
|
+
if (connectionRef.current === "live") connRef.current?.sendAgentChat(agentClientId, messageId, iv, ciphertext);
|
|
5310
|
+
},
|
|
4902
5311
|
setProfile: (p) => {
|
|
4903
5312
|
if (connectionRef.current === "live") connRef.current?.setProfile(p);
|
|
4904
5313
|
},
|
|
@@ -4962,7 +5371,7 @@ function CollabEditor(opts) {
|
|
|
4962
5371
|
const session = useCollab(opts);
|
|
4963
5372
|
useEffect(() => {
|
|
4964
5373
|
opts.onSession?.(session);
|
|
4965
|
-
}, [session.version, session.ready, session.refused, session.roster, session.epochChanged, session.sessionWarning, session.writesBlocked, session.connection, session.persistErrors, session.storeSlow, session.notOwner, session.arrival, session.offline?.editsHeld, session.offline?.capped]);
|
|
5374
|
+
}, [session.version, session.ready, session.refused, session.roster, session.agentConnections, session.agentChat, session.agentInviteError, session.epochChanged, session.sessionWarning, session.writesBlocked, session.connection, session.persistErrors, session.storeSlow, session.notOwner, session.arrival, session.offline?.editsHeld, session.offline?.capped]);
|
|
4966
5375
|
const [api, setApi] = useState(null);
|
|
4967
5376
|
const pendingCaretRef = useRef(null);
|
|
4968
5377
|
const seenEpochRef = useRef(session.docEpoch);
|
|
@@ -5077,6 +5486,8 @@ function CollabEditor(opts) {
|
|
|
5077
5486
|
// and lost it, which is why the read-only banner could be on screen while
|
|
5078
5487
|
// the user's text appeared and then vanished.
|
|
5079
5488
|
editable: (opts.editable ?? true) && !session.writesBlocked,
|
|
5489
|
+
commentAuthor: opts.commentAuthor,
|
|
5490
|
+
commentMentions: opts.commentMentions,
|
|
5080
5491
|
revisions: opts.revisions,
|
|
5081
5492
|
// Re-key only on docEpoch (a document replacement), not on every change.
|
|
5082
5493
|
// Between replacements the live doc mutates in place and the key stays stable,
|
|
@@ -5090,7 +5501,8 @@ function CollabEditor(opts) {
|
|
|
5090
5501
|
createElement(DocxToolbar, {
|
|
5091
5502
|
api,
|
|
5092
5503
|
mode: opts.toolbarMode ?? "advanced",
|
|
5093
|
-
features: { ...COLLAB_TOOLBAR_DEFAULTS, ...opts.toolbarFeatures }
|
|
5504
|
+
features: { ...COLLAB_TOOLBAR_DEFAULTS, ...opts.toolbarFeatures },
|
|
5505
|
+
commentMentions: opts.commentMentions
|
|
5094
5506
|
}),
|
|
5095
5507
|
createElement("div", { style: { flex: 1, minHeight: 0 } }, view)
|
|
5096
5508
|
);
|