@stll/folio-core 0.4.0 → 0.5.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 (99) 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 +33 -4
  6. package/dist/ai-edits/headless.js +83 -15
  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 +122 -0
  19. package/dist/document-operations.js +575 -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/index.d.ts +3 -2
  37. package/dist/index.js +6 -5
  38. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  39. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  40. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
  41. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
  42. package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
  43. package/dist/layout-engine/index.d.ts +6 -5
  44. package/dist/layout-engine/index.js +89 -26
  45. package/dist/layout-engine/keep-together.d.ts +13 -8
  46. package/dist/layout-engine/keep-together.js +40 -25
  47. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  48. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  49. package/dist/layout-engine/measure/measureBlocks.js +28 -18
  50. package/dist/layout-engine/measure/measureParagraph.js +61 -15
  51. package/dist/layout-engine/paginator.d.ts +2 -1
  52. package/dist/layout-engine/paginator.js +9 -3
  53. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  54. package/dist/layout-engine/textBoxGroup.js +8 -0
  55. package/dist/layout-engine/types.d.ts +30 -9
  56. package/dist/layout-engine/types.js +20 -2
  57. package/dist/layout-painter/renderPage.d.ts +15 -1
  58. package/dist/layout-painter/renderPage.js +27 -11
  59. package/dist/layout-painter/renderParagraph.js +18 -8
  60. package/dist/layout-painter/renderTable.js +10 -7
  61. package/dist/managers/DocumentLoaderManager.js +12 -5
  62. package/dist/managers/TableSelectionManager.js +1 -1
  63. package/dist/model.js +1 -1
  64. package/dist/paged-layout/pageText.d.ts +14 -0
  65. package/dist/paged-layout/pageText.js +21 -0
  66. package/dist/prosemirror/attrs/index.js +2 -0
  67. package/dist/prosemirror/commands/comments.js +75 -5
  68. package/dist/prosemirror/commands/hyperlink.js +4 -2
  69. package/dist/prosemirror/commands/image.js +2 -1
  70. package/dist/prosemirror/commands/index.js +1 -1
  71. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  72. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  73. package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
  74. package/dist/prosemirror/conversion/toProseDoc.js +29 -8
  75. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  76. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  77. package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
  78. package/dist/prosemirror/findReplaceSelection.js +2 -1
  79. package/dist/prosemirror/index.js +1 -1
  80. package/dist/prosemirror/insertOperations.js +1 -1
  81. package/dist/prosemirror/schema/nodes.d.ts +10 -5
  82. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  83. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  84. package/dist/redline.d.ts +20 -0
  85. package/dist/redline.js +127 -0
  86. package/dist/server.d.ts +7 -3
  87. package/dist/server.js +6 -2
  88. package/dist/symbols.d.ts +35 -0
  89. package/dist/symbols.js +512 -0
  90. package/dist/utils/clipboard.js +3 -3
  91. package/dist/utils/fontLoader.js +3 -0
  92. package/dist/utils/fontResolver.js +48 -1
  93. package/dist/utils/zoom.d.ts +21 -0
  94. package/dist/utils/zoom.js +21 -0
  95. package/dist/version-comparison.d.ts +92 -0
  96. package/dist/version-comparison.js +436 -0
  97. package/package.json +3 -3
  98. package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
  99. 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 } 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;
@@ -105,6 +121,13 @@ declare class FolioDocxReviewer {
105
121
  * (including any comments) is retained for {@link toBuffer}.
106
122
  */
107
123
  applyOperations(operations: FolioAIEditOperation[], options?: FolioApplyOperationsOptions): FolioAIEditApplyResult;
124
+ /**
125
+ * Apply a versioned document-operation batch against the current state.
126
+ * This is the contract entry point for serialized callers; the legacy
127
+ * {@link applyOperations} method delegates here so both APIs keep identical
128
+ * edit semantics.
129
+ */
130
+ applyDocumentOperations(batch: FolioDocumentOperationBatch, options?: FolioApplyDocumentOperationsOptions): FolioDocumentOperationResult;
108
131
  /**
109
132
  * The body blocks with their stable ids, in document order — the same
110
133
  * `FolioAIBlock` shape {@link snapshot} builds, reused verbatim so the ids
@@ -131,6 +154,10 @@ declare class FolioDocxReviewer {
131
154
  * Empty parts and separator notes are omitted.
132
155
  */
133
156
  getNotesAsText(): string;
157
+ /** Discover every readable document story through a typed, serializable handle. */
158
+ listStories(): FolioDocumentStory[];
159
+ /** Read one discovered story; returns null when its handle is no longer present. */
160
+ readStory(handle: FolioDocumentStoryHandle): FolioDocumentStory | null;
134
161
  /**
135
162
  * The tracked changes (insertions and deletions) present in the body, read
136
163
  * from the same `insertion` / `deletion` marks the editor renders. Each
@@ -210,9 +237,11 @@ declare class FolioDocxReviewer {
210
237
  private trySelectiveSave;
211
238
  /**
212
239
  * 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.
240
+ * inline insertion / deletion groups {@link getChanges} enumerates, plus the
241
+ * property-change records living on node attrs rather than inline marks —
242
+ * paragraph-level (`pPrMark`, `_propertyChanges`, the inline sectPr's
243
+ * `propertyChanges`) and table-level (`tblPrChange` / `trPrChange` /
244
+ * `tcPrChange`).
216
245
  */
217
246
  private countTrackedChanges;
218
247
  /** Apply any {@link resolveComment} overrides recorded for these comments. */
@@ -250,4 +279,4 @@ type ApplyFolioAIEditsToBufferResult = FolioAIEditApplyResult & {
250
279
  */
251
280
  declare const applyFolioAIEditsToBuffer: (buffer: ArrayBuffer, operations: FolioAIEditOperation[], options?: ApplyFolioAIEditsToBufferOptions) => Promise<ApplyFolioAIEditsToBufferResult>;
252
281
  //#endregion
253
- export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioGetContentAsTextOptions, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer };
282
+ 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";
@@ -210,18 +210,33 @@ var FolioDocxReviewer = class FolioDocxReviewer {
210
210
  * (including any comments) is retained for {@link toBuffer}.
211
211
  */
212
212
  applyOperations(operations, options = {}) {
213
- const snapshot = options.snapshot ?? this.snapshot();
213
+ const { applied, skipped } = this.applyDocumentOperations({
214
+ version: 1,
215
+ operations,
216
+ mode: options.mode ?? "tracked-changes"
217
+ }, { ...options.snapshot !== void 0 && { snapshot: options.snapshot } });
218
+ return {
219
+ applied,
220
+ skipped
221
+ };
222
+ }
223
+ /**
224
+ * Apply a versioned document-operation batch against the current state.
225
+ * This is the contract entry point for serialized callers; the legacy
226
+ * {@link applyOperations} method delegates here so both APIs keep identical
227
+ * edit semantics.
228
+ */
229
+ applyDocumentOperations(batch, options = {}) {
214
230
  const view = {
215
231
  state: this.state,
216
232
  dispatch: (transaction) => {
217
233
  view.state = view.state.apply(transaction);
218
234
  }
219
235
  };
220
- const result = applyFolioAIEditOperations({
236
+ const result = applyFolioDocumentOperations({
221
237
  view,
222
- snapshot,
223
- operations,
224
- mode: options.mode ?? "tracked-changes",
238
+ snapshot: options.snapshot ?? this.snapshot(),
239
+ batch,
225
240
  author: this.author,
226
241
  createCommentId: (text) => {
227
242
  const comment = createReviewerComment(text, this.author);
@@ -259,7 +274,8 @@ var FolioDocxReviewer = class FolioDocxReviewer {
259
274
  return snapshot.blocks.map((block) => {
260
275
  const from = startById.get(block.id);
261
276
  const node = from === void 0 ? null : this.state.doc.nodeAt(from);
262
- return formatBlockLine(block, node ? buildAnnotatedBlockText(node) : block.text);
277
+ const text = node ? buildAnnotatedBlockText(node) : block.text;
278
+ return formatBlockLine(block, text);
263
279
  }).join("\n");
264
280
  }
265
281
  /**
@@ -293,6 +309,50 @@ var FolioDocxReviewer = class FolioDocxReviewer {
293
309
  }
294
310
  return lines.join("\n");
295
311
  }
312
+ /** Discover every readable document story through a typed, serializable handle. */
313
+ listStories() {
314
+ const pkg = this.baseDocument.package;
315
+ const stories = [{
316
+ handle: { type: "main" },
317
+ text: this.getContentAsText()
318
+ }];
319
+ for (const [relationshipId, header] of pkg.headers ?? []) stories.push({
320
+ handle: {
321
+ type: "header",
322
+ relationshipId
323
+ },
324
+ text: normalizeFolioAIBlockText(getHeaderFooterText(header))
325
+ });
326
+ for (const [relationshipId, footer] of pkg.footers ?? []) stories.push({
327
+ handle: {
328
+ type: "footer",
329
+ relationshipId
330
+ },
331
+ text: normalizeFolioAIBlockText(getHeaderFooterText(footer))
332
+ });
333
+ for (const footnote of pkg.footnotes ?? []) if (!isSeparatorFootnote(footnote)) stories.push({
334
+ handle: {
335
+ type: "footnote",
336
+ noteId: footnote.id
337
+ },
338
+ text: normalizeFolioAIBlockText(getFootnoteText(footnote))
339
+ });
340
+ for (const endnote of pkg.endnotes ?? []) if (!isSeparatorEndnote(endnote)) stories.push({
341
+ handle: {
342
+ type: "endnote",
343
+ noteId: endnote.id
344
+ },
345
+ text: normalizeFolioAIBlockText(getEndnoteText(endnote))
346
+ });
347
+ return stories;
348
+ }
349
+ /** Read one discovered story; returns null when its handle is no longer present. */
350
+ readStory(handle) {
351
+ const stories = this.listStories();
352
+ if (handle.type === "main") return stories.at(0) ?? null;
353
+ if (handle.type === "header" || handle.type === "footer") return stories.find(({ handle: candidate }) => candidate.type === handle.type && candidate.relationshipId === handle.relationshipId) ?? null;
354
+ return stories.find(({ handle: candidate }) => (candidate.type === "footnote" || candidate.type === "endnote") && candidate.type === handle.type && candidate.noteId === handle.noteId) ?? null;
355
+ }
296
356
  /**
297
357
  * The tracked changes (insertions and deletions) present in the body, read
298
358
  * from the same `insertion` / `deletion` marks the editor renders. Each
@@ -464,18 +524,26 @@ var FolioDocxReviewer = class FolioDocxReviewer {
464
524
  }
465
525
  /**
466
526
  * 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.
527
+ * inline insertion / deletion groups {@link getChanges} enumerates, plus the
528
+ * property-change records living on node attrs rather than inline marks —
529
+ * paragraph-level (`pPrMark`, `_propertyChanges`, the inline sectPr's
530
+ * `propertyChanges`) and table-level (`tblPrChange` / `trPrChange` /
531
+ * `tcPrChange`).
470
532
  */
471
533
  countTrackedChanges() {
534
+ const hasEntries = (value) => Array.isArray(value) && value.length > 0;
472
535
  let count = this.getChanges().length;
473
536
  this.state.doc.descendants((node) => {
474
- if (node.type.name !== "paragraph") return;
537
+ const typeName = node.type.name;
538
+ if (typeName === "table" && hasEntries(node.attrs["tblPrChange"])) count += 1;
539
+ if (typeName === "tableRow" && hasEntries(node.attrs["trPrChange"])) count += 1;
540
+ if ((typeName === "tableCell" || typeName === "tableHeader") && hasEntries(node.attrs["tcPrChange"])) count += 1;
541
+ if (typeName !== "paragraph") return;
475
542
  const pPrMark = node.attrs["pPrMark"];
476
543
  if (pPrMark !== null && pPrMark !== void 0) count += 1;
477
- const propertyChanges = node.attrs["_propertyChanges"];
478
- if (Array.isArray(propertyChanges) && propertyChanges.length > 0) count += 1;
544
+ if (hasEntries(node.attrs["_propertyChanges"])) count += 1;
545
+ const sectionProperties = node.attrs["_sectionProperties"];
546
+ if (hasEntries(sectionProperties?.propertyChanges)) count += 1;
479
547
  return false;
480
548
  });
481
549
  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, 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 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 };
@@ -1,7 +1,8 @@
1
1
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
2
2
  import { buildAnnotatedBlockText } from "./clean-text.js";
3
- import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
3
+ import { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
4
4
  import { diffWordSegments } from "./word-diff.js";
5
5
  import { applyFolioAIEditOperations } from "./apply.js";
6
+ import { 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, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js";
6
7
  import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
7
- export { applyFolioAIEditOperations, buildAnnotatedBlockText, createFolioAIEditSnapshot, diffWordSegments, getCommentAnchorsFromDoc, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, normalizeFolioAIBlockText };
8
+ export { 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, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioAIEditOperations, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, buildAnnotatedBlockText, createFolioAIEditSnapshot, createFolioAITextRangeHandle, diffWordSegments, getCommentAnchorsFromDoc, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch };
@@ -1,9 +1,21 @@
1
- import { FolioAIEditSnapshot } from "./types.js";
1
+ import { FolioAIEditSnapshot, FolioAITextRangeHandle } from "./types.js";
2
2
  import { Node } from "prosemirror-model";
3
3
 
4
4
  //#region src/ai-edits/snapshot.d.ts
5
5
  declare const normalizeFolioAIBlockText: (text: string) => string;
6
6
  declare const hashFolioAIBlockText: (text: string) => string;
7
+ type CreateFolioAITextRangeHandleOptions = {
8
+ blockId: string;
9
+ text: string;
10
+ startOffset: number;
11
+ endOffset: number;
12
+ };
13
+ declare const createFolioAITextRangeHandle: ({
14
+ blockId,
15
+ text,
16
+ startOffset,
17
+ endOffset
18
+ }: CreateFolioAITextRangeHandleOptions) => FolioAITextRangeHandle | null;
7
19
  declare const createFolioAIEditSnapshot: (doc: Node) => FolioAIEditSnapshot;
8
20
  //#endregion
9
- export { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText };
21
+ export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText };
@@ -7,6 +7,17 @@ const hashFolioAIBlockText = (text) => {
7
7
  for (const character of text) hash = (hash * 33 + (character.codePointAt(0) ?? 0)) % 2147483647;
8
8
  return `h${hash.toString(36)}`;
9
9
  };
10
+ const createFolioAITextRangeHandle = ({ blockId, text, startOffset, endOffset }) => {
11
+ if (blockId.length === 0 || !Number.isInteger(startOffset) || !Number.isInteger(endOffset) || startOffset < 0 || endOffset <= startOffset || endOffset > text.length) return null;
12
+ return {
13
+ type: "textRange",
14
+ story: "main",
15
+ blockId,
16
+ startOffset,
17
+ endOffset,
18
+ selectedTextHash: hashFolioAIBlockText(text.slice(startOffset, endOffset))
19
+ };
20
+ };
10
21
  const createFolioAIEditSnapshot = (doc) => {
11
22
  const draftBlocks = [];
12
23
  const hashCounts = /* @__PURE__ */ new Map();
@@ -192,4 +203,4 @@ const getColorFromAttrs = (attrs) => {
192
203
  const samePreviewRunStyle = (run, style) => run.bold === style.bold && run.italic === style.italic && run.underline === style.underline && run.strike === style.strike && run.fontFamily === style.fontFamily && run.fontSizePt === style.fontSizePt && run.color === style.color;
193
204
  const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
194
205
  //#endregion
195
- export { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText };
206
+ export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText };