@stll/folio-core 0.37.5 → 0.38.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 (121) hide show
  1. package/dist/ai-edits/apply.js +275 -88
  2. package/dist/ai-edits/clean-text.js +12 -1
  3. package/dist/ai-edits/headless.d.ts +110 -0
  4. package/dist/ai-edits/headless.js +569 -40
  5. package/dist/ai-edits/snapshot.d.ts +24 -1
  6. package/dist/ai-edits/snapshot.js +206 -9
  7. package/dist/ai-edits/table-geometry.d.ts +7 -7
  8. package/dist/ai-edits/table-geometry.js +43 -27
  9. package/dist/ai-edits/table-template.d.ts +2 -1
  10. package/dist/ai-edits/table-template.js +9 -3
  11. package/dist/ai-edits/types.d.ts +37 -15
  12. package/dist/compare/__fixtures__/body-sequence.d.ts +4 -0
  13. package/dist/compare/__fixtures__/body-sequence.js +5 -3
  14. package/dist/compare/compare.d.ts +26 -2
  15. package/dist/compare/compare.js +444 -16
  16. package/dist/compare/content-alignment.js +3 -2
  17. package/dist/compare/content-types.d.ts +34 -2
  18. package/dist/compare/content.d.ts +13 -10
  19. package/dist/compare/content.js +69 -23
  20. package/dist/compare/formatting.d.ts +4 -22
  21. package/dist/compare/formatting.js +8 -112
  22. package/dist/compare/inline-atom-resources.d.ts +6 -0
  23. package/dist/compare/inline-atom-resources.js +26 -0
  24. package/dist/compare/inline-atoms.d.ts +35 -0
  25. package/dist/compare/inline-atoms.js +383 -0
  26. package/dist/compare/inline-provenance.d.ts +36 -0
  27. package/dist/compare/inline-provenance.js +334 -0
  28. package/dist/compare/plan.d.ts +4 -1
  29. package/dist/compare/plan.js +32 -4
  30. package/dist/compare/scenario.d.ts +4 -6
  31. package/dist/compare/section-boundary-properties.d.ts +63 -0
  32. package/dist/compare/section-boundary-properties.js +235 -0
  33. package/dist/compare/style-resources.d.ts +30 -0
  34. package/dist/compare/style-resources.js +404 -0
  35. package/dist/compare/types.d.ts +50 -11
  36. package/dist/compare/types.js +3 -1
  37. package/dist/compare/verification.d.ts +3 -5
  38. package/dist/compare/verification.js +8 -39
  39. package/dist/compat/eigenpal.d.ts +4 -4
  40. package/dist/compat/eigenpal.js +3 -3
  41. package/dist/document-operations.d.ts +5 -3
  42. package/dist/document-operations.js +112 -5
  43. package/dist/docx/blockContentParser.js +14 -2
  44. package/dist/docx/drawingIdNormalization.js +43 -2
  45. package/dist/docx/drawingRelationships.d.ts +10 -0
  46. package/dist/docx/drawingRelationships.js +28 -0
  47. package/dist/docx/fieldParser.js +1 -1
  48. package/dist/docx/hyperlinkParser.js +3 -5
  49. package/dist/docx/imageRawXml.d.ts +8 -0
  50. package/dist/docx/imageRawXml.js +13 -0
  51. package/dist/docx/newImage.d.ts +2 -2
  52. package/dist/docx/newImage.js +3 -3
  53. package/dist/docx/numberingParser.js +30 -27
  54. package/dist/docx/paragraphParser.js +11 -4
  55. package/dist/docx/removeHeaderFooterParts.d.ts +14 -0
  56. package/dist/docx/removeHeaderFooterParts.js +87 -0
  57. package/dist/docx/rezip.d.ts +3 -1
  58. package/dist/docx/rezip.js +143 -41
  59. package/dist/docx/runParser.js +16 -5
  60. package/dist/docx/sectionParser.js +3 -0
  61. package/dist/docx/sectionReferenceHistory.d.ts +17 -0
  62. package/dist/docx/sectionReferenceHistory.js +59 -0
  63. package/dist/docx/selectiveSave.js +11 -31
  64. package/dist/docx/selectiveXmlPatch.d.ts +8 -1
  65. package/dist/docx/selectiveXmlPatch.js +62 -2
  66. package/dist/docx/serializer/paragraphSerializer.js +2 -0
  67. package/dist/docx/serializer/partNamespaces.d.ts +1 -1
  68. package/dist/docx/serializer/partNamespaces.js +4 -0
  69. package/dist/docx/serializer/runSerializer.js +2 -1
  70. package/dist/docx/serializer/sectionPropertiesSerializer.js +5 -2
  71. package/dist/docx/serializer/textFormattingSerializer.d.ts +1 -1
  72. package/dist/docx/serializer/textFormattingSerializer.js +3 -1
  73. package/dist/docx/tableParser.js +21 -11
  74. package/dist/docx/xmlParser.d.ts +10 -1
  75. package/dist/docx/xmlParser.js +23 -1
  76. package/dist/index.d.ts +4 -4
  77. package/dist/index.js +3 -3
  78. package/dist/internal/compare/inline-presentation.d.ts +20 -0
  79. package/dist/internal/compare/inline-presentation.js +222 -0
  80. package/dist/internal/paragraphFormattingSerialization.d.ts +3 -1
  81. package/dist/internal/paragraphFormattingSerialization.js +8 -1
  82. package/dist/internal/sectionReferenceResolution.d.ts +21 -0
  83. package/dist/internal/sectionReferenceResolution.js +72 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  85. package/dist/markdown/renderRuns.js +1 -0
  86. package/dist/markdown/renderTable.js +1 -0
  87. package/dist/prosemirror/attrs/index.js +20 -1
  88. package/dist/prosemirror/commands/comments.d.ts +11 -1
  89. package/dist/prosemirror/commands/comments.js +68 -6
  90. package/dist/prosemirror/commands/propertyChangeScope.d.ts +9 -3
  91. package/dist/prosemirror/commands/propertyChangeScope.js +4 -10
  92. package/dist/prosemirror/conversion/fromProseDoc.js +16 -8
  93. package/dist/prosemirror/conversion/toProseDoc.js +16 -57
  94. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  95. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +3 -0
  96. package/dist/prosemirror/imageCommit.js +7 -9
  97. package/dist/prosemirror/paragraphIndentation.d.ts +21 -0
  98. package/dist/prosemirror/paragraphIndentation.js +72 -0
  99. package/dist/prosemirror/plugins/documentNumbering.d.ts +3 -1
  100. package/dist/prosemirror/plugins/documentNumbering.js +7 -1
  101. package/dist/prosemirror/plugins/documentStyles.d.ts +3 -1
  102. package/dist/prosemirror/plugins/documentStyles.js +7 -1
  103. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +3 -1
  104. package/dist/prosemirror/rebaseParagraphRunFormatting.js +2 -1
  105. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +15 -2
  106. package/dist/prosemirror/runFormattingInlineCarriers.js +33 -3
  107. package/dist/prosemirror/runStyleFormatting.d.ts +15 -2
  108. package/dist/prosemirror/runStyleFormatting.js +76 -2
  109. package/dist/prosemirror/schema/marks.d.ts +3 -1
  110. package/dist/prosemirror/schema/marks.js +2 -0
  111. package/dist/prosemirror/schema/nodes.d.ts +3 -0
  112. package/dist/prosemirror/tableCellRevisionVisibility.d.ts +6 -0
  113. package/dist/prosemirror/tableCellRevisionVisibility.js +14 -0
  114. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  115. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  116. package/dist/redline.js +15 -2
  117. package/dist/server.d.ts +2 -2
  118. package/dist/server.js +2 -2
  119. package/dist/version-comparison.d.ts +2 -0
  120. package/dist/version-comparison.js +7 -2
  121. package/package.json +2 -2
@@ -19,7 +19,24 @@ type FolioContentInlineFormatting = Partial<Record<FolioContentInlineBooleanProp
19
19
  * An inline-formatting mutation: `false` is an explicit off value, while
20
20
  * `null` removes the direct property so its inherited value becomes effective.
21
21
  */
22
- type FolioContentInlineFormattingPatch = Omit<FolioContentInlineFormatting, FolioContentInlineBooleanProperty> & Partial<Record<FolioContentInlineBooleanProperty, boolean | null>>;
22
+ type FolioContentInlineFormattingPatch = { [Property in keyof FolioContentInlineFormatting]?: FolioContentInlineFormatting[Property] | null; };
23
+ /** Presentation differences for one UTF-16 range in a text-aligned block pair. */
24
+ type FolioContentFormatRange = {
25
+ startOffset: number;
26
+ endOffset: number;
27
+ formatting: FolioContentInlineFormattingPatch;
28
+ };
29
+ /** A trusted inline projection or an explicit reason no ranges can be used. */
30
+ type FolioContentInlineComparisonResult = {
31
+ status: "compared";
32
+ segments: readonly FolioContentFormatRange[];
33
+ } | {
34
+ status: "unalignable";
35
+ side: "base" | "revised" | "both";
36
+ } | {
37
+ status: "budget-exceeded";
38
+ maximum: number;
39
+ };
23
40
  type FolioContentRun = {
24
41
  text: string;
25
42
  bold?: boolean;
@@ -61,6 +78,18 @@ type FolioContentTableLocation = {
61
78
  type FolioContentParagraphAlignment = "left" | "center" | "right" | "both" | "distribute" | "mediumKashida" | "highKashida" | "lowKashida" | "thaiDistribute";
62
79
  /** Line-height interpretation understood by the neutral comparison model. */
63
80
  type FolioContentLineSpacingRule = "auto" | "exact" | "atLeast";
81
+ /** The complete modeled attribute set of direct paragraph indentation. */
82
+ type FolioContentParagraphIndentation = {
83
+ indentLeft?: number;
84
+ indentRight?: number;
85
+ indentFirstLine?: number;
86
+ hangingIndent?: boolean;
87
+ };
88
+ /** A concrete numbering instance and its zero-based level. */
89
+ type FolioContentListReference = {
90
+ numId: number;
91
+ level: number;
92
+ };
64
93
  /**
65
94
  * The complete modeled attribute set of direct paragraph spacing. Optional
66
95
  * fields distinguish an absent attribute from an explicit zero or false value.
@@ -87,8 +116,11 @@ type FolioContentBlock<Kind extends string = string> = {
87
116
  directAlignment?: FolioContentParagraphAlignment;
88
117
  /** Direct paragraph spacing; absent when every spacing value is inherited. */
89
118
  directSpacing?: FolioContentParagraphSpacing;
119
+ /** Direct paragraph indentation; absent when every indentation value is inherited. */
120
+ directIndentation?: FolioContentParagraphIndentation;
90
121
  /** Zero-based list indent level when the block carries numbering. */
91
122
  listLevel?: number;
123
+ listReference?: FolioContentListReference;
92
124
  previewRuns?: readonly FolioContentRun[];
93
125
  table?: FolioContentTableLocation;
94
126
  /** Structural ancestry, ordered from the outermost to the innermost container. */
@@ -99,4 +131,4 @@ type FolioContentSnapshot<Block extends FolioContentBlock = FolioContentBlock> =
99
131
  blocks: readonly Block[];
100
132
  };
101
133
  //#endregion
102
- export { FolioContentBlock, FolioContentContainerPathEntry, FolioContentIdStability, FolioContentInlineBooleanProperty, FolioContentInlineFormatting, FolioContentInlineFormattingPatch, FolioContentLineSpacingRule, FolioContentParagraphAlignment, FolioContentParagraphKind, FolioContentParagraphSpacing, FolioContentRun, FolioContentSnapshot, FolioContentTableLocation };
134
+ export { FolioContentBlock, FolioContentContainerPathEntry, FolioContentFormatRange, FolioContentIdStability, FolioContentInlineBooleanProperty, FolioContentInlineComparisonResult, FolioContentInlineFormatting, FolioContentInlineFormattingPatch, FolioContentLineSpacingRule, FolioContentListReference, FolioContentParagraphAlignment, FolioContentParagraphIndentation, FolioContentParagraphKind, FolioContentParagraphSpacing, FolioContentRun, FolioContentSnapshot, FolioContentTableLocation };
@@ -1,4 +1,4 @@
1
- import { FolioContentBlock, FolioContentIdStability, FolioContentInlineFormattingPatch, FolioContentParagraphSpacing, FolioContentSnapshot } from "./content-types.js";
1
+ import { FolioContentBlock, FolioContentFormatRange, FolioContentIdStability, FolioContentListReference, FolioContentParagraphIndentation, FolioContentParagraphSpacing, FolioContentSnapshot } from "./content-types.js";
2
2
  import { WordDiffGranularity, createWordDiffSession } from "../ai-edits/word-diff.js";
3
3
  import { FolioContentAlignmentStep, FolioContentAlignmentWorkSession } from "./content-alignment.js";
4
4
  import { Result } from "better-result";
@@ -34,8 +34,15 @@ declare class FolioContentComparisonLimitError extends FolioContentComparisonLim
34
34
  blockIndex?: number;
35
35
  field?: string;
36
36
  }> {}
37
+ declare const FolioContentInlinePresentationProjectionError_base: import("better-result").TaggedErrorClass<"FolioContentInlinePresentationProjectionError">;
38
+ declare class FolioContentInlinePresentationProjectionError extends FolioContentInlinePresentationProjectionError_base<{
39
+ message: string;
40
+ side: "base" | "revised" | "both";
41
+ baseBlockId: string;
42
+ revisedBlockId: string;
43
+ }> {}
37
44
  /** Errors returned by {@link compareContent}. */
38
- type FolioContentComparisonError = InvalidFolioContentComparisonError | FolioContentComparisonLimitError;
45
+ type FolioContentComparisonError = InvalidFolioContentComparisonError | FolioContentComparisonLimitError | FolioContentInlinePresentationProjectionError;
39
46
  /** One Folio word-diff segment with JavaScript-slice-compatible offsets. */
40
47
  type FolioContentTextSegment = {
41
48
  type: "equal" | "del" | "ins";
@@ -49,14 +56,10 @@ type FolioContentTextSegment = {
49
56
  type FolioContentParagraphFormattingPatch = {
50
57
  styleId?: string | null;
51
58
  listLevel?: number | null;
59
+ listReference?: FolioContentListReference | null;
52
60
  alignment?: FolioContentBlock["directAlignment"] | null;
53
61
  spacing?: FolioContentParagraphSpacing | null;
54
- };
55
- /** Presentation differences for one text-aligned block pair. */
56
- type FolioContentFormatRange = {
57
- startOffset: number;
58
- endOffset: number;
59
- formatting: FolioContentInlineFormattingPatch;
62
+ indentation?: FolioContentParagraphIndentation | null;
60
63
  };
61
64
  /** Presentation differences for one text-aligned block pair. */
62
65
  type FolioContentFormattingChange = {
@@ -202,8 +205,8 @@ type CompareAlignedContentOptions<Block extends FolioContentBlock> = {
202
205
  maxChanges: number;
203
206
  idStability?: (block: Block) => FolioContentIdStability;
204
207
  };
205
- declare const compareAlignedFolioContent: <Block extends FolioContentBlock>({ baseBlocks, revisedBlocks, steps, workSession, maxChanges, idStability }: CompareAlignedContentOptions<Block>) => Result<FolioContentComparison<Block>, FolioContentComparisonLimitError>;
208
+ declare const compareAlignedFolioContent: <Block extends FolioContentBlock>({ baseBlocks, revisedBlocks, steps, workSession, maxChanges, idStability }: CompareAlignedContentOptions<Block>) => Result<FolioContentComparison<Block>, FolioContentComparisonLimitError | FolioContentInlinePresentationProjectionError>;
206
209
  /** Compare two representation-neutral ordered content snapshots. */
207
210
  declare const compareContent: <Block extends FolioContentBlock = FolioContentBlock>(options: CompareContentOptions<Block>) => Result<FolioContentComparison<Block>, FolioContentComparisonError>;
208
211
  //#endregion
209
- export { CompareContentOptions, FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentBlockProperty, FolioContentComparison, FolioContentComparisonError, FolioContentComparisonEvent, FolioContentComparisonLimit, FolioContentComparisonLimitError, FolioContentComparisonWorkSession, FolioContentFormatRange, FolioContentFormattingChange, FolioContentParagraphFormattingPatch, FolioContentStructuralChange, FolioContentTextSegment, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };
212
+ export { CompareContentOptions, FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentBlockProperty, FolioContentComparison, FolioContentComparisonError, FolioContentComparisonEvent, FolioContentComparisonLimit, FolioContentComparisonLimitError, FolioContentComparisonWorkSession, type FolioContentFormatRange, FolioContentFormattingChange, FolioContentInlinePresentationProjectionError, FolioContentParagraphFormattingPatch, FolioContentStructuralChange, FolioContentTextSegment, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };
@@ -1,4 +1,5 @@
1
1
  import { WORD_DIFF_GRANULARITIES, createWordDiffSession } from "../ai-edits/word-diff.js";
2
+ import { paragraphIndentationEqual } from "../prosemirror/paragraphIndentation.js";
2
3
  import { alignFolioContentStructure, contentBlocksShareContainer, createFolioContentAlignmentWorkSession } from "./content-alignment.js";
3
4
  import { inlineFormattingSegments } from "./formatting.js";
4
5
  import { Result, TaggedError, panic } from "better-result";
@@ -40,6 +41,7 @@ const MAX_MOVE_PROFILE_TOKENS = 16384;
40
41
  const MAX_MOVE_PROFILE_CODE_UNITS = 1048576;
41
42
  var InvalidFolioContentComparisonError = class extends TaggedError("InvalidFolioContentComparisonError") {};
42
43
  var FolioContentComparisonLimitError = class extends TaggedError("FolioContentComparisonLimitError") {};
44
+ var FolioContentInlinePresentationProjectionError = class extends TaggedError("FolioContentInlinePresentationProjectionError") {};
43
45
  const createContentComparisonWorkSession = (granularity) => ({
44
46
  alignment: createFolioContentAlignmentWorkSession(),
45
47
  remainingMoveComparisons: MAX_MOVE_SIMILARITY_COMPARISONS,
@@ -416,9 +418,13 @@ const paragraphSpacingEqual = (base, revised) => base?.spaceBefore === revised?.
416
418
  const changedFolioContentParagraphFormatting = (base, revised) => {
417
419
  const patch = {};
418
420
  if ((base.styleId ?? null) !== (revised.styleId ?? null)) patch.styleId = revised.styleId ?? null;
419
- if (base.listLevel !== revised.listLevel) patch.listLevel = revised.listLevel ?? null;
421
+ const listReferenceChanged = base.listReference?.numId !== revised.listReference?.numId;
422
+ const targetOmitsLevelOnChangedReference = listReferenceChanged && revised.listReference !== void 0 && revised.listLevel === void 0;
423
+ if (base.listLevel !== revised.listLevel || targetOmitsLevelOnChangedReference) patch.listLevel = revised.listLevel ?? null;
424
+ if (listReferenceChanged || base.listLevel !== revised.listLevel && revised.listLevel === void 0 && revised.listReference !== void 0) patch.listReference = revised.listReference ?? null;
420
425
  if (base.directAlignment !== revised.directAlignment) patch.alignment = revised.directAlignment ?? null;
421
426
  if (!paragraphSpacingEqual(base.directSpacing, revised.directSpacing)) patch.spacing = revised.directSpacing ?? null;
427
+ if (!paragraphIndentationEqual(base.directIndentation, revised.directIndentation)) patch.indentation = revised.directIndentation ?? null;
422
428
  return Object.keys(patch).length > 0 ? patch : null;
423
429
  };
424
430
  const changedBlockProperties = (base, revised) => {
@@ -640,16 +646,37 @@ const structuralChangeForStep = (step, id) => {
640
646
  };
641
647
  const formattingChange = (base, revised, maxRanges) => {
642
648
  const paragraph = changedFolioContentParagraphFormatting(base, revised);
643
- const ranges = base.text === revised.text ? inlineFormattingSegments({
649
+ const inlineComparison = base.text === revised.text ? inlineFormattingSegments({
644
650
  baseBlock: base,
645
651
  targetBlock: revised,
646
652
  maxSegments: maxRanges
647
- }) : [];
648
- if (ranges === null) return "limit";
649
- return paragraph || ranges.length > 0 ? {
650
- ...paragraph && { paragraph },
651
- ranges
652
- } : null;
653
+ }) : {
654
+ status: "compared",
655
+ segments: []
656
+ };
657
+ switch (inlineComparison.status) {
658
+ case "compared": {
659
+ const { segments: ranges } = inlineComparison;
660
+ return {
661
+ status: "compared",
662
+ formatting: paragraph || ranges.length > 0 ? {
663
+ ...paragraph && { paragraph },
664
+ ranges
665
+ } : null
666
+ };
667
+ }
668
+ case "budget-exceeded": return { status: "budget-exceeded" };
669
+ case "unalignable": return {
670
+ status: "unalignable",
671
+ error: new FolioContentInlinePresentationProjectionError({
672
+ message: "Inline formatting runs cannot be aligned to their block text.",
673
+ side: inlineComparison.side,
674
+ baseBlockId: base.id,
675
+ revisedBlockId: revised.id
676
+ })
677
+ };
678
+ default: return panic("Unhandled inline formatting comparison result", { result: inlineComparison });
679
+ }
653
680
  };
654
681
  const compareAlignedFolioContent = ({ baseBlocks, revisedBlocks, steps, workSession, maxChanges, idStability }) => {
655
682
  const paragraphPlans = detectFolioContentParagraphMarkPlans(steps);
@@ -733,13 +760,21 @@ const compareAlignedFolioContent = ({ baseBlocks, revisedBlocks, steps, workSess
733
760
  const base = step.baseBlock;
734
761
  const revised = step.revisedBlock;
735
762
  const properties = changedBlockProperties(base, revised);
736
- const formatting = formattingChange(base, revised, remainingFormattingRanges);
737
- if (formatting === "limit") return Result.err(limitExceeded({
738
- input: "result",
739
- limit: "changes",
740
- maximum: maxChanges,
741
- actual: maxChanges + 1
742
- }));
763
+ const formattingResult = formattingChange(base, revised, remainingFormattingRanges);
764
+ let formatting;
765
+ switch (formattingResult.status) {
766
+ case "compared":
767
+ formatting = formattingResult.formatting;
768
+ break;
769
+ case "budget-exceeded": return Result.err(limitExceeded({
770
+ input: "result",
771
+ limit: "changes",
772
+ maximum: maxChanges,
773
+ actual: maxChanges + 1
774
+ }));
775
+ case "unalignable": return Result.err(formattingResult.error);
776
+ default: return panic("Unhandled formatting comparison result", { result: formattingResult });
777
+ }
743
778
  remainingFormattingRanges -= formatting?.ranges.length ?? 0;
744
779
  let event;
745
780
  if (base.text !== revised.text || properties.length > 0) event = {
@@ -780,13 +815,24 @@ const compareAlignedFolioContent = ({ baseBlocks, revisedBlocks, steps, workSess
780
815
  }
781
816
  if (step.type === "revisedOnly") {
782
817
  const move = moveByRevisedId.get(step.block.id);
783
- const moveFormatting = move ? formattingChange(move.baseBlock, step.block, remainingFormattingRanges) : null;
784
- if (moveFormatting === "limit") return Result.err(limitExceeded({
785
- input: "result",
786
- limit: "changes",
787
- maximum: maxChanges,
788
- actual: maxChanges + 1
789
- }));
818
+ const moveFormattingResult = move ? formattingChange(move.baseBlock, step.block, remainingFormattingRanges) : {
819
+ status: "compared",
820
+ formatting: null
821
+ };
822
+ let moveFormatting;
823
+ switch (moveFormattingResult.status) {
824
+ case "compared":
825
+ moveFormatting = moveFormattingResult.formatting;
826
+ break;
827
+ case "budget-exceeded": return Result.err(limitExceeded({
828
+ input: "result",
829
+ limit: "changes",
830
+ maximum: maxChanges,
831
+ actual: maxChanges + 1
832
+ }));
833
+ case "unalignable": return Result.err(moveFormattingResult.error);
834
+ default: return panic("Unhandled formatting comparison result", { result: moveFormattingResult });
835
+ }
790
836
  remainingFormattingRanges -= moveFormatting?.ranges.length ?? 0;
791
837
  const moveProperties = move ? changedBlockProperties(move.baseBlock, step.block) : [];
792
838
  if (!addRelation(move ? {
@@ -882,4 +928,4 @@ const compareContent = (options) => {
882
928
  });
883
929
  };
884
930
  //#endregion
885
- export { FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentComparisonLimitError, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };
931
+ export { FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentComparisonLimitError, FolioContentInlinePresentationProjectionError, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };
@@ -1,29 +1,11 @@
1
- import { FolioContentBlock, FolioContentInlineFormattingPatch } from "./content-types.js";
1
+ import { FolioContentBlock, FolioContentInlineComparisonResult } from "./content-types.js";
2
2
  //#region src/compare/formatting.d.ts
3
- /** One run of characters whose supported inline formatting differs. */
4
- type InlineFormattingSegment = {
5
- /** Zero-based UTF-16 offset into the block's visible text. */
6
- startOffset: number;
7
- endOffset: number;
8
- /** Differing properties set to the target value; null removes a direct property. */
9
- formatting: FolioContentInlineFormattingPatch;
10
- };
11
3
  type InlineFormattingSegmentsOptions = {
12
4
  baseBlock: FolioContentBlock;
13
5
  targetBlock: FolioContentBlock;
14
- /** Refuse (return `null`) rather than build more segments than this. */
15
6
  maxSegments: number;
16
7
  };
17
- /**
18
- * Segments where `targetBlock`'s supported run formatting differs from
19
- * `baseBlock`'s, for two blocks that carry the same text. Returns `null` only
20
- * when the diff would exceed `maxSegments`.
21
- *
22
- * A block whose runs cannot be aligned to its text reports no segments rather
23
- * than guessing: attributing formatting to the wrong characters is worse than
24
- * missing a formatting-only change, and the caller has no offset it could
25
- * trust instead.
26
- */
27
- declare const inlineFormattingSegments: ({ baseBlock, targetBlock, maxSegments }: InlineFormattingSegmentsOptions) => InlineFormattingSegment[] | null;
8
+ /** Malformed run streams take precedence over the output budget. */
9
+ declare const inlineFormattingSegments: ({ baseBlock, targetBlock, maxSegments }: InlineFormattingSegmentsOptions) => FolioContentInlineComparisonResult;
28
10
  //#endregion
29
- export { InlineFormattingSegment, inlineFormattingSegments };
11
+ export { inlineFormattingSegments };
@@ -1,115 +1,11 @@
1
+ import { canonicalInlinePresentationSegments } from "../internal/compare/inline-presentation.js";
1
2
  //#region src/compare/formatting.ts
2
- const HEX_COLOR = /^#?(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/u;
3
- const normalizeInlineFormattingColor = (color) => HEX_COLOR.test(color) ? color.replace(/^#/u, "").toUpperCase() : color;
4
- const normalizeEffectiveInlineFormattingColor = (color) => color === void 0 ? void 0 : normalizeInlineFormattingColor(color);
5
- const normalizeDirectInlineFormattingColor = (color) => color === null || color === void 0 ? color : normalizeInlineFormattingColor(color);
6
- const changedStringValue = (baseEffective, targetEffective, baseAuthored, targetAuthored) => {
7
- if (baseAuthored !== targetAuthored) return targetAuthored ?? null;
8
- return baseEffective === targetEffective ? void 0 : targetEffective ?? null;
9
- };
10
- const changedNumberValue = (baseEffective, targetEffective, baseAuthored, targetAuthored) => {
11
- if (baseAuthored !== targetAuthored) return targetAuthored ?? null;
12
- return baseEffective === targetEffective ? void 0 : targetEffective ?? null;
13
- };
14
- const changedSupportedFormatting = (base, target) => {
15
- const baseDirect = base.directFormatting ?? {};
16
- const targetDirect = target.directFormatting ?? {};
17
- const formatting = {};
18
- const changedBoolean = (property) => {
19
- if (baseDirect[property] !== targetDirect[property]) return targetDirect[property] ?? null;
20
- return Boolean(base[property]) === Boolean(target[property]) ? void 0 : Boolean(target[property]);
21
- };
22
- for (const property of [
23
- "bold",
24
- "italic",
25
- "underline",
26
- "strike"
27
- ]) {
28
- const changed = changedBoolean(property);
29
- if (changed !== void 0) formatting[property] = changed;
30
- }
31
- const fontFamily = changedStringValue(base.fontFamily, target.fontFamily, baseDirect.fontFamily, targetDirect.fontFamily);
32
- if (fontFamily !== void 0) formatting.fontFamily = fontFamily;
33
- const fontSizePt = changedNumberValue(base.fontSizePt, target.fontSizePt, baseDirect.fontSizePt, targetDirect.fontSizePt);
34
- if (fontSizePt !== void 0) formatting.fontSizePt = fontSizePt;
35
- const color = changedStringValue(normalizeEffectiveInlineFormattingColor(base.color), normalizeEffectiveInlineFormattingColor(target.color), normalizeDirectInlineFormattingColor(baseDirect.color), normalizeDirectInlineFormattingColor(targetDirect.color));
36
- if (color !== void 0) formatting.color = color;
37
- return formatting;
38
- };
39
- const sameInlineFormatting = (left, right) => left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.strike === right.strike && left.fontFamily === right.fontFamily && left.fontSizePt === right.fontSizePt && left.color === right.color;
40
- const hasInlineFormatting = (formatting) => formatting.bold !== void 0 || formatting.italic !== void 0 || formatting.underline !== void 0 || formatting.strike !== void 0 || formatting.fontFamily !== void 0 || formatting.fontSizePt !== void 0 || formatting.color !== void 0;
41
- /**
42
- * A block's runs, or `null` when they cannot describe the block's text.
43
- * Non-text inline content (a field, an image) leaves the concatenated run text
44
- * shorter than the block text; attributing formatting by offset would then
45
- * point at the wrong characters, so the caller must back off instead.
46
- */
47
- const previewRunsForBlock = (block) => {
48
- const runs = block.previewRuns ?? [{ text: block.text }];
49
- return runs.map(({ text }) => text).join("") === block.text ? runs : null;
50
- };
51
- /**
52
- * Segments where `targetBlock`'s supported run formatting differs from
53
- * `baseBlock`'s, for two blocks that carry the same text. Returns `null` only
54
- * when the diff would exceed `maxSegments`.
55
- *
56
- * A block whose runs cannot be aligned to its text reports no segments rather
57
- * than guessing: attributing formatting to the wrong characters is worse than
58
- * missing a formatting-only change, and the caller has no offset it could
59
- * trust instead.
60
- */
61
- const inlineFormattingSegments = ({ baseBlock, targetBlock, maxSegments }) => {
62
- const baseRuns = previewRunsForBlock(baseBlock);
63
- const targetRuns = previewRunsForBlock(targetBlock);
64
- if (!baseRuns || !targetRuns || baseBlock.text.length === 0) return [];
65
- const segments = [];
66
- let baseRunIndex = 0;
67
- let targetRunIndex = 0;
68
- let baseRunOffset = 0;
69
- let targetRunOffset = 0;
70
- let textOffset = 0;
71
- while (baseRunIndex < baseRuns.length && targetRunIndex < targetRuns.length) {
72
- const baseRun = baseRuns[baseRunIndex];
73
- const targetRun = targetRuns[targetRunIndex];
74
- if (!baseRun || !targetRun) break;
75
- const length = Math.min(baseRun.text.length - baseRunOffset, targetRun.text.length - targetRunOffset);
76
- if (length <= 0) {
77
- if (baseRunOffset >= baseRun.text.length) {
78
- baseRunIndex++;
79
- baseRunOffset = 0;
80
- }
81
- if (targetRunOffset >= targetRun.text.length) {
82
- targetRunIndex++;
83
- targetRunOffset = 0;
84
- }
85
- continue;
86
- }
87
- const formatting = changedSupportedFormatting(baseRun, targetRun);
88
- if (hasInlineFormatting(formatting)) {
89
- const previous = segments.at(-1);
90
- if (previous && previous.endOffset === textOffset && sameInlineFormatting(previous.formatting, formatting)) previous.endOffset += length;
91
- else {
92
- if (segments.length >= maxSegments) return null;
93
- segments.push({
94
- startOffset: textOffset,
95
- endOffset: textOffset + length,
96
- formatting
97
- });
98
- }
99
- }
100
- textOffset += length;
101
- baseRunOffset += length;
102
- targetRunOffset += length;
103
- if (baseRunOffset >= baseRun.text.length) {
104
- baseRunIndex++;
105
- baseRunOffset = 0;
106
- }
107
- if (targetRunOffset >= targetRun.text.length) {
108
- targetRunIndex++;
109
- targetRunOffset = 0;
110
- }
111
- }
112
- return segments;
113
- };
3
+ /** Character-aligned inline-formatting comparison of two text-equal blocks. */
4
+ /** Malformed run streams take precedence over the output budget. */
5
+ const inlineFormattingSegments = ({ baseBlock, targetBlock, maxSegments }) => canonicalInlinePresentationSegments({
6
+ baseBlock,
7
+ revisedBlock: targetBlock,
8
+ maxSegments
9
+ });
114
10
  //#endregion
115
11
  export { inlineFormattingSegments };
@@ -0,0 +1,6 @@
1
+ import { Node } from "prosemirror-model";
2
+ //#region src/compare/inline-atom-resources.d.ts
3
+ /** Detach supported atoms from the source package; save allocates the receiving media relationship. */
4
+ declare const prepareTargetInlineAtom: (node: Node) => Node | null;
5
+ //#endregion
6
+ export { prepareTargetInlineAtom };
@@ -0,0 +1,26 @@
1
+ import { rebindDrawingImageRelationship } from "../docx/drawingRelationships.js";
2
+ import { expectImageAttrs } from "../prosemirror/attrs/index.js";
3
+ //#region src/compare/inline-atom-resources.ts
4
+ /** Detach supported atoms from the source package; save allocates the receiving media relationship. */
5
+ const prepareTargetInlineAtom = (node) => {
6
+ if (node.type.name === "field") return node;
7
+ if (node.type.name === "pageBreakRun") return node;
8
+ if (node.type.name !== "image") return null;
9
+ const attrs = expectImageAttrs(node);
10
+ if (!attrs.src.startsWith("data:") || attrs.hlinkRId || attrs.hlinkHref) return null;
11
+ const rId = "rId_img_compare";
12
+ const rawXml = attrs._docxRawXml;
13
+ const reboundXml = rawXml ? rebindDrawingImageRelationship({
14
+ xml: rawXml,
15
+ previousId: attrs.rId ?? "",
16
+ nextId: rId
17
+ }) : void 0;
18
+ if (reboundXml === null) return null;
19
+ return node.type.create({
20
+ ...node.attrs,
21
+ rId,
22
+ ...reboundXml !== void 0 && { _docxRawXml: reboundXml }
23
+ }, node.content, node.marks);
24
+ };
25
+ //#endregion
26
+ export { prepareTargetInlineAtom };
@@ -0,0 +1,35 @@
1
+ import { FolioAIEditSnapshot } from "../ai-edits/types.js";
2
+ import { FolioRevisionStamp } from "../ai-edits/apply.js";
3
+ import { EditorState, Transaction } from "prosemirror-state";
4
+ import { Node } from "prosemirror-model";
5
+ //#region src/compare/inline-atoms.d.ts
6
+ type MatchInlineAtomsOptions = {
7
+ state: EditorState;
8
+ targetSnapshot: FolioAIEditSnapshot;
9
+ revisionStamp: FolioRevisionStamp;
10
+ /** First id reserved for this comparison, before planned operations allocated ids. */
11
+ originalRevisionIdSeed: number;
12
+ author: string;
13
+ maxRanges: number;
14
+ };
15
+ type MatchInlineAtomsResult = {
16
+ status: "matched";
17
+ transaction: Transaction;
18
+ nextRevisionId: number;
19
+ changedTargetBlockIds: readonly string[];
20
+ rangeCount: number;
21
+ } | {
22
+ status: "unalignable";
23
+ } | {
24
+ status: "budget-exceeded";
25
+ };
26
+ /**
27
+ * Restore field, image, and page-break atoms omitted by text-only comparison operations.
28
+ * Positions come from the legacy review resolver's mapping, never a guessed
29
+ * paragraph correspondence: paragraph-mark deletions can merge paragraphs.
30
+ */
31
+ declare const matchInlineAtoms: ({ state, targetSnapshot, revisionStamp, originalRevisionIdSeed, author, maxRanges }: MatchInlineAtomsOptions) => MatchInlineAtomsResult;
32
+ /** Compare supported inline atom identity after accept or reject projection. */
33
+ declare const sameInlineAtoms: (leftDocument: Node, rightDocument: Node) => boolean;
34
+ //#endregion
35
+ export { MatchInlineAtomsOptions, MatchInlineAtomsResult, matchInlineAtoms, sameInlineAtoms };