web-doc 0.7.0 → 0.8.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/THIRD_PARTY_NOTICES.md +10 -3
- package/dist/contracts.d.ts +6 -0
- package/dist/edit/ai/outline.d.ts +47 -0
- package/dist/edit/ai/outline.js +338 -0
- package/dist/edit/ai/targets.d.ts +16 -0
- package/dist/edit/ai/targets.js +309 -0
- package/dist/edit/ai/tools.d.ts +28 -0
- package/dist/edit/ai/tools.js +605 -0
- package/dist/edit/ai/types.d.ts +175 -0
- package/dist/edit/ai/types.js +1 -0
- package/dist/edit/docx/engine.d.ts +10 -4
- package/dist/edit/docx/engine.js +80 -6
- package/dist/edit/docx/model.d.ts +2 -0
- package/dist/edit/docx/model.js +11 -0
- package/dist/edit/docx/operations.d.ts +10 -0
- package/dist/edit/docx/provider.d.ts +4 -1
- package/dist/edit/docx/provider.js +3 -0
- package/dist/edit/docx/session.d.ts +12 -1
- package/dist/edit/docx/session.js +41 -0
- package/dist/edit/docx/structure-ops.js +39 -4
- package/dist/edit/docx/table-ops.js +26 -4
- package/dist/edit/docx/text-ops.d.ts +22 -0
- package/dist/edit/docx/text-ops.js +49 -16
- package/dist/edit/docx/text.js +21 -0
- package/dist/edit/docx/tracked.d.ts +52 -0
- package/dist/edit/docx/tracked.js +347 -0
- package/dist/edit/docx/types.d.ts +16 -1
- package/dist/edit/docx/write.d.ts +19 -5
- package/dist/edit/docx/write.js +31 -8
- package/dist/edit/engine.d.ts +14 -3
- package/dist/edit/history.d.ts +27 -3
- package/dist/edit/history.js +29 -7
- package/dist/edit/pdf/range-map.d.ts +4 -2
- package/dist/edit/pdf/session.d.ts +10 -0
- package/dist/edit/pdf/session.js +39 -0
- package/dist/edit/pptx/handler.js +10 -2
- package/dist/edit/pptx/session.d.ts +10 -0
- package/dist/edit/pptx/session.js +31 -0
- package/dist/edit/session.d.ts +11 -0
- package/dist/edit/session.js +269 -27
- package/dist/edit/types.d.ts +17 -3
- package/dist/edit/worker-engine.d.ts +2 -2
- package/dist/edit/worker-engine.js +2 -2
- package/dist/fuzzy-alignment.d.ts +11 -4
- package/dist/fuzzy-alignment.js +3 -9
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -2
- package/dist/limits.js +3 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/dist/workers/fuzzy-search-worker.js +1 -1
- package/dist/workers/ooxml-edit-worker.js +1400 -859
- package/dist/workers/pdf-edit-worker.js +4 -1
- package/package.json +1 -1
package/dist/edit/session.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { linkedAbortController } from "../abort.js";
|
|
2
2
|
import { abortError, ViewerError } from "../errors.js";
|
|
3
|
+
import { readDescription, readOutline } from "./ai/outline.js";
|
|
4
|
+
import { resolveTargets } from "./ai/targets.js";
|
|
5
|
+
import { buildToolSet, callTool as runTool } from "./ai/tools.js";
|
|
3
6
|
import { assetIdOf, AssetStore, binaryFields, isAssetReference, } from "./assets.js";
|
|
4
|
-
import { EditHistory } from "./history.js";
|
|
7
|
+
import { batchOf, EditHistory, modeOf } from "./history.js";
|
|
5
8
|
import { assertBatchSize, checkOperations, freezeOperations, invalidOperationError, parseReference, } from "./operations.js";
|
|
6
9
|
/**
|
|
7
10
|
* The format-independent editing session: validation, history, revisions,
|
|
@@ -17,15 +20,20 @@ export class EditSessionController {
|
|
|
17
20
|
#original;
|
|
18
21
|
#originalPageCount;
|
|
19
22
|
#ending = new AbortController();
|
|
20
|
-
sessionId =
|
|
23
|
+
sessionId = randomId();
|
|
21
24
|
/** Bytes of the last committed state; what the viewer shows and what a broken session saves. */
|
|
22
25
|
#committedBytes;
|
|
23
26
|
/** Materialized bytes of some committed states, by state id, so restores replay less. */
|
|
24
27
|
#checkpoints = new Map();
|
|
25
28
|
#checkpointBytes = 0;
|
|
29
|
+
/** Named checkpoints by id, in creation order. */
|
|
30
|
+
#named = new Map();
|
|
31
|
+
/** State ids named checkpoints pin, with how many name each; never evicted. */
|
|
32
|
+
#pinned = new Map();
|
|
26
33
|
/** Binary payloads of this session's batches, by content id. */
|
|
27
34
|
#assets = new AssetStore();
|
|
28
35
|
#state;
|
|
36
|
+
#tools;
|
|
29
37
|
#queue = Promise.resolve();
|
|
30
38
|
#revision = 0;
|
|
31
39
|
#savedStateId = 0;
|
|
@@ -46,6 +54,9 @@ export class EditSessionController {
|
|
|
46
54
|
get state() {
|
|
47
55
|
return this.#state;
|
|
48
56
|
}
|
|
57
|
+
get limits() {
|
|
58
|
+
return this.#host.limits;
|
|
59
|
+
}
|
|
49
60
|
applyJson(operations, options = {}) {
|
|
50
61
|
return this.apply(operations, options);
|
|
51
62
|
}
|
|
@@ -58,6 +69,9 @@ export class EditSessionController {
|
|
|
58
69
|
return this.#enqueue(options.signal, async (signal) => {
|
|
59
70
|
this.#assertRevision(options);
|
|
60
71
|
assertBatchSize(batch, this.#host.limits.maxEditOperations);
|
|
72
|
+
const modeIssues = checkChangeMode(this.format, options);
|
|
73
|
+
if (modeIssues.length > 0)
|
|
74
|
+
throw invalidOperationError(modeIssues);
|
|
61
75
|
const shapeIssues = checkOperations(batch, this.schemas);
|
|
62
76
|
if (shapeIssues.length > 0)
|
|
63
77
|
throw invalidOperationError(shapeIssues);
|
|
@@ -65,7 +79,8 @@ export class EditSessionController {
|
|
|
65
79
|
if (referenceIssues.length > 0)
|
|
66
80
|
throw invalidOperationError(referenceIssues);
|
|
67
81
|
const interned = await this.#intern(batch, signal);
|
|
68
|
-
const
|
|
82
|
+
const mode = modeOf(options);
|
|
83
|
+
const engineIssues = await this.#engine.validate(interned, signal, mode);
|
|
69
84
|
throwIfAborted(signal);
|
|
70
85
|
if (engineIssues.length > 0)
|
|
71
86
|
throw invalidOperationError(engineIssues);
|
|
@@ -74,6 +89,7 @@ export class EditSessionController {
|
|
|
74
89
|
const engineBatch = {
|
|
75
90
|
stateId: this.#history.nextStateId,
|
|
76
91
|
operations: interned,
|
|
92
|
+
...mode,
|
|
77
93
|
};
|
|
78
94
|
if (options.dryRun) {
|
|
79
95
|
const change = await this.#transaction(signal, "apply", async () => {
|
|
@@ -99,6 +115,7 @@ export class EditSessionController {
|
|
|
99
115
|
});
|
|
100
116
|
this.#history.push({
|
|
101
117
|
operations: interned,
|
|
118
|
+
...mode,
|
|
102
119
|
...(options.label === undefined ? {} : { label: options.label }),
|
|
103
120
|
createdIds: change.createdIds,
|
|
104
121
|
removedIds: change.removedIds,
|
|
@@ -230,6 +247,88 @@ export class EditSessionController {
|
|
|
230
247
|
const { signal: own, ...engineOptions } = options;
|
|
231
248
|
return this.#enqueue(own, async (signal) => this.#items(await this.#engine.findText(query, engineOptions, signal)));
|
|
232
249
|
}
|
|
250
|
+
getOutline(options) {
|
|
251
|
+
return readOutline(this, this.#host.limits, options);
|
|
252
|
+
}
|
|
253
|
+
describe(options) {
|
|
254
|
+
return readDescription(this, this.#host.limits, options);
|
|
255
|
+
}
|
|
256
|
+
resolveTargets(query, options) {
|
|
257
|
+
return resolveTargets(this, query, options);
|
|
258
|
+
}
|
|
259
|
+
createCheckpoint(label) {
|
|
260
|
+
return this.#enqueue(undefined, async () => {
|
|
261
|
+
const limit = this.#host.limits.maxEditCheckpoints;
|
|
262
|
+
if (this.#named.size >= limit)
|
|
263
|
+
throw new ViewerError("resource-limit", "Too many edit checkpoints; drop one first", { details: { limit } });
|
|
264
|
+
const stateId = this.#history.stateId;
|
|
265
|
+
const checkpoint = Object.freeze({
|
|
266
|
+
id: randomId(),
|
|
267
|
+
...(label === undefined ? {} : { label }),
|
|
268
|
+
revision: this.#revision,
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
});
|
|
271
|
+
const entries = this.#history.entriesAt(this.#history.position);
|
|
272
|
+
this.#named.set(checkpoint.id, {
|
|
273
|
+
checkpoint,
|
|
274
|
+
stateId,
|
|
275
|
+
pageCount: this.#history.pageCount,
|
|
276
|
+
entries,
|
|
277
|
+
batches: batchesFromOriginal(entries),
|
|
278
|
+
});
|
|
279
|
+
this.#pin(stateId, this.#committedBytes);
|
|
280
|
+
return checkpoint;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
listCheckpoints() {
|
|
284
|
+
return Object.freeze([...this.#named.values()].map((named) => named.checkpoint));
|
|
285
|
+
}
|
|
286
|
+
restoreCheckpoint(id, options = {}) {
|
|
287
|
+
return this.#enqueue(options.signal, async (signal) => {
|
|
288
|
+
this.#assertRevision(options);
|
|
289
|
+
const named = this.#named.get(id);
|
|
290
|
+
if (!named)
|
|
291
|
+
throw new ViewerError("invalid-operation", `Unknown edit checkpoint ${id}`, { details: { checkpointId: id } });
|
|
292
|
+
if (named.stateId === this.#history.stateId)
|
|
293
|
+
return this.#noop();
|
|
294
|
+
const before = this.#history.pageCount;
|
|
295
|
+
const changedPages = allPages(Math.max(before, named.pageCount));
|
|
296
|
+
const shown = await this.#transaction(signal, "apply", async () => {
|
|
297
|
+
await this.#engine.restore(this.#targetFor(named.entries), signal);
|
|
298
|
+
return this.#show(signal, changedPages);
|
|
299
|
+
});
|
|
300
|
+
const diff = entryDiff(this.#history.entriesAt(this.#history.position), named.entries);
|
|
301
|
+
this.#history.push({
|
|
302
|
+
operations: [],
|
|
303
|
+
createdIds: diff.created,
|
|
304
|
+
removedIds: diff.removed,
|
|
305
|
+
changedPages: shown.changedPages,
|
|
306
|
+
pageCountBefore: before,
|
|
307
|
+
pageCountAfter: shown.pageCount,
|
|
308
|
+
base: { stateId: named.stateId, batches: named.batches },
|
|
309
|
+
}, named.stateId);
|
|
310
|
+
this.#commit("restore", shown.changedPages, shown);
|
|
311
|
+
return this.#receipt(false, 0, diff.created, {
|
|
312
|
+
removedIds: diff.removed,
|
|
313
|
+
changedPages: shown.changedPages,
|
|
314
|
+
pageCount: shown.pageCount,
|
|
315
|
+
warnings: [],
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
dropCheckpoint(id) {
|
|
320
|
+
const named = this.#named.get(id);
|
|
321
|
+
if (!named)
|
|
322
|
+
return;
|
|
323
|
+
this.#named.delete(id);
|
|
324
|
+
this.#unpin(named.stateId);
|
|
325
|
+
}
|
|
326
|
+
get tools() {
|
|
327
|
+
return (this.#tools ??= buildToolSet(this.format, this.schemas));
|
|
328
|
+
}
|
|
329
|
+
callTool(call, options) {
|
|
330
|
+
return runTool(this, this.tools, call, options);
|
|
331
|
+
}
|
|
233
332
|
readItem(options, task) {
|
|
234
333
|
return this.#enqueue(options?.signal, async (signal) => Object.freeze({
|
|
235
334
|
sessionId: this.sessionId,
|
|
@@ -412,7 +511,7 @@ export class EditSessionController {
|
|
|
412
511
|
}
|
|
413
512
|
#commit(reason, changedPages, shown) {
|
|
414
513
|
this.#committedBytes = shown.bytes;
|
|
415
|
-
if (reason === "apply")
|
|
514
|
+
if (reason === "apply" || reason === "restore")
|
|
416
515
|
this.#keepCheckpoint(shown.bytes);
|
|
417
516
|
else if (reason === "reset")
|
|
418
517
|
this.#dropCheckpoints();
|
|
@@ -500,49 +599,107 @@ export class EditSessionController {
|
|
|
500
599
|
#keepCheckpoint(bytes) {
|
|
501
600
|
const stride = Math.max(1, Math.floor(this.#host.limits.maxEditHistory / 4));
|
|
502
601
|
const stateId = this.#history.stateId;
|
|
503
|
-
// Entries dropped by a new change after an undo can never be restored
|
|
504
|
-
|
|
602
|
+
// Entries dropped by a new change after an undo can never be restored;
|
|
603
|
+
// a pinned state stays whatever the history does.
|
|
604
|
+
const reachable = this.#reachable();
|
|
505
605
|
for (const [id, kept] of this.#checkpoints)
|
|
506
|
-
if (!reachable.has(id))
|
|
606
|
+
if (!reachable.has(id) && !this.#pinned.has(id))
|
|
507
607
|
this.#forgetCheckpoint(id, kept);
|
|
508
|
-
if (stateId
|
|
608
|
+
if (stateId === 0 ||
|
|
609
|
+
this.#checkpoints.has(stateId) ||
|
|
610
|
+
stateId % stride !== 0)
|
|
509
611
|
return;
|
|
612
|
+
this.#retain(stateId, bytes);
|
|
613
|
+
}
|
|
614
|
+
/** Stores a state's bytes when the budget, less the pinned states, can hold them. */
|
|
615
|
+
#retain(stateId, bytes) {
|
|
510
616
|
const budget = this.#host.limits.maxEditCheckpointBytes;
|
|
511
617
|
if (bytes.byteLength > budget)
|
|
512
618
|
return;
|
|
513
|
-
const
|
|
619
|
+
const evictable = [...this.#checkpoints.keys()]
|
|
620
|
+
.filter((id) => !this.#pinned.has(id))
|
|
621
|
+
.sort((a, b) => a - b);
|
|
514
622
|
while (this.#checkpointBytes + bytes.byteLength > budget &&
|
|
515
|
-
|
|
516
|
-
const id =
|
|
623
|
+
evictable.length > 0) {
|
|
624
|
+
const id = evictable.shift();
|
|
517
625
|
this.#forgetCheckpoint(id, this.#checkpoints.get(id));
|
|
518
626
|
}
|
|
627
|
+
if (this.#checkpointBytes + bytes.byteLength > budget)
|
|
628
|
+
return;
|
|
519
629
|
this.#checkpoints.set(stateId, bytes);
|
|
520
630
|
this.#checkpointBytes += bytes.byteLength;
|
|
521
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Pins a state for a named checkpoint: its bytes are kept while any
|
|
634
|
+
* checkpoint names it, or rebuilt by replay when the budget cannot hold
|
|
635
|
+
* them. The original (state 0) needs no bytes.
|
|
636
|
+
*/
|
|
637
|
+
#pin(stateId, bytes) {
|
|
638
|
+
if (stateId === 0)
|
|
639
|
+
return;
|
|
640
|
+
this.#pinned.set(stateId, (this.#pinned.get(stateId) ?? 0) + 1);
|
|
641
|
+
if (!this.#checkpoints.has(stateId))
|
|
642
|
+
this.#retain(stateId, bytes);
|
|
643
|
+
}
|
|
644
|
+
#unpin(stateId) {
|
|
645
|
+
const count = this.#pinned.get(stateId);
|
|
646
|
+
if (!count)
|
|
647
|
+
return;
|
|
648
|
+
if (count > 1) {
|
|
649
|
+
this.#pinned.set(stateId, count - 1);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
this.#pinned.delete(stateId);
|
|
653
|
+
const kept = this.#checkpoints.get(stateId);
|
|
654
|
+
if (kept && !this.#reachable().has(stateId))
|
|
655
|
+
this.#forgetCheckpoint(stateId, kept);
|
|
656
|
+
}
|
|
657
|
+
/** State ids a replay may start from: every entry's, and the base of every restore. */
|
|
658
|
+
#reachable() {
|
|
659
|
+
const reachable = new Set();
|
|
660
|
+
for (const entry of this.#history.allEntries) {
|
|
661
|
+
reachable.add(entry.stateId);
|
|
662
|
+
if (entry.base)
|
|
663
|
+
reachable.add(entry.base.stateId);
|
|
664
|
+
}
|
|
665
|
+
return reachable;
|
|
666
|
+
}
|
|
522
667
|
#forgetCheckpoint(id, bytes) {
|
|
523
668
|
this.#checkpoints.delete(id);
|
|
524
669
|
this.#checkpointBytes -= bytes.byteLength;
|
|
525
670
|
}
|
|
671
|
+
/** After a reset nothing in the history is reachable; pinned states stay. */
|
|
526
672
|
#dropCheckpoints() {
|
|
527
|
-
this.#checkpoints
|
|
528
|
-
|
|
673
|
+
for (const [id, kept] of this.#checkpoints)
|
|
674
|
+
if (!this.#pinned.has(id))
|
|
675
|
+
this.#forgetCheckpoint(id, kept);
|
|
529
676
|
}
|
|
530
677
|
/**
|
|
531
678
|
* The cheapest way to rebuild the state at `position`: the newest
|
|
532
|
-
*
|
|
679
|
+
* retained bytes at or before it, plus the batches after them.
|
|
533
680
|
*/
|
|
534
681
|
#restoreTarget(position = this.#history.position) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
682
|
+
return this.#targetFor(this.#history.entriesAt(position));
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Walks `entries` from the end: the first with retained bytes is the base;
|
|
686
|
+
* a restore entry without them starts from the original plus the batches
|
|
687
|
+
* that built its checkpoint.
|
|
688
|
+
*/
|
|
689
|
+
#targetFor(entries) {
|
|
690
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
691
|
+
const entry = entries[index];
|
|
692
|
+
const after = () => batchesOf(entries.slice(index + 1));
|
|
693
|
+
const retained = this.#checkpoints.get(entry.stateId);
|
|
694
|
+
if (retained)
|
|
695
|
+
return { base: retained, batches: after() };
|
|
696
|
+
// A restore entry carries its checkpoint's state id, so its bytes
|
|
697
|
+
// were just looked up; without them the checkpoint's batches rebuild
|
|
698
|
+
// it from the original.
|
|
699
|
+
if (entry.base)
|
|
700
|
+
return { batches: [...entry.base.batches, ...after()] };
|
|
701
|
+
}
|
|
702
|
+
return { batches: batchesOf(entries) };
|
|
546
703
|
}
|
|
547
704
|
#stateToken(stateId) {
|
|
548
705
|
return `${this.sessionId}:${stateId}`;
|
|
@@ -667,6 +824,43 @@ function checkBatchReferences(operations) {
|
|
|
667
824
|
});
|
|
668
825
|
return issues;
|
|
669
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Tracked changes exist in DOCX only and always name their author (decision
|
|
829
|
+
* 8 of the ai-edit module): the batch is refused before any engine work.
|
|
830
|
+
*/
|
|
831
|
+
function checkChangeMode(format, options) {
|
|
832
|
+
const issues = [];
|
|
833
|
+
// A timestamp is written into the file where a format records one, so
|
|
834
|
+
// it must be a date-time the file can hold whatever the mode.
|
|
835
|
+
if (options.timestamp !== undefined &&
|
|
836
|
+
(!DATE_TIME.test(options.timestamp) ||
|
|
837
|
+
Number.isNaN(Date.parse(options.timestamp))))
|
|
838
|
+
issues.push({
|
|
839
|
+
operationIndex: -1,
|
|
840
|
+
path: "/timestamp",
|
|
841
|
+
code: "invalid-value",
|
|
842
|
+
message: "The timestamp must be an ISO 8601 date-time",
|
|
843
|
+
});
|
|
844
|
+
if (options.changeMode !== "tracked")
|
|
845
|
+
return issues;
|
|
846
|
+
if (format !== "docx")
|
|
847
|
+
issues.push({
|
|
848
|
+
operationIndex: -1,
|
|
849
|
+
path: "",
|
|
850
|
+
code: "unsupported-change-mode",
|
|
851
|
+
message: `${format.toUpperCase()} has no tracked changes; apply directly and review with checkpoints`,
|
|
852
|
+
});
|
|
853
|
+
else if (!options.author || options.author.trim().length === 0)
|
|
854
|
+
issues.push({
|
|
855
|
+
operationIndex: -1,
|
|
856
|
+
path: "/author",
|
|
857
|
+
code: "required",
|
|
858
|
+
message: "Tracked changes name their author; pass ApplyOptions.author",
|
|
859
|
+
});
|
|
860
|
+
return issues;
|
|
861
|
+
}
|
|
862
|
+
/** ISO 8601 as `xsd:dateTime` takes it. */
|
|
863
|
+
const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
670
864
|
/** The renderer owns the page count; an engine that disagrees is reported, not trusted. */
|
|
671
865
|
function pageCountWarning(change, shown) {
|
|
672
866
|
if (change.pageCount === undefined || change.pageCount === shown.pageCount)
|
|
@@ -679,8 +873,56 @@ function pageCountWarning(change, shown) {
|
|
|
679
873
|
},
|
|
680
874
|
];
|
|
681
875
|
}
|
|
682
|
-
/**
|
|
683
|
-
function
|
|
876
|
+
/** The engine batches of entries that carry operations; restore entries carry none. */
|
|
877
|
+
function batchesOf(entries) {
|
|
878
|
+
return entries.filter((entry) => entry.operations.length > 0).map(batchOf);
|
|
879
|
+
}
|
|
880
|
+
/** The batches from the original to the state after `entries`, through the last restore. */
|
|
881
|
+
function batchesFromOriginal(entries) {
|
|
882
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
883
|
+
const base = entries[index].base;
|
|
884
|
+
if (base)
|
|
885
|
+
return [...base.batches, ...batchesOf(entries.slice(index + 1))];
|
|
886
|
+
}
|
|
887
|
+
return batchesOf(entries);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* The ids a move from the state after `current` to the state after `target`
|
|
891
|
+
* removes and creates: the entries past the common prefix are undone in
|
|
892
|
+
* reverse, then the target's are redone, each id netted out.
|
|
893
|
+
*/
|
|
894
|
+
function entryDiff(current, target) {
|
|
895
|
+
let common = 0;
|
|
896
|
+
while (common < current.length &&
|
|
897
|
+
common < target.length &&
|
|
898
|
+
current[common].stateId === target[common].stateId)
|
|
899
|
+
common += 1;
|
|
900
|
+
const created = new Set();
|
|
901
|
+
const removed = new Set();
|
|
902
|
+
const create = (id) => {
|
|
903
|
+
if (removed.has(id))
|
|
904
|
+
removed.delete(id);
|
|
905
|
+
else
|
|
906
|
+
created.add(id);
|
|
907
|
+
};
|
|
908
|
+
const remove = (id) => {
|
|
909
|
+
if (created.has(id))
|
|
910
|
+
created.delete(id);
|
|
911
|
+
else
|
|
912
|
+
removed.add(id);
|
|
913
|
+
};
|
|
914
|
+
for (const entry of current.slice(common).reverse()) {
|
|
915
|
+
entry.createdIds.forEach(remove);
|
|
916
|
+
entry.removedIds.forEach(create);
|
|
917
|
+
}
|
|
918
|
+
for (const entry of target.slice(common)) {
|
|
919
|
+
entry.removedIds.forEach(remove);
|
|
920
|
+
entry.createdIds.forEach(create);
|
|
921
|
+
}
|
|
922
|
+
return { created: [...created], removed: [...removed] };
|
|
923
|
+
}
|
|
924
|
+
/** 128 random bits as 22 URL-safe base64 characters; unique across sessions and reloads. */
|
|
925
|
+
function randomId() {
|
|
684
926
|
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
685
927
|
let binary = "";
|
|
686
928
|
for (const byte of bytes)
|
package/dist/edit/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ViewerWarning } from "../contracts.js";
|
|
2
|
+
import type { EditSessionReads } from "./ai/types.js";
|
|
2
3
|
/** Formats an edit session can be started for. */
|
|
3
4
|
export type EditableFormat = "pdf" | "pptx" | "docx";
|
|
4
5
|
export interface EditOptions {
|
|
@@ -163,16 +164,27 @@ export interface EditState {
|
|
|
163
164
|
readonly canRedo: boolean;
|
|
164
165
|
readonly pageCount: number;
|
|
165
166
|
}
|
|
167
|
+
/** How a batch is written: in place, or as tracked changes (DOCX) for a person to accept or reject. */
|
|
168
|
+
export type ChangeMode = "direct" | "tracked";
|
|
166
169
|
export interface ApplyOptions {
|
|
167
170
|
/** Rejects the call with `edit-conflict` unless `state.revision` equals this value. */
|
|
168
171
|
readonly expectedRevision?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Default `direct`. `tracked` writes Word revisions (`w:ins`, `w:del`,
|
|
174
|
+
* `w:rPrChange`, `w:pPrChange`) with `author` and `timestamp`; refused
|
|
175
|
+
* with `unsupported-change-mode` by formats and operations without a
|
|
176
|
+
* tracked form, and with `required` at `/author` without an author.
|
|
177
|
+
*/
|
|
178
|
+
readonly changeMode?: ChangeMode;
|
|
179
|
+
/** The `w:author` of tracked changes; required with `changeMode: "tracked"`. */
|
|
180
|
+
readonly author?: string;
|
|
169
181
|
/** Rejects with `edit-conflict` unless it equals `sessionId`; for callers that outlive a session. */
|
|
170
182
|
readonly expectedSessionId?: string;
|
|
171
183
|
/** Validates and simulates the batch without changing anything. */
|
|
172
184
|
readonly dryRun?: boolean;
|
|
173
185
|
/** Free text stored with the history entry, for host UIs and audit logs. */
|
|
174
186
|
readonly label?: string;
|
|
175
|
-
/**
|
|
187
|
+
/** ISO 8601 time a format records with the change (the `w:date` of a tracked change); never generated by web-doc. */
|
|
176
188
|
readonly timestamp?: string;
|
|
177
189
|
readonly signal?: AbortSignal;
|
|
178
190
|
}
|
|
@@ -228,7 +240,7 @@ export interface OperationIssue {
|
|
|
228
240
|
readonly code: string;
|
|
229
241
|
readonly message: string;
|
|
230
242
|
}
|
|
231
|
-
export interface EditSessionBase<TOperation extends EditOperation, TElement extends EditElement> {
|
|
243
|
+
export interface EditSessionBase<TOperation extends EditOperation, TElement extends EditElement> extends EditSessionReads {
|
|
232
244
|
readonly format: EditableFormat;
|
|
233
245
|
/** Unique per session; stamped on state, receipts, read results and events. */
|
|
234
246
|
readonly sessionId: string;
|
|
@@ -258,7 +270,9 @@ export interface EditStateChange extends EditState {
|
|
|
258
270
|
readonly active: boolean;
|
|
259
271
|
readonly format?: EditableFormat;
|
|
260
272
|
}
|
|
261
|
-
export type DocumentChangeReason = "apply" | "undo" | "redo" | "reset"
|
|
273
|
+
export type DocumentChangeReason = "apply" | "undo" | "redo" | "reset"
|
|
274
|
+
/** `restoreCheckpoint()`: the content of a named checkpoint, as one history entry. */
|
|
275
|
+
| "restore";
|
|
262
276
|
export interface DocumentChange {
|
|
263
277
|
readonly sessionId: string;
|
|
264
278
|
readonly revision: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkerRpcClient } from "../worker-client.js";
|
|
2
2
|
import type { EditWorkerOperation } from "../worker-protocol.js";
|
|
3
|
-
import type { EditEngine, EditEngineContext, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "./engine.js";
|
|
3
|
+
import type { EditEngine, EditEngineContext, BatchMode, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "./engine.js";
|
|
4
4
|
import type { EditElement, EditFindOptions, EditOperation, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, TextTarget } from "./types.js";
|
|
5
5
|
export declare abstract class WorkerEngineClient implements EditEngine {
|
|
6
6
|
#private;
|
|
@@ -8,7 +8,7 @@ export declare abstract class WorkerEngineClient implements EditEngine {
|
|
|
8
8
|
protected readonly rpc: WorkerRpcClient;
|
|
9
9
|
protected readonly context: EditEngineContext;
|
|
10
10
|
constructor(rpc: WorkerRpcClient, context: EditEngineContext);
|
|
11
|
-
validate(operations: readonly EditOperation[], signal: AbortSignal): Promise<readonly OperationIssue[]>;
|
|
11
|
+
validate(operations: readonly EditOperation[], signal: AbortSignal, mode?: BatchMode): Promise<readonly OperationIssue[]>;
|
|
12
12
|
/** Plain operation arrays, as the unit tests pass them, become the next batch. */
|
|
13
13
|
apply(input: EngineBatch | readonly EditOperation[], signal: AbortSignal): Promise<EngineChange>;
|
|
14
14
|
/** A bare signal, as the unit tests pass it, asks for the shown form. */
|
|
@@ -12,8 +12,8 @@ export class WorkerEngineClient {
|
|
|
12
12
|
this.rpc = rpc;
|
|
13
13
|
this.context = context;
|
|
14
14
|
}
|
|
15
|
-
validate(operations, signal) {
|
|
16
|
-
return this.request("edit-validate", { operations }, signal);
|
|
15
|
+
validate(operations, signal, mode = {}) {
|
|
16
|
+
return this.request("edit-validate", { operations, mode }, signal);
|
|
17
17
|
}
|
|
18
18
|
/** Plain operation arrays, as the unit tests pass them, become the next batch. */
|
|
19
19
|
apply(input, signal) {
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* than a page × query traceback matrix. Equal-cost occurrences prefer the
|
|
7
7
|
* earliest end, so an unmatched character after a passage cannot extend it.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/** The best contiguous alignment of a query in a text, with its edit cost. */
|
|
10
|
+
export interface FuzzyPassage {
|
|
11
|
+
/** Offsets into the original text, in UTF-16 code units. */
|
|
12
|
+
readonly start: number;
|
|
13
|
+
readonly end: number;
|
|
14
|
+
/** Edits between the query and the passage; `cost / length` is the score Fuse bounds. */
|
|
15
|
+
readonly cost: number;
|
|
16
|
+
/** The normalized query's length. */
|
|
17
|
+
readonly length: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function alignFuzzyPassage(text: string, query: string, caseSensitive: boolean, maxScore: number): FuzzyPassage | undefined;
|
package/dist/fuzzy-alignment.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { normalizeSearchText, normalizeWithMap } from "./search-text.js";
|
|
2
|
-
/**
|
|
3
|
-
* Fuse's indices describe character masks, not a contiguous edit alignment.
|
|
4
|
-
* Refine an accepted page with semi-global Levenshtein alignment: consume the
|
|
5
|
-
* whole query, allowing free text before and after one occurrence. Tracking
|
|
6
|
-
* the start alongside each cost needs O(query length) working memory rather
|
|
7
|
-
* than a page × query traceback matrix. Equal-cost occurrences prefer the
|
|
8
|
-
* earliest end, so an unmatched character after a passage cannot extend it.
|
|
9
|
-
*/
|
|
10
2
|
export function alignFuzzyPassage(text, query, caseSensitive, maxScore) {
|
|
11
3
|
const source = normalizeWithMap(text, caseSensitive);
|
|
12
4
|
const pattern = normalizeSearchText(query, caseSensitive);
|
|
@@ -59,5 +51,7 @@ export function alignFuzzyPassage(text, query, caseSensitive, maxScore) {
|
|
|
59
51
|
return undefined;
|
|
60
52
|
const start = source.starts[bestStart];
|
|
61
53
|
const end = source.ends[bestEnd - 1];
|
|
62
|
-
return start === undefined || end === undefined
|
|
54
|
+
return start === undefined || end === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: { start, end, cost: bestCost, length };
|
|
63
57
|
}
|
package/dist/headless.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export * from "./edit/pptx/types.js";
|
|
|
6
6
|
export * from "./client.js";
|
|
7
7
|
export * from "./viewer.js";
|
|
8
8
|
export * from "./detect.js";
|
|
9
|
-
export
|
|
9
|
+
export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
|
|
10
10
|
export * from "./format.js";
|
|
11
11
|
export * from "./limits.js";
|
|
12
12
|
export * from "./interaction.js";
|
package/dist/headless.js
CHANGED
|
@@ -6,7 +6,10 @@ export * from "./edit/pptx/types.js";
|
|
|
6
6
|
export * from "./client.js";
|
|
7
7
|
export * from "./viewer.js";
|
|
8
8
|
export * from "./detect.js";
|
|
9
|
-
|
|
9
|
+
// Named on purpose: a bundler that inlines the lazily loaded edit engines
|
|
10
|
+
// (esbuild without code splitting) initialises this module lazily, and a
|
|
11
|
+
// star re-export would then hand a consumer an undefined class.
|
|
12
|
+
export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
|
|
10
13
|
export * from "./format.js";
|
|
11
14
|
export * from "./limits.js";
|
|
12
15
|
export * from "./interaction.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export * from "./contracts.js";
|
|
2
2
|
export * from "./edit/types.js";
|
|
3
|
+
export * from "./edit/ai/types.js";
|
|
4
|
+
export { describeReceipt } from "./edit/ai/tools.js";
|
|
3
5
|
export * from "./edit/sessions.js";
|
|
4
6
|
export * from "./edit/pdf/types.js";
|
|
5
7
|
export * from "./edit/pptx/types.js";
|
|
6
8
|
export * from "./edit/docx/types.js";
|
|
7
9
|
export * from "./client.js";
|
|
8
10
|
export * from "./detect.js";
|
|
9
|
-
export
|
|
11
|
+
export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
|
|
10
12
|
export * from "./format.js";
|
|
11
13
|
export * from "./limits.js";
|
|
12
14
|
export * from "./interaction.js";
|
|
@@ -23,7 +25,8 @@ export * from "./ui.js";
|
|
|
23
25
|
export * from "./ui-styles.js";
|
|
24
26
|
export * from "./registry.js";
|
|
25
27
|
export * from "./viewer.js";
|
|
26
|
-
export
|
|
28
|
+
export { WorkerRpcClient } from "./worker-client.js";
|
|
29
|
+
export type { WorkerLike, WorkerRequestOptions } from "./worker-client.js";
|
|
27
30
|
export * from "./worker-adapter.js";
|
|
28
31
|
export * from "./worker-endpoint.js";
|
|
29
32
|
export * from "./worker-protocol.js";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export * from "./contracts.js";
|
|
2
2
|
export * from "./edit/types.js";
|
|
3
|
+
export * from "./edit/ai/types.js";
|
|
4
|
+
export { describeReceipt } from "./edit/ai/tools.js";
|
|
3
5
|
export * from "./edit/sessions.js";
|
|
4
6
|
export * from "./edit/pdf/types.js";
|
|
5
7
|
export * from "./edit/pptx/types.js";
|
|
6
8
|
export * from "./edit/docx/types.js";
|
|
7
9
|
export * from "./client.js";
|
|
8
10
|
export * from "./detect.js";
|
|
9
|
-
|
|
11
|
+
// Named like the worker client below: the error class is reached from the
|
|
12
|
+
// lazily loaded engines too and must not come out undefined in a consumer
|
|
13
|
+
// bundle without code splitting.
|
|
14
|
+
export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
|
|
10
15
|
export * from "./format.js";
|
|
11
16
|
export * from "./limits.js";
|
|
12
17
|
export * from "./interaction.js";
|
|
@@ -23,7 +28,11 @@ export * from "./ui.js";
|
|
|
23
28
|
export * from "./ui-styles.js";
|
|
24
29
|
export * from "./registry.js";
|
|
25
30
|
export * from "./viewer.js";
|
|
26
|
-
|
|
31
|
+
// Named on purpose: a bundler that inlines the lazily loaded edit engines
|
|
32
|
+
// (esbuild without code splitting) initialises this module lazily, and a
|
|
33
|
+
// star re-export would then hand a consumer an undefined class
|
|
34
|
+
// (`scripts/consumer-bundle.test.mjs` guards every class and constant).
|
|
35
|
+
export { WorkerRpcClient } from "./worker-client.js";
|
|
27
36
|
export * from "./worker-adapter.js";
|
|
28
37
|
export * from "./worker-endpoint.js";
|
|
29
38
|
export * from "./worker-protocol.js";
|
package/dist/limits.js
CHANGED
|
@@ -13,6 +13,9 @@ export const defaultResourceLimits = Object.freeze({
|
|
|
13
13
|
maxEditOperations: 500,
|
|
14
14
|
maxEditHistory: 200,
|
|
15
15
|
maxEditCheckpointBytes: 64 * 1024 * 1024,
|
|
16
|
+
maxOutlineNodes: 5000,
|
|
17
|
+
maxDescribeChars: 200_000,
|
|
18
|
+
maxEditCheckpoints: 20,
|
|
16
19
|
});
|
|
17
20
|
export function resolveLimits(base = {}, override = {}) {
|
|
18
21
|
const result = { ...defaultResourceLimits, ...base, ...override };
|
|
@@ -2,7 +2,7 @@ import type { DocumentFormat, DocumentInfo, ResourceLimits, TextRun, ViewerError
|
|
|
2
2
|
import type { EditableFormat } from "./edit/types.js";
|
|
3
3
|
export type DocumentWorkerOperation = "init" | "open" | "get-info" | "render" | "get-text-map" | "close" | "destroy";
|
|
4
4
|
/** Operations of an edit engine worker; payloads mirror the engine interface. */
|
|
5
|
-
export type EditWorkerOperation = "edit-init" | "edit-open" | "edit-validate" | "edit-apply" | "edit-materialize" | "edit-restore" | "edit-put-asset" | "edit-elements" | "edit-element" | "edit-elements-at" | "edit-find-text" | "edit-text-layout" | "edit-position-at" | "edit-range-rects" | "edit-render-without" | "edit-page-layout" | "edit-pptx-slides" | "edit-pptx-layouts" | "edit-dispose";
|
|
5
|
+
export type EditWorkerOperation = "edit-init" | "edit-open" | "edit-validate" | "edit-apply" | "edit-materialize" | "edit-restore" | "edit-put-asset" | "edit-elements" | "edit-element" | "edit-elements-at" | "edit-find-text" | "edit-text-layout" | "edit-position-at" | "edit-range-rects" | "edit-render-without" | "edit-page-layout" | "edit-pptx-slides" | "edit-pptx-layouts" | "edit-docx-revisions" | "edit-dispose";
|
|
6
6
|
export type WorkerOperation = DocumentWorkerOperation | EditWorkerOperation;
|
|
7
7
|
export interface WorkerRequest {
|
|
8
8
|
readonly kind: "request";
|
|
@@ -1634,7 +1634,7 @@ function alignFuzzyPassage(text, query, caseSensitive, maxScore) {
|
|
|
1634
1634
|
if (bestEnd <= bestStart || bestCost / length > maxScore) return void 0;
|
|
1635
1635
|
const start = source.starts[bestStart];
|
|
1636
1636
|
const end = source.ends[bestEnd - 1];
|
|
1637
|
-
return start === void 0 || end === void 0 ? void 0 : { start, end };
|
|
1637
|
+
return start === void 0 || end === void 0 ? void 0 : { start, end, cost: bestCost, length };
|
|
1638
1638
|
}
|
|
1639
1639
|
|
|
1640
1640
|
// src/fuzzy-search.ts
|