@stll/folio-core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +37 -4
  6. package/dist/ai-edits/headless.js +134 -16
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -137
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +139 -0
  19. package/dist/document-operations.js +578 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.js +2 -1
  22. package/dist/docx/encryption/compoundFile.js +4 -2
  23. package/dist/docx/footnoteParser.d.ts +1 -17
  24. package/dist/docx/footnoteParser.js +1 -27
  25. package/dist/docx/groupDrawingParser.d.ts +8 -0
  26. package/dist/docx/groupDrawingParser.js +131 -0
  27. package/dist/docx/numberingParser.d.ts +3 -1
  28. package/dist/docx/numberingParser.js +8 -2
  29. package/dist/docx/paragraphParser.js +9 -3
  30. package/dist/docx/runParser.js +8 -1
  31. package/dist/docx/selectiveXmlPatch.js +4 -10
  32. package/dist/docx/serializer/numberingSerializer.js +3 -1
  33. package/dist/docx/settingsParser.d.ts +6 -3
  34. package/dist/docx/settingsParser.js +2 -0
  35. package/dist/docx/xmlParser.js +11 -4
  36. package/dist/i18n/messages/catalogs.gen.d.ts +1054 -34
  37. package/dist/i18n/messages/catalogs.gen.js +1122 -68
  38. package/dist/i18n/messages/messages.gen.d.ts +62 -2
  39. package/dist/index.d.ts +3 -2
  40. package/dist/index.js +6 -5
  41. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  42. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  43. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
  45. package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
  46. package/dist/layout-engine/index.d.ts +6 -5
  47. package/dist/layout-engine/index.js +143 -30
  48. package/dist/layout-engine/keep-together.d.ts +13 -8
  49. package/dist/layout-engine/keep-together.js +40 -25
  50. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  51. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  52. package/dist/layout-engine/measure/measureBlocks.js +50 -18
  53. package/dist/layout-engine/measure/measureParagraph.js +61 -15
  54. package/dist/layout-engine/paginator.d.ts +2 -1
  55. package/dist/layout-engine/paginator.js +9 -3
  56. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  57. package/dist/layout-engine/textBoxGroup.js +8 -0
  58. package/dist/layout-engine/types.d.ts +30 -9
  59. package/dist/layout-engine/types.js +20 -2
  60. package/dist/layout-painter/renderPage.d.ts +15 -1
  61. package/dist/layout-painter/renderPage.js +27 -11
  62. package/dist/layout-painter/renderParagraph.js +18 -8
  63. package/dist/layout-painter/renderTable.js +10 -7
  64. package/dist/managers/DocumentLoaderManager.js +12 -5
  65. package/dist/model.js +1 -1
  66. package/dist/paged-layout/pageText.d.ts +14 -0
  67. package/dist/paged-layout/pageText.js +21 -0
  68. package/dist/prosemirror/attrs/index.js +2 -0
  69. package/dist/prosemirror/commands/comments.js +75 -5
  70. package/dist/prosemirror/commands/hyperlink.js +4 -2
  71. package/dist/prosemirror/commands/image.js +2 -1
  72. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  73. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  74. package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
  75. package/dist/prosemirror/conversion/toProseDoc.js +29 -8
  76. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  77. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  78. package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
  79. package/dist/prosemirror/findReplaceSelection.js +2 -1
  80. package/dist/prosemirror/schema/nodes.d.ts +10 -5
  81. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  82. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  83. package/dist/redline.d.ts +20 -0
  84. package/dist/redline.js +127 -0
  85. package/dist/server.d.ts +7 -3
  86. package/dist/server.js +6 -2
  87. package/dist/symbols.d.ts +35 -0
  88. package/dist/symbols.js +512 -0
  89. package/dist/utils/clipboard.js +3 -3
  90. package/dist/utils/fontLoader.js +3 -0
  91. package/dist/utils/fontResolver.js +48 -1
  92. package/dist/utils/zoom.d.ts +21 -0
  93. package/dist/utils/zoom.js +21 -0
  94. package/dist/version-comparison.d.ts +92 -0
  95. package/dist/version-comparison.js +436 -0
  96. package/package.json +3 -3
  97. package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
  98. package/dist/paged-layout/headerFooterMargins.js +0 -122
@@ -23,13 +23,7 @@ type ApplyFolioAIEditOperationsOptions = {
23
23
  author?: string;
24
24
  createCommentId?: (text: string) => number;
25
25
  };
26
- declare const applyFolioAIEditOperations: ({
27
- view,
28
- snapshot,
29
- operations,
30
- mode,
31
- author,
32
- createCommentId
33
- }: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyResult;
26
+ declare const applyFolioAIEditOperations: (options: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyResult;
27
+ declare const previewFolioAIEditOperations: (options: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyResult;
34
28
  //#endregion
35
- export { FolioAIEditView, applyFolioAIEditOperations };
29
+ export { FolioAIEditView, applyFolioAIEditOperations, previewFolioAIEditOperations };
@@ -193,7 +193,7 @@ const buildEmphasisInlineContent = (schema, text, baseMarks) => {
193
193
  }
194
194
  return nodes.length > 0 ? nodes : [schema.text(text, [...baseMarks])];
195
195
  };
196
- const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", createCommentId }) => {
196
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", createCommentId, revisionIdSeed }) => {
197
197
  const applied = [];
198
198
  const skipped = [];
199
199
  const resolved = [];
@@ -210,6 +210,13 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
210
210
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
211
211
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
212
212
  for (const [index, operation] of operations.entries()) {
213
+ if (mode === "tracked-changes" && operation.type === "formatRange") {
214
+ skipped.push({
215
+ id: operation.id,
216
+ reason: "unsupportedMode"
217
+ });
218
+ continue;
219
+ }
213
220
  const commentText = getOperationCommentText(operation);
214
221
  if (commentText !== void 0 && (!commentType || createCommentId === void 0)) {
215
222
  skipped.push({
@@ -244,7 +251,7 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
244
251
  skipped
245
252
  };
246
253
  let tr = view.state.tr;
247
- let revisionSeed = nextRevisionSeed(resolved.length);
254
+ let revisionSeed = revisionIdSeed ?? nextRevisionSeed(resolved.length);
248
255
  const date = (/* @__PURE__ */ new Date()).toISOString();
249
256
  for (const item of resolved.toSorted((left, right) => {
250
257
  if (left.from !== right.from) return right.from - left.from;
@@ -254,7 +261,8 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
254
261
  const stepsBefore = tr.steps.length;
255
262
  let appliedRevisionIds;
256
263
  switch (item.operation.type) {
257
- case "replaceInBlock": {
264
+ case "replaceInBlock":
265
+ case "replaceRange": {
258
266
  const revisionIdDelete = revisionSeed++;
259
267
  const revisionIdInsert = revisionSeed++;
260
268
  tr = applyTextReplacement({
@@ -270,6 +278,17 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
270
278
  if (mode === "tracked-changes") appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
271
279
  break;
272
280
  }
281
+ case "commentOnRange":
282
+ if (commentMark) tr = tr.addMark(item.from, item.to, commentMark);
283
+ break;
284
+ case "formatRange":
285
+ for (const [name, enabled] of Object.entries(item.operation.formatting)) {
286
+ const markType = view.state.schema.marks[name];
287
+ if (!markType) continue;
288
+ if (enabled) tr = tr.addMark(item.from, item.to, name === "underline" ? markType.create({ style: "single" }) : markType.create());
289
+ else tr = tr.removeMark(item.from, item.to, markType);
290
+ }
291
+ break;
273
292
  case "replaceBlock": {
274
293
  const revisionIdDelete = revisionSeed++;
275
294
  const revisionIdInsert = revisionSeed++;
@@ -314,7 +333,7 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
314
333
  if (mode === "tracked-changes" && item.operation.pageBreakBefore === true && (!item.insertText || item.insertText.length === 0)) {
315
334
  skipped.push({
316
335
  id: item.operation.id,
317
- reason: "unsupportedBlock"
336
+ reason: "unsupportedMode"
318
337
  });
319
338
  continue;
320
339
  }
@@ -338,6 +357,7 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
338
357
  attrs["listMarker"] = null;
339
358
  attrs["listMarkerHidden"] = null;
340
359
  attrs["listLevelNumFmts"] = null;
360
+ attrs["listLevelStarts"] = null;
341
361
  attrs["listAbstractNumId"] = null;
342
362
  attrs["listStartOverride"] = null;
343
363
  }
@@ -350,7 +370,7 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
350
370
  if (mode === "tracked-changes") {
351
371
  skipped.push({
352
372
  id: item.operation.id,
353
- reason: "unsupportedBlock"
373
+ reason: "unsupportedMode"
354
374
  });
355
375
  continue;
356
376
  }
@@ -411,15 +431,36 @@ const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracke
411
431
  skipped
412
432
  };
413
433
  };
434
+ const applyFolioAIEditOperations = (options) => applyFolioAIEditOperationsInternal(options);
435
+ const previewFolioAIEditOperations = (options) => {
436
+ const { view, createCommentId, ...applyOptions } = options;
437
+ let previewCommentId = -1;
438
+ const previewView = {
439
+ state: view.state,
440
+ dispatch: (transaction) => {
441
+ previewView.state = previewView.state.apply(transaction);
442
+ }
443
+ };
444
+ const result = applyFolioAIEditOperationsInternal({
445
+ ...applyOptions,
446
+ view: previewView,
447
+ ...createCommentId !== void 0 && { createCommentId: () => previewCommentId-- },
448
+ revisionIdSeed: -1e9
449
+ });
450
+ return {
451
+ applied: result.applied.map(({ id }) => ({ id })),
452
+ skipped: result.skipped
453
+ };
454
+ };
414
455
  const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark }) => {
415
456
  let nextTr = tr;
416
457
  const replacement = stripInlineEmphasisMarkers((() => {
417
- if (item.operation.type === "replaceInBlock") return item.operation.replace;
458
+ if (item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") return item.operation.replace;
418
459
  if (item.operation.type === "replaceBlock") return item.operation.text;
419
460
  return "";
420
461
  })());
421
462
  if (mode === "direct") {
422
- if (item.operation.type === "replaceInBlock" && hasInlineEmphasis(item.operation.replace)) {
463
+ if ((item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") && hasInlineEmphasis(item.operation.replace)) {
423
464
  const content = buildEmphasisInlineContent(nextTr.doc.type.schema, item.operation.replace, commentMark ? [commentMark] : []);
424
465
  return nextTr.replaceWith(item.from, item.to, content);
425
466
  }
@@ -447,6 +488,9 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
447
488
  sourceText = item.operation.find;
448
489
  sourceCleanStart = cleanBlock.text.indexOf(item.operation.find);
449
490
  if (sourceCleanStart === -1) sourceText = null;
491
+ } else if (item.operation.type === "replaceRange") {
492
+ sourceCleanStart = item.operation.range.startOffset;
493
+ sourceText = cleanBlock.text.slice(sourceCleanStart, item.operation.range.endOffset);
450
494
  } else if (item.operation.type === "replaceBlock") {
451
495
  sourceText = cleanBlock.text;
452
496
  sourceCleanStart = 0;
@@ -518,12 +562,13 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
518
562
  return nextTr;
519
563
  };
520
564
  const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => {
521
- const anchor = snapshot.anchors[operation.blockId];
565
+ const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
566
+ const anchor = snapshot.anchors[blockId];
522
567
  if (!anchor) return {
523
568
  type: "skip",
524
569
  reason: "missingBlock"
525
570
  };
526
- const encodedParaId = getFolioParaIdFromBlockId(operation.blockId);
571
+ const encodedParaId = getFolioParaIdFromBlockId(blockId);
527
572
  let live;
528
573
  if (encodedParaId !== null) {
529
574
  live = liveBlocksByParaId.get(encodedParaId);
@@ -532,7 +577,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
532
577
  reason: "missingBlock"
533
578
  };
534
579
  } else {
535
- const ordinal = ordinalAmongSameHash(snapshot, operation.blockId);
580
+ const ordinal = ordinalAmongSameHash(snapshot, blockId);
536
581
  if (ordinal < 0) return {
537
582
  type: "skip",
538
583
  reason: "missingBlock"
@@ -548,10 +593,44 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
548
593
  const blockTo = live.to;
549
594
  const cleanBlock = buildCleanBlockText(blockNode, blockFrom);
550
595
  const currentText = cleanBlock.text;
551
- if (hashFolioAIBlockText(normalizeFolioAIBlockText(currentText)) !== anchor.textHash) return {
596
+ const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
597
+ if (operation.precondition !== void 0 && currentTextHash !== operation.precondition.blockTextHash) return {
598
+ type: "skip",
599
+ reason: "preconditionFailed"
600
+ };
601
+ if (currentTextHash !== anchor.textHash) return {
552
602
  type: "skip",
553
603
  reason: "changedBlock"
554
604
  };
605
+ if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
606
+ const { startOffset, endOffset, selectedTextHash } = operation.range;
607
+ const from = cleanBlock.offsets[startOffset];
608
+ const to = cleanBlock.offsets[endOffset];
609
+ if (from === void 0 || to === void 0) return {
610
+ type: "skip",
611
+ reason: "staleRange"
612
+ };
613
+ const selectedText = currentText.slice(startOffset, endOffset);
614
+ if (hashFolioAIBlockText(selectedText) !== selectedTextHash) return {
615
+ type: "skip",
616
+ reason: "staleRange"
617
+ };
618
+ if (operation.type === "replaceRange" && selectedText === operation.replace) return {
619
+ type: "skip",
620
+ reason: "noopOperation"
621
+ };
622
+ return {
623
+ type: "resolved",
624
+ operation: {
625
+ operation,
626
+ from,
627
+ to,
628
+ blockFrom,
629
+ blockTo,
630
+ blockNode
631
+ }
632
+ };
633
+ }
555
634
  if (operation.type === "insertAfterBlock" || operation.type === "insertBeforeBlock") {
556
635
  if (operation.text.length === 0 && operation.pageBreakBefore !== true) return {
557
636
  type: "skip",
@@ -623,7 +702,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
623
702
  type: "skip",
624
703
  reason: "noopOperation"
625
704
  };
626
- const range = resolveTextInCleanBlock(cleanBlock, getOperationQuote(operation) || currentText);
705
+ const quote = getOperationQuote(operation);
706
+ const range = resolveTextInCleanBlock(cleanBlock, quote || currentText);
627
707
  if (range.type !== "resolved") return range;
628
708
  return {
629
709
  type: "resolved",
@@ -674,10 +754,10 @@ const getTextRangeFromCleanBlock = (cleanBlock) => {
674
754
  to
675
755
  };
676
756
  };
677
- const getOperationCommentText = (operation) => operation.comment?.text;
757
+ const getOperationCommentText = (operation) => "comment" in operation ? operation.comment?.text : void 0;
678
758
  const getOperationQuote = (operation) => {
679
759
  if (operation.type === "replaceInBlock") return operation.find;
680
760
  if (operation.type === "commentOnBlock") return operation.quote;
681
761
  };
682
762
  //#endregion
683
- export { applyFolioAIEditOperations };
763
+ export { applyFolioAIEditOperations, previewFolioAIEditOperations };
@@ -0,0 +1,33 @@
1
+ import { FolioAIEditSnapshot } from "./types.js";
2
+ import { Node } from "prosemirror-model";
3
+
4
+ //#region src/ai-edits/blockRange.d.ts
5
+ type DocPositionRange = {
6
+ from: number;
7
+ to: number;
8
+ };
9
+ type ResolveFolioAIBlockRangeOptions = {
10
+ blockId: string;
11
+ doc: Node;
12
+ snapshot?: FolioAIEditSnapshot | null | undefined;
13
+ };
14
+ declare const resolveFolioAIBlockRange: ({
15
+ blockId,
16
+ doc,
17
+ snapshot
18
+ }: ResolveFolioAIBlockRangeOptions) => DocPositionRange | null;
19
+ /**
20
+ * Clamp a `{from, to}` pair so both endpoints fit inside a document of
21
+ * `docSize` (in PM content positions). Block-boundary snapshots and stale
22
+ * range data sometimes produce a `to` one past the last inline position;
23
+ * `view.state.doc.resolve(...)` rejects that with
24
+ * "Position … out of range", and `TextSelection.between` doesn't help — it
25
+ * needs *valid* resolved positions. Clamping before resolution is the cheap
26
+ * defensive step.
27
+ *
28
+ * Order is preserved: if both endpoints exceed `docSize`, the returned
29
+ * `from` may equal `to`, yielding a cursor selection at the doc end.
30
+ */
31
+ declare function clampRangeToDocSize(docSize: number, range: DocPositionRange): DocPositionRange;
32
+ //#endregion
33
+ export { DocPositionRange, clampRangeToDocSize, resolveFolioAIBlockRange };
@@ -0,0 +1,58 @@
1
+ import { getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex } from "../types/block-id.js";
2
+ import { createFolioAIEditSnapshot } from "./snapshot.js";
3
+ import { findParagraphByParaId } from "../prosemirror/utils/findParagraphByParaId.js";
4
+ //#region src/ai-edits/blockRange.ts
5
+ const resolveFolioAIBlockRange = ({ blockId, doc, snapshot }) => {
6
+ const paraId = getFolioParaIdFromBlockId(blockId);
7
+ if (paraId !== null) {
8
+ const liveRange = findParagraphByParaId(doc, paraId);
9
+ if (liveRange !== null) return {
10
+ from: liveRange.from,
11
+ to: liveRange.to
12
+ };
13
+ }
14
+ const resolvedSnapshot = snapshot ?? createFolioAIEditSnapshot(doc);
15
+ const anchor = resolvedSnapshot.anchors[blockId] ?? resolveSequentialBlockAnchor(blockId, resolvedSnapshot);
16
+ if (!anchor) return null;
17
+ return clampRangeToDocSize(doc.content.size, anchor);
18
+ };
19
+ /**
20
+ * Resolve a `seq-NNNN` fallback id by document position.
21
+ *
22
+ * A sequential id is only minted for a paragraph the source DOCX left
23
+ * without a `w14:paraId`. The live editor's `ParaIdAllocator` fills
24
+ * that gap with a fresh random hex paraId, so a snapshot of the live
25
+ * document keys the block by that hex and never reproduces the
26
+ * server's `seq-NNNN`: the direct anchor lookup misses, and a
27
+ * paraId-based live lookup can't match either (no node carries a
28
+ * `seq-` paraId). The seq number is the block's 1-based position in
29
+ * the same non-empty-block walk the server extractor and
30
+ * `createFolioAIEditSnapshot` share, so it indexes the snapshot's
31
+ * ordered `blocks` directly.
32
+ */
33
+ const resolveSequentialBlockAnchor = (blockId, snapshot) => {
34
+ const index = getSequentialFolioBlockIdIndex(blockId);
35
+ if (index === null) return;
36
+ const block = snapshot.blocks.at(index - 1);
37
+ return block ? snapshot.anchors[block.id] : void 0;
38
+ };
39
+ /**
40
+ * Clamp a `{from, to}` pair so both endpoints fit inside a document of
41
+ * `docSize` (in PM content positions). Block-boundary snapshots and stale
42
+ * range data sometimes produce a `to` one past the last inline position;
43
+ * `view.state.doc.resolve(...)` rejects that with
44
+ * "Position … out of range", and `TextSelection.between` doesn't help — it
45
+ * needs *valid* resolved positions. Clamping before resolution is the cheap
46
+ * defensive step.
47
+ *
48
+ * Order is preserved: if both endpoints exceed `docSize`, the returned
49
+ * `from` may equal `to`, yielding a cursor selection at the doc end.
50
+ */
51
+ function clampRangeToDocSize(docSize, range) {
52
+ return {
53
+ from: Math.min(Math.max(range.from, 0), docSize),
54
+ to: Math.min(Math.max(range.to, 0), docSize)
55
+ };
56
+ }
57
+ //#endregion
58
+ export { clampRangeToDocSize, resolveFolioAIBlockRange };
@@ -1,5 +1,6 @@
1
1
  import { FolioAIBlock, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js";
2
2
  import { document_d_exports } from "../types/document.js";
3
+ import { FolioDocumentOperationBatch, FolioDocumentOperationResult, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult } from "../document-operations.js";
3
4
  import { FolioReviewChange, FolioReviewChangeKind } from "./read.js";
4
5
 
5
6
  //#region src/ai-edits/headless.d.ts
@@ -18,6 +19,8 @@ type FolioApplyOperationsOptions = {
18
19
  */
19
20
  snapshot?: FolioAIEditSnapshot;
20
21
  };
22
+ /** Options for {@link FolioDocxReviewer.applyDocumentOperations}. */
23
+ type FolioApplyDocumentOperationsOptions = Omit<FolioApplyOperationsOptions, "mode">;
21
24
  /** Options for {@link FolioDocxReviewer.getContentAsText}. */
22
25
  type FolioGetContentAsTextOptions = {
23
26
  /**
@@ -27,6 +30,19 @@ type FolioGetContentAsTextOptions = {
27
30
  */
28
31
  annotated?: boolean;
29
32
  };
33
+ type FolioDocumentStoryHandle = {
34
+ type: "main";
35
+ } | {
36
+ type: "header" | "footer";
37
+ relationshipId: string;
38
+ } | {
39
+ type: "footnote" | "endnote";
40
+ noteId: number;
41
+ };
42
+ type FolioDocumentStory = {
43
+ handle: FolioDocumentStoryHandle;
44
+ text: string;
45
+ };
30
46
  /** Filter for {@link FolioDocxReviewer.getChanges}. */
31
47
  type FolioReviewChangeFilter = {
32
48
  author?: string;
@@ -82,6 +98,7 @@ declare class FolioDocxReviewer {
82
98
  private readonly originalBuffer;
83
99
  private state;
84
100
  private readonly createdComments;
101
+ private readonly documentOperationUndoEntries;
85
102
  /**
86
103
  * Resolved-state overrides recorded by {@link resolveComment}, keyed by
87
104
  * comment id. Applied on read ({@link getComments}) and on write
@@ -105,6 +122,16 @@ declare class FolioDocxReviewer {
105
122
  * (including any comments) is retained for {@link toBuffer}.
106
123
  */
107
124
  applyOperations(operations: FolioAIEditOperation[], options?: FolioApplyOperationsOptions): FolioAIEditApplyResult;
125
+ /**
126
+ * Apply a versioned document-operation batch against the current state.
127
+ * This is the contract entry point for serialized callers; the legacy
128
+ * {@link applyOperations} method delegates here so both APIs keep identical
129
+ * edit semantics.
130
+ */
131
+ applyDocumentOperations(batch: FolioDocumentOperationBatch, options?: FolioApplyDocumentOperationsOptions): FolioDocumentOperationResult;
132
+ private applyDocumentOperationsInternal;
133
+ /** Undo the latest unchanged document-operation batch and its created comments. */
134
+ undoDocumentOperations(undoHandle: FolioDocumentOperationUndoHandle): FolioDocumentOperationUndoResult;
108
135
  /**
109
136
  * The body blocks with their stable ids, in document order — the same
110
137
  * `FolioAIBlock` shape {@link snapshot} builds, reused verbatim so the ids
@@ -131,6 +158,10 @@ declare class FolioDocxReviewer {
131
158
  * Empty parts and separator notes are omitted.
132
159
  */
133
160
  getNotesAsText(): string;
161
+ /** Discover every readable document story through a typed, serializable handle. */
162
+ listStories(): FolioDocumentStory[];
163
+ /** Read one discovered story; returns null when its handle is no longer present. */
164
+ readStory(handle: FolioDocumentStoryHandle): FolioDocumentStory | null;
134
165
  /**
135
166
  * The tracked changes (insertions and deletions) present in the body, read
136
167
  * from the same `insertion` / `deletion` marks the editor renders. Each
@@ -210,9 +241,11 @@ declare class FolioDocxReviewer {
210
241
  private trySelectiveSave;
211
242
  /**
212
243
  * Count every tracked change an accept-all / reject-all sweep resolves: the
213
- * inline insertion / deletion groups {@link getChanges} enumerates, plus
214
- * paragraph-level changes (`pPrMark`, `_propertyChanges`) that live on
215
- * paragraph attrs rather than inline marks.
244
+ * inline insertion / deletion groups {@link getChanges} enumerates, plus the
245
+ * property-change records living on node attrs rather than inline marks —
246
+ * paragraph-level (`pPrMark`, `_propertyChanges`, the inline sectPr's
247
+ * `propertyChanges`) and table-level (`tblPrChange` / `trPrChange` /
248
+ * `tcPrChange`).
216
249
  */
217
250
  private countTrackedChanges;
218
251
  /** Apply any {@link resolveComment} overrides recorded for these comments. */
@@ -250,4 +283,4 @@ type ApplyFolioAIEditsToBufferResult = FolioAIEditApplyResult & {
250
283
  */
251
284
  declare const applyFolioAIEditsToBuffer: (buffer: ArrayBuffer, operations: FolioAIEditOperation[], options?: ApplyFolioAIEditsToBufferOptions) => Promise<ApplyFolioAIEditsToBufferResult>;
252
285
  //#endregion
253
- export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioGetContentAsTextOptions, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer };
286
+ export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyDocumentOperationsOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocxReviewer, FolioDocxReviewerOptions, FolioGetContentAsTextOptions, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer };
@@ -1,9 +1,9 @@
1
+ import { buildAnnotatedBlockText } from "./clean-text.js";
2
+ import { createFolioAIEditSnapshot, normalizeFolioAIBlockText } from "./snapshot.js";
3
+ import { applyFolioDocumentOperations } from "../document-operations.js";
1
4
  import { getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
2
5
  import { deterministicHexId } from "../utils/hexId.js";
3
6
  import { repackDocx } from "../docx/rezip.js";
4
- import { buildAnnotatedBlockText } from "./clean-text.js";
5
- import { createFolioAIEditSnapshot, normalizeFolioAIBlockText } from "./snapshot.js";
6
- import { applyFolioAIEditOperations } from "./apply.js";
7
7
  import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
8
8
  import { createReply } from "../docx/replyToComment.js";
9
9
  import { attemptSelectiveSave } from "../docx/selectiveSave.js";
@@ -44,6 +44,7 @@ import { EditorState } from "prosemirror-state";
44
44
  * across reviewers created within the same millisecond.
45
45
  */
46
46
  let commentIdCursor = Date.now();
47
+ let undoHandleCursor = Date.now();
47
48
  /**
48
49
  * Build the note-free comment thread the apply layer references by id.
49
50
  * Mirrors `commentsHelpers.createComment` (a pure object literal there) so a
@@ -162,6 +163,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
162
163
  originalBuffer;
163
164
  state;
164
165
  createdComments = [];
166
+ documentOperationUndoEntries = [];
165
167
  /**
166
168
  * Resolved-state overrides recorded by {@link resolveComment}, keyed by
167
169
  * comment id. Applied on read ({@link getComments}) and on write
@@ -210,28 +212,91 @@ var FolioDocxReviewer = class FolioDocxReviewer {
210
212
  * (including any comments) is retained for {@link toBuffer}.
211
213
  */
212
214
  applyOperations(operations, options = {}) {
213
- const snapshot = options.snapshot ?? this.snapshot();
215
+ const { applied, skipped } = this.applyDocumentOperationsInternal({
216
+ version: 1,
217
+ operations,
218
+ mode: options.mode ?? "tracked-changes"
219
+ }, { ...options.snapshot !== void 0 && { snapshot: options.snapshot } }, false);
220
+ return {
221
+ applied,
222
+ skipped
223
+ };
224
+ }
225
+ /**
226
+ * Apply a versioned document-operation batch against the current state.
227
+ * This is the contract entry point for serialized callers; the legacy
228
+ * {@link applyOperations} method delegates here so both APIs keep identical
229
+ * edit semantics.
230
+ */
231
+ applyDocumentOperations(batch, options = {}) {
232
+ return this.applyDocumentOperationsInternal(batch, options, true);
233
+ }
234
+ applyDocumentOperationsInternal(batch, options, createUndoEntry) {
235
+ const beforeState = this.state;
236
+ const createdCommentsLengthBefore = this.createdComments.length;
214
237
  const view = {
215
238
  state: this.state,
216
239
  dispatch: (transaction) => {
217
240
  view.state = view.state.apply(transaction);
218
241
  }
219
242
  };
220
- const result = applyFolioAIEditOperations({
243
+ const result = applyFolioDocumentOperations({
221
244
  view,
222
- snapshot,
223
- operations,
224
- mode: options.mode ?? "tracked-changes",
245
+ snapshot: options.snapshot ?? this.snapshot(),
246
+ batch,
225
247
  author: this.author,
226
248
  createCommentId: (text) => {
227
249
  const comment = createReviewerComment(text, this.author);
228
250
  this.createdComments.push(comment);
229
251
  return comment.id;
230
- }
252
+ },
253
+ ...createUndoEntry && { createUndoHandle: () => ({
254
+ type: "documentOperationUndo",
255
+ id: `headless-${String(undoHandleCursor++)}`
256
+ }) }
231
257
  });
232
258
  this.state = view.state;
259
+ if (result.undoHandle !== null) this.documentOperationUndoEntries.push({
260
+ undoHandle: result.undoHandle,
261
+ beforeState,
262
+ afterState: this.state,
263
+ createdCommentsLengthBefore,
264
+ createdCommentsLengthAfter: this.createdComments.length
265
+ });
233
266
  return result;
234
267
  }
268
+ /** Undo the latest unchanged document-operation batch and its created comments. */
269
+ undoDocumentOperations(undoHandle) {
270
+ const entryIndex = this.documentOperationUndoEntries.findIndex((entry) => entry.undoHandle.type === undoHandle.type && entry.undoHandle.id === undoHandle.id);
271
+ if (entryIndex === -1) return {
272
+ status: "rejected",
273
+ undoHandle,
274
+ reason: "unknownHandle"
275
+ };
276
+ if (entryIndex !== this.documentOperationUndoEntries.length - 1) return {
277
+ status: "rejected",
278
+ undoHandle,
279
+ reason: "notLatest"
280
+ };
281
+ const entry = this.documentOperationUndoEntries.at(-1);
282
+ if (!entry) return {
283
+ status: "rejected",
284
+ undoHandle,
285
+ reason: "unknownHandle"
286
+ };
287
+ if (this.state !== entry.afterState || this.createdComments.length !== entry.createdCommentsLengthAfter) return {
288
+ status: "rejected",
289
+ undoHandle,
290
+ reason: "documentChanged"
291
+ };
292
+ this.state = entry.beforeState;
293
+ this.createdComments.length = entry.createdCommentsLengthBefore;
294
+ this.documentOperationUndoEntries.pop();
295
+ return {
296
+ status: "undone",
297
+ undoHandle
298
+ };
299
+ }
235
300
  /**
236
301
  * The body blocks with their stable ids, in document order — the same
237
302
  * `FolioAIBlock` shape {@link snapshot} builds, reused verbatim so the ids
@@ -259,7 +324,8 @@ var FolioDocxReviewer = class FolioDocxReviewer {
259
324
  return snapshot.blocks.map((block) => {
260
325
  const from = startById.get(block.id);
261
326
  const node = from === void 0 ? null : this.state.doc.nodeAt(from);
262
- return formatBlockLine(block, node ? buildAnnotatedBlockText(node) : block.text);
327
+ const text = node ? buildAnnotatedBlockText(node) : block.text;
328
+ return formatBlockLine(block, text);
263
329
  }).join("\n");
264
330
  }
265
331
  /**
@@ -293,6 +359,50 @@ var FolioDocxReviewer = class FolioDocxReviewer {
293
359
  }
294
360
  return lines.join("\n");
295
361
  }
362
+ /** Discover every readable document story through a typed, serializable handle. */
363
+ listStories() {
364
+ const pkg = this.baseDocument.package;
365
+ const stories = [{
366
+ handle: { type: "main" },
367
+ text: this.getContentAsText()
368
+ }];
369
+ for (const [relationshipId, header] of pkg.headers ?? []) stories.push({
370
+ handle: {
371
+ type: "header",
372
+ relationshipId
373
+ },
374
+ text: normalizeFolioAIBlockText(getHeaderFooterText(header))
375
+ });
376
+ for (const [relationshipId, footer] of pkg.footers ?? []) stories.push({
377
+ handle: {
378
+ type: "footer",
379
+ relationshipId
380
+ },
381
+ text: normalizeFolioAIBlockText(getHeaderFooterText(footer))
382
+ });
383
+ for (const footnote of pkg.footnotes ?? []) if (!isSeparatorFootnote(footnote)) stories.push({
384
+ handle: {
385
+ type: "footnote",
386
+ noteId: footnote.id
387
+ },
388
+ text: normalizeFolioAIBlockText(getFootnoteText(footnote))
389
+ });
390
+ for (const endnote of pkg.endnotes ?? []) if (!isSeparatorEndnote(endnote)) stories.push({
391
+ handle: {
392
+ type: "endnote",
393
+ noteId: endnote.id
394
+ },
395
+ text: normalizeFolioAIBlockText(getEndnoteText(endnote))
396
+ });
397
+ return stories;
398
+ }
399
+ /** Read one discovered story; returns null when its handle is no longer present. */
400
+ readStory(handle) {
401
+ const stories = this.listStories();
402
+ if (handle.type === "main") return stories.at(0) ?? null;
403
+ if (handle.type === "header" || handle.type === "footer") return stories.find(({ handle: candidate }) => candidate.type === handle.type && candidate.relationshipId === handle.relationshipId) ?? null;
404
+ return stories.find(({ handle: candidate }) => (candidate.type === "footnote" || candidate.type === "endnote") && candidate.type === handle.type && candidate.noteId === handle.noteId) ?? null;
405
+ }
296
406
  /**
297
407
  * The tracked changes (insertions and deletions) present in the body, read
298
408
  * from the same `insertion` / `deletion` marks the editor renders. Each
@@ -464,18 +574,26 @@ var FolioDocxReviewer = class FolioDocxReviewer {
464
574
  }
465
575
  /**
466
576
  * Count every tracked change an accept-all / reject-all sweep resolves: the
467
- * inline insertion / deletion groups {@link getChanges} enumerates, plus
468
- * paragraph-level changes (`pPrMark`, `_propertyChanges`) that live on
469
- * paragraph attrs rather than inline marks.
577
+ * inline insertion / deletion groups {@link getChanges} enumerates, plus the
578
+ * property-change records living on node attrs rather than inline marks —
579
+ * paragraph-level (`pPrMark`, `_propertyChanges`, the inline sectPr's
580
+ * `propertyChanges`) and table-level (`tblPrChange` / `trPrChange` /
581
+ * `tcPrChange`).
470
582
  */
471
583
  countTrackedChanges() {
584
+ const hasEntries = (value) => Array.isArray(value) && value.length > 0;
472
585
  let count = this.getChanges().length;
473
586
  this.state.doc.descendants((node) => {
474
- if (node.type.name !== "paragraph") return;
587
+ const typeName = node.type.name;
588
+ if (typeName === "table" && hasEntries(node.attrs["tblPrChange"])) count += 1;
589
+ if (typeName === "tableRow" && hasEntries(node.attrs["trPrChange"])) count += 1;
590
+ if ((typeName === "tableCell" || typeName === "tableHeader") && hasEntries(node.attrs["tcPrChange"])) count += 1;
591
+ if (typeName !== "paragraph") return;
475
592
  const pPrMark = node.attrs["pPrMark"];
476
593
  if (pPrMark !== null && pPrMark !== void 0) count += 1;
477
- const propertyChanges = node.attrs["_propertyChanges"];
478
- if (Array.isArray(propertyChanges) && propertyChanges.length > 0) count += 1;
594
+ if (hasEntries(node.attrs["_propertyChanges"])) count += 1;
595
+ const sectionProperties = node.attrs["_sectionProperties"];
596
+ if (hasEntries(sectionProperties?.propertyChanges)) count += 1;
479
597
  return false;
480
598
  });
481
599
  return count;
@@ -1,8 +1,10 @@
1
- import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./types.js";
1
+ import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAIInlineFormatting, FolioAISignatureParty, FolioAITextRangeHandle } from "./types.js";
2
2
  import { FolioAIEditView, applyFolioAIEditOperations } from "./apply.js";
3
3
  import { buildAnnotatedBlockText } from "./clean-text.js";
4
+ import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, FolioDocumentOperationUndoFailureReason, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js";
4
5
  import { FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
5
- import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
6
+ import { FolioDocumentStory, FolioDocumentStoryHandle } from "./headless.js";
7
+ import { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
6
8
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
7
9
  import { WordDiffSegment, diffWordSegments } from "./word-diff.js";
8
- export { type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAISignatureParty, type FolioCommentAnchor, type FolioReviewChange, type FolioReviewChangeKind, type WordDiffSegment, applyFolioAIEditOperations, buildAnnotatedBlockText, createFolioAIEditSnapshot, diffWordSegments, getCommentAnchorsFromDoc, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, normalizeFolioAIBlockText };
10
+ export { type ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAIInlineFormatting, type FolioAISignatureParty, type FolioAITextRangeHandle, type FolioCommentAnchor, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentStory, type FolioDocumentStoryHandle, type FolioReviewChange, type FolioReviewChangeKind, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, applyFolioAIEditOperations, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, buildAnnotatedBlockText, createFolioAIEditSnapshot, createFolioAITextRangeHandle, diffWordSegments, getCommentAnchorsFromDoc, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch };