@stll/folio-core 0.36.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/dist/ai-edits/apply.js +689 -367
- package/dist/ai-edits/block-identity.d.ts +4 -1
- package/dist/ai-edits/block-identity.js +4 -1
- package/dist/ai-edits/blockRange.js +14 -14
- package/dist/ai-edits/clean-text.d.ts +40 -1
- package/dist/ai-edits/clean-text.js +52 -2
- package/dist/ai-edits/headless.d.ts +3 -1
- package/dist/ai-edits/headless.js +133 -71
- package/dist/ai-edits/index.d.ts +3 -3
- package/dist/ai-edits/index.js +2 -2
- package/dist/ai-edits/read.js +38 -20
- package/dist/ai-edits/snapshot.d.ts +14 -6
- package/dist/ai-edits/snapshot.js +99 -19
- package/dist/ai-edits/table-cell-mutations.js +1 -1
- package/dist/ai-edits/table-row-column-mutations.js +7 -0
- package/dist/ai-edits/table-template.d.ts +11 -1
- package/dist/ai-edits/table-template.js +49 -17
- package/dist/ai-edits/types.d.ts +60 -57
- package/dist/ai-suggestions/conflict.js +1 -4
- package/dist/ai-suggestions/text-positions.d.ts +8 -0
- package/dist/ai-suggestions/text-positions.js +27 -5
- package/dist/compare/__fixtures__/body-sequence.js +3 -4
- package/dist/compare/column-alignment.d.ts +13 -13
- package/dist/compare/column-alignment.js +78 -39
- package/dist/compare/compare.d.ts +4 -3
- package/dist/compare/compare.js +41 -5
- package/dist/compare/content-alignment.d.ts +95 -0
- package/dist/compare/content-alignment.js +1450 -0
- package/dist/compare/content-types.d.ts +100 -0
- package/dist/compare/content-types.js +0 -0
- package/dist/compare/content.d.ts +209 -0
- package/dist/compare/content.js +885 -0
- package/dist/compare/formatting.d.ts +5 -5
- package/dist/compare/formatting.js +6 -4
- package/dist/compare/plan.d.ts +6 -1
- package/dist/compare/plan.js +201 -569
- package/dist/compare/scenario.d.ts +2 -2
- package/dist/compare/types.d.ts +3 -3
- package/dist/compare/verification.d.ts +2 -2
- package/dist/compare/verification.js +25 -8
- package/dist/compat/eigenpal.d.ts +5 -3
- package/dist/compat/eigenpal.js +3 -2
- package/dist/controller/headerFooterEditorManager.js +14 -6
- package/dist/controller/hiddenEditorManager.d.ts +7 -2
- package/dist/controller/hiddenEditorManager.js +15 -6
- package/dist/controller/layoutPipeline.js +5 -3
- package/dist/controller/noteEditorManager.js +12 -7
- package/dist/display-list/build/headerFooterPrimitives.js +1 -0
- package/dist/display-list/build/storyPrimitives.d.ts +5 -2
- package/dist/display-list/build/storyPrimitives.js +60 -3
- package/dist/display-list/build/tablePrimitives.js +30 -0
- package/dist/display-list/primitives.d.ts +1 -1
- package/dist/document-operations.d.ts +8 -6
- package/dist/document-operations.js +121 -26
- package/dist/docx/blockContentParser.js +6 -27
- package/dist/docx/ensureParaIds.js +9 -4
- package/dist/docx/numberingParser.d.ts +3 -1
- package/dist/docx/numberingParser.js +4 -1
- package/dist/docx/paragraphParser.js +7 -6
- package/dist/docx/paragraphTraversal.js +30 -34
- package/dist/docx/rezip.d.ts +0 -8
- package/dist/docx/rezip.js +41 -16
- package/dist/docx/runParser.js +4 -2
- package/dist/docx/serializer/paragraphSerializer.js +2 -0
- package/dist/docx/serializer/runSerializer.js +5 -3
- package/dist/docx/serializer/trackedChangeAttributes.d.ts +3 -1
- package/dist/docx/serializer/trackedChangeAttributes.js +11 -1
- package/dist/docx/server/build.d.ts +1 -1
- package/dist/headless-layout.js +5 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/internal/headlessRevisionResolution.d.ts +23 -0
- package/dist/internal/headlessRevisionResolution.js +176 -0
- package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
- package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
- package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
- package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
- package/dist/internal/pageBreakRunPartition.d.ts +26 -0
- package/dist/internal/pageBreakRunPartition.js +35 -0
- package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
- package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
- package/dist/internal/sectionEndpointResolution.d.ts +32 -0
- package/dist/internal/sectionEndpointResolution.js +38 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
- package/dist/layout-bridge/convert/headerFooterLayout.js +48 -5
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
- package/dist/layout-bridge/convert/toFlowBlocks.js +379 -75
- package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
- package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
- package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
- package/dist/layout-bridge/dom/textStreamDom.js +75 -0
- package/dist/layout-bridge/headerFooterLayout.js +16 -30
- package/dist/layout-engine/index.js +12 -3
- package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
- package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
- package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
- package/dist/layout-engine/types.d.ts +7 -0
- package/dist/layout-painter/imageLayout.d.ts +1 -1
- package/dist/layout-painter/renderTable.js +20 -0
- package/dist/model.d.ts +1 -1
- package/dist/paged-layout/rangeProjection.js +6 -9
- package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
- package/dist/paged-layout/transactionDirtyRange.js +23 -1
- package/dist/prosemirror/attrs/index.d.ts +7 -3
- package/dist/prosemirror/attrs/index.js +113 -7
- package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
- package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
- package/dist/prosemirror/commands/comments.d.ts +10 -1
- package/dist/prosemirror/commands/comments.js +167 -59
- package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
- package/dist/prosemirror/commands/formatPainter.js +319 -63
- package/dist/prosemirror/commands/index.d.ts +2 -2
- package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
- package/dist/prosemirror/commands/pageBreak.js +18 -36
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +7 -5
- package/dist/prosemirror/commands/propertyChangeScope.js +27 -18
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +7 -2
- package/dist/prosemirror/conversion/fromProseDoc.js +809 -209
- package/dist/prosemirror/conversion/runShadingMark.js +2 -2
- package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
- package/dist/prosemirror/conversion/toProseDoc.js +416 -283
- package/dist/prosemirror/extensions/StarterKit.js +4 -0
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +12 -8
- package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
- package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
- package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
- package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
- package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
- package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
- package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
- package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
- package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
- package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
- package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
- package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
- package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
- package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
- package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
- package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
- package/dist/prosemirror/findReplaceSelection.js +8 -3
- package/dist/prosemirror/index.d.ts +3 -3
- package/dist/prosemirror/listMarker.d.ts +26 -1
- package/dist/prosemirror/listMarker.js +108 -14
- package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
- package/dist/prosemirror/pageBreakRunProjection.js +47 -0
- package/dist/prosemirror/paraText.js +6 -0
- package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
- package/dist/prosemirror/paragraphSpacing.js +125 -0
- package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
- package/dist/prosemirror/plugins/documentNumbering.js +18 -0
- package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
- package/dist/prosemirror/plugins/pmTextScan.js +14 -4
- package/dist/prosemirror/plugins/suggestionMode.js +8 -10
- package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
- package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
- package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
- package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
- package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
- package/dist/prosemirror/runFormattingProvenance.js +48 -0
- package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
- package/dist/prosemirror/runFormattingReconciliation.js +54 -0
- package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
- package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
- package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
- package/dist/prosemirror/runStyleFormatting.js +105 -0
- package/dist/prosemirror/schema/index.d.ts +3 -3
- package/dist/prosemirror/schema/marks.d.ts +82 -9
- package/dist/prosemirror/schema/marks.js +73 -1
- package/dist/prosemirror/schema/nodes.d.ts +18 -5
- package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +44 -23
- package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
- package/dist/prosemirror/styles/styleResolver.js +3 -3
- package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
- package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
- package/dist/prosemirror/tableGridMutation.d.ts +20 -0
- package/dist/prosemirror/tableGridMutation.js +30 -0
- package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
- package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
- package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
- package/dist/prosemirror/validation.js +23 -4
- package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
- package/dist/render-dom/RenderedDomContext.js +17 -34
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/style-engine/styleEngine.d.ts +1 -1
- package/dist/utils/clipboard.js +1 -1
- package/dist/utils/findReplace.js +38 -21
- package/dist/version-comparison.d.ts +38 -44
- package/dist/version-comparison.js +270 -401
- package/package.json +3 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { folioAIBlockIdStability } from "./ai-edits/block-identity.js";
|
|
1
2
|
import { FolioDocxReviewer } from "./ai-edits/headless.js";
|
|
2
|
-
import {
|
|
3
|
+
import { alignFolioContentBlocks, alignFolioContentStructure, createFolioContentAlignmentWorkSession, exceedsFolioContentLcsBudget } from "./compare/content-alignment.js";
|
|
4
|
+
import { compareAlignedFolioContent, createContentComparisonWorkSession } from "./compare/content.js";
|
|
3
5
|
import { pairFolioDocumentStories } from "./document-stories.js";
|
|
4
6
|
import { FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM, isFolioDocumentPrivacyTransform } from "./docx/metadataPrivacy.js";
|
|
5
|
-
import { getFolioParaIdFromBlockId } from "./types/block-id.js";
|
|
6
7
|
import { TaggedError, panic } from "better-result";
|
|
7
8
|
//#region src/version-comparison.ts
|
|
8
9
|
/**
|
|
@@ -19,25 +20,17 @@ import { TaggedError, panic } from "better-result";
|
|
|
19
20
|
*
|
|
20
21
|
* ## Alignment
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* Compatible body and table/container segments are aligned by the neutral
|
|
24
|
+
* comparison core. Within each compatible segment, blocks are paired in
|
|
25
|
+
* three passes, each only considering blocks the previous pass left unpaired:
|
|
24
26
|
*
|
|
25
|
-
* 1. **Stable-id pairing.** Blocks whose ids are equal
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* TEXT plus its document ordinal. It is structurally indistinguishable
|
|
33
|
-
* from a real paraId ({@link getFolioParaIdFromBlockId} can't tell them
|
|
34
|
-
* apart), but pairing on equality is still safe: two equal deterministic
|
|
35
|
-
* ids necessarily came from identical text at an identical ordinal, so
|
|
36
|
-
* the pair is always text-equal (`unchanged`) — never a false
|
|
37
|
-
* `modified`. What it can't do is FIND a paragraph whose ordinal shifted
|
|
38
|
-
* (an insertion/deletion earlier in the document) even though its text
|
|
39
|
-
* is unchanged: that pair has two different fallback ids and falls
|
|
40
|
-
* through to pass 2.
|
|
27
|
+
* 1. **Stable-id pairing.** Blocks whose ids are equal and whose snapshot
|
|
28
|
+
* provenance marks those ids stable are paired directly. A source
|
|
29
|
+
* `w14:paraId` is stable identity independent of text, so an equal-id pair
|
|
30
|
+
* with different text is a genuine edit (`modified`). Deterministically
|
|
31
|
+
* synthesized ids and `seq-NNNN` fallbacks are positional instead: the
|
|
32
|
+
* adapter preserves that provenance so an ordinal shift falls through to
|
|
33
|
+
* exact-text alignment rather than becoming a false identity match.
|
|
41
34
|
* 2. **Exact-text pairing.** An order-preserving LCS over remaining blocks,
|
|
42
35
|
* matched by exact text equality. This is what recovers same-text blocks
|
|
43
36
|
* that pass 1 missed because a fallback id shifted with the ordinal. Its
|
|
@@ -61,13 +54,10 @@ import { TaggedError, panic } from "better-result";
|
|
|
61
54
|
*
|
|
62
55
|
* Relocated content would otherwise report as an unrelated `deleted` +
|
|
63
56
|
* `added` pair (both order-preserving passes drop crossing matches by
|
|
64
|
-
* design).
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* headings) from pairing as spurious moves. Blocks a positional zip already
|
|
69
|
-
* mis-paired as `modified` are out of this pass's reach — a known limitation
|
|
70
|
-
* of the gap fallback, not of the move pass.
|
|
57
|
+
* design). The neutral comparison core re-classifies eligible exact and
|
|
58
|
+
* closely edited pairs as `movedFrom` / `movedTo` entries sharing a
|
|
59
|
+
* `moveGroupId`. Candidate counts and similarity work share bounded budgets
|
|
60
|
+
* across every story in the package comparison.
|
|
71
61
|
*
|
|
72
62
|
* ## Format-only changes
|
|
73
63
|
*
|
|
@@ -89,319 +79,58 @@ const isFolioVersionComparisonScope = (value) => FOLIO_VERSION_COMPARISON_SCOPES
|
|
|
89
79
|
var InvalidFolioVersionComparisonOptionsError = class extends TaggedError("InvalidFolioVersionComparisonOptionsError") {};
|
|
90
80
|
const FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS = FOLIO_DOCUMENT_PRIVACY_TRANSFORMS;
|
|
91
81
|
const isFolioVersionComparisonPrivacyTransform = (value) => isFolioDocumentPrivacyTransform(value);
|
|
92
|
-
/**
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
];
|
|
102
|
-
const isStableBlockId = (id) => getFolioParaIdFromBlockId(id) !== null;
|
|
103
|
-
/**
|
|
104
|
-
* Longest increasing subsequence by `revisedIndex`, assuming `pairs` is
|
|
105
|
-
* already sorted by `baseIndex` ascending. Drops any pair that would make
|
|
106
|
-
* the alignment walk backward in the revised document — the guard against
|
|
107
|
-
* both id collisions (pass 1) and any crossing match (pass 1 + 2 combined).
|
|
108
|
-
*/
|
|
109
|
-
const longestIncreasingByRevisedIndex = (pairs) => {
|
|
110
|
-
if (pairs.length === 0) return [];
|
|
111
|
-
const lengths = new Int32Array(pairs.length).fill(1);
|
|
112
|
-
const predecessors = new Int32Array(pairs.length).fill(-1);
|
|
113
|
-
let bestEnd = 0;
|
|
114
|
-
for (let i = 0; i < pairs.length; i++) {
|
|
115
|
-
for (let j = 0; j < i; j++) {
|
|
116
|
-
const current = pairs[j];
|
|
117
|
-
const candidate = pairs[i];
|
|
118
|
-
if (!current || !candidate) continue;
|
|
119
|
-
if (current.revisedIndex < candidate.revisedIndex && (lengths[j] ?? 0) + 1 > (lengths[i] ?? 0)) {
|
|
120
|
-
lengths[i] = (lengths[j] ?? 0) + 1;
|
|
121
|
-
predecessors[i] = j;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if ((lengths[i] ?? 0) > (lengths[bestEnd] ?? 0)) bestEnd = i;
|
|
125
|
-
}
|
|
126
|
-
const ordered = [];
|
|
127
|
-
for (let cursor = bestEnd; cursor !== -1; cursor = predecessors[cursor] ?? -1) {
|
|
128
|
-
const pair = pairs[cursor];
|
|
129
|
-
if (pair) ordered.push(pair);
|
|
130
|
-
}
|
|
131
|
-
return ordered.toReversed();
|
|
82
|
+
/** Inline formatting properties compared for `formatChanged` detection. */
|
|
83
|
+
const INLINE_FORMAT_PROPERTIES = {
|
|
84
|
+
bold: true,
|
|
85
|
+
italic: true,
|
|
86
|
+
underline: true,
|
|
87
|
+
strike: true,
|
|
88
|
+
fontFamily: true,
|
|
89
|
+
fontSizePt: true,
|
|
90
|
+
color: true
|
|
132
91
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
const candidates = [];
|
|
140
|
-
base.forEach((block, baseIndex) => {
|
|
141
|
-
if (!isStableBlockId(block.id)) return;
|
|
142
|
-
const revisedIndex = revisedIndexById.get(block.id);
|
|
143
|
-
if (revisedIndex !== void 0) candidates.push({
|
|
144
|
-
baseIndex,
|
|
145
|
-
revisedIndex
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
return longestIncreasingByRevisedIndex(candidates);
|
|
92
|
+
const PARAGRAPH_FORMAT_PROPERTIES = {
|
|
93
|
+
styleId: true,
|
|
94
|
+
listLevel: true,
|
|
95
|
+
alignment: true,
|
|
96
|
+
spacing: true
|
|
149
97
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* adversarial one crafted to defeat pass 1 — can leave thousands of blocks
|
|
154
|
-
* unpaired on both sides; without a cap, `pairByExactText` would allocate a
|
|
155
|
-
* quadratic-sized table for it. Past this budget, {@link exceedsLcsBudget}
|
|
156
|
-
* makes pass 2 back off entirely so alignment falls through to pass 3's
|
|
157
|
-
* linear positional zip instead — pairing is less precise for these
|
|
158
|
-
* degenerate inputs, but memory use stays bounded.
|
|
159
|
-
*/
|
|
160
|
-
const MAX_LCS_CELLS = 4e6;
|
|
161
|
-
/** True when an `unpairedBaseCount * unpairedRevisedCount` LCS table would exceed {@link MAX_LCS_CELLS}. */
|
|
162
|
-
const exceedsLcsBudget = (unpairedBaseCount, unpairedRevisedCount) => unpairedBaseCount * unpairedRevisedCount > MAX_LCS_CELLS;
|
|
163
|
-
const createLcsBudget = () => ({ remainingCells: MAX_LCS_CELLS });
|
|
164
|
-
/** Pass 2: order-preserving LCS by exact text equality over the blocks pass 1 left unpaired. */
|
|
165
|
-
const pairByExactText = (base, revised, lcsBudget) => {
|
|
166
|
-
const m = base.length;
|
|
167
|
-
const n = revised.length;
|
|
168
|
-
if (m === 0 || n === 0) return [];
|
|
169
|
-
if (exceedsLcsBudget(m, n) || lcsBudget.remainingCells <= 0) return [];
|
|
170
|
-
lcsBudget.remainingCells -= m * n;
|
|
171
|
-
const baseTexts = base.map(({ block }) => block.text);
|
|
172
|
-
const revisedTexts = revised.map(({ block }) => block.text);
|
|
173
|
-
const stride = n + 1;
|
|
174
|
-
const dp = new Int32Array((m + 1) * stride);
|
|
175
|
-
for (let i = m - 1; i >= 0; i--) {
|
|
176
|
-
const rowOffset = i * stride;
|
|
177
|
-
const nextRowOffset = (i + 1) * stride;
|
|
178
|
-
const baseText = baseTexts[i];
|
|
179
|
-
for (let j = n - 1; j >= 0; j--) {
|
|
180
|
-
const revisedText = revisedTexts[j];
|
|
181
|
-
dp[rowOffset + j] = baseText === revisedText ? (dp[nextRowOffset + j + 1] ?? 0) + 1 : Math.max(dp[nextRowOffset + j] ?? 0, dp[rowOffset + j + 1] ?? 0);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const pairs = [];
|
|
185
|
-
let i = 0;
|
|
186
|
-
let j = 0;
|
|
187
|
-
while (i < m && j < n) {
|
|
188
|
-
const baseEntry = base[i];
|
|
189
|
-
const revisedEntry = revised[j];
|
|
190
|
-
if (!baseEntry || !revisedEntry) break;
|
|
191
|
-
if (baseTexts[i] === revisedTexts[j]) {
|
|
192
|
-
pairs.push({
|
|
193
|
-
baseIndex: baseEntry.index,
|
|
194
|
-
revisedIndex: revisedEntry.index
|
|
195
|
-
});
|
|
196
|
-
i++;
|
|
197
|
-
j++;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if ((dp[(i + 1) * stride + j] ?? 0) >= (dp[i * stride + j + 1] ?? 0)) i++;
|
|
201
|
-
else j++;
|
|
202
|
-
}
|
|
203
|
-
return pairs;
|
|
98
|
+
const exceedsLcsBudget = exceedsFolioContentLcsBudget;
|
|
99
|
+
const createLcsBudget = () => {
|
|
100
|
+
return { remainingCells: createFolioContentAlignmentWorkSession().remainingLcsCells };
|
|
204
101
|
};
|
|
205
102
|
/**
|
|
206
|
-
*
|
|
207
|
-
* snapshots and flatten it into an ordered event stream. Shared by
|
|
208
|
-
* {@link compareDocxVersions} and the redline generator so both interpret
|
|
209
|
-
* one document walk instead of re-deriving it.
|
|
103
|
+
* Compatibility adapter for the DOCX snapshot comparison surface.
|
|
210
104
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* every story pair instead, so pass 2's cell allowance is aggregate across
|
|
215
|
-
* the whole comparison rather than reset per story.
|
|
105
|
+
* Older snapshots encode id stability in the id shape, so the adapter resolves
|
|
106
|
+
* that policy while the representation-neutral core can treat caller ids as
|
|
107
|
+
* stable by default.
|
|
216
108
|
*/
|
|
217
109
|
const alignFolioBlocks = (baseBlocks, revisedBlocks, lcsBudget = createLcsBudget()) => {
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!usedBaseIndexes.has(blockIndex)) baseRemaining.push({
|
|
224
|
-
block,
|
|
225
|
-
index: blockIndex
|
|
226
|
-
});
|
|
110
|
+
const workSession = createFolioContentAlignmentWorkSession({ lcsCells: lcsBudget.remainingCells });
|
|
111
|
+
const events = alignFolioContentBlocks(baseBlocks, revisedBlocks, {
|
|
112
|
+
workSession,
|
|
113
|
+
stableIdMismatch: "pair",
|
|
114
|
+
idStability: folioAIBlockIdStability
|
|
227
115
|
});
|
|
228
|
-
|
|
229
|
-
revisedBlocks.forEach((block, blockIndex) => {
|
|
230
|
-
if (!usedRevisedIndexes.has(blockIndex)) revisedRemaining.push({
|
|
231
|
-
block,
|
|
232
|
-
index: blockIndex
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
const exactTextAnchors = pairByExactText(baseRemaining, revisedRemaining, lcsBudget);
|
|
236
|
-
const anchors = longestIncreasingByRevisedIndex([...stableIdAnchors, ...exactTextAnchors].toSorted((a, b) => a.baseIndex - b.baseIndex));
|
|
237
|
-
const events = [];
|
|
238
|
-
/** Pass 3: positionally zip the leftover blocks in one gap between anchors. */
|
|
239
|
-
const emitGap = (baseFrom, baseTo, revisedFrom, revisedTo) => {
|
|
240
|
-
const pairedCount = Math.min(baseTo - baseFrom, revisedTo - revisedFrom);
|
|
241
|
-
for (let k = 0; k < pairedCount; k++) {
|
|
242
|
-
const baseBlock = baseBlocks[baseFrom + k];
|
|
243
|
-
const revisedBlock = revisedBlocks[revisedFrom + k];
|
|
244
|
-
if (baseBlock && revisedBlock) events.push({
|
|
245
|
-
type: "pair",
|
|
246
|
-
baseBlock,
|
|
247
|
-
revisedBlock
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
for (let k = baseFrom + pairedCount; k < baseTo; k++) {
|
|
251
|
-
const block = baseBlocks[k];
|
|
252
|
-
if (block) events.push({
|
|
253
|
-
type: "baseOnly",
|
|
254
|
-
block
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
for (let k = revisedFrom + pairedCount; k < revisedTo; k++) {
|
|
258
|
-
const block = revisedBlocks[k];
|
|
259
|
-
if (block) events.push({
|
|
260
|
-
type: "revisedOnly",
|
|
261
|
-
block
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
let baseCursor = 0;
|
|
266
|
-
let revisedCursor = 0;
|
|
267
|
-
for (const anchor of anchors) {
|
|
268
|
-
emitGap(baseCursor, anchor.baseIndex, revisedCursor, anchor.revisedIndex);
|
|
269
|
-
const baseBlock = baseBlocks[anchor.baseIndex];
|
|
270
|
-
const revisedBlock = revisedBlocks[anchor.revisedIndex];
|
|
271
|
-
if (baseBlock && revisedBlock) events.push({
|
|
272
|
-
type: "pair",
|
|
273
|
-
baseBlock,
|
|
274
|
-
revisedBlock
|
|
275
|
-
});
|
|
276
|
-
baseCursor = anchor.baseIndex + 1;
|
|
277
|
-
revisedCursor = anchor.revisedIndex + 1;
|
|
278
|
-
}
|
|
279
|
-
emitGap(baseCursor, baseBlocks.length, revisedCursor, revisedBlocks.length);
|
|
116
|
+
lcsBudget.remainingCells = workSession.remainingLcsCells;
|
|
280
117
|
return events;
|
|
281
118
|
};
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*/
|
|
293
|
-
const diffPreviewRunFormatting = (base, revised) => {
|
|
294
|
-
if (base.previewRuns === void 0 && revised.previewRuns === void 0) return [];
|
|
295
|
-
const baseText = base.previewRuns === void 0 ? null : previewRunsText(base.previewRuns);
|
|
296
|
-
const revisedText = revised.previewRuns === void 0 ? null : previewRunsText(revised.previewRuns);
|
|
297
|
-
if (baseText !== null && revisedText !== null && baseText !== revisedText) return [];
|
|
298
|
-
const text = baseText ?? revisedText;
|
|
299
|
-
if (text === null || text.length === 0) return [];
|
|
300
|
-
const baseRuns = base.previewRuns ?? [{ text }];
|
|
301
|
-
const revisedRuns = revised.previewRuns ?? [{ text }];
|
|
302
|
-
const changed = /* @__PURE__ */ new Set();
|
|
303
|
-
let baseRunIndex = 0;
|
|
304
|
-
let revisedRunIndex = 0;
|
|
305
|
-
let baseOffset = 0;
|
|
306
|
-
let revisedOffset = 0;
|
|
307
|
-
while (baseRunIndex < baseRuns.length && revisedRunIndex < revisedRuns.length) {
|
|
308
|
-
const baseRun = baseRuns[baseRunIndex];
|
|
309
|
-
const revisedRun = revisedRuns[revisedRunIndex];
|
|
310
|
-
if (!baseRun || !revisedRun) break;
|
|
311
|
-
const step = Math.min(baseRun.text.length - baseOffset, revisedRun.text.length - revisedOffset);
|
|
312
|
-
if (step > 0) {
|
|
313
|
-
for (const property of FORMAT_PROPERTIES) if (baseRun[property] !== revisedRun[property]) changed.add(property);
|
|
314
|
-
}
|
|
315
|
-
baseOffset += step;
|
|
316
|
-
revisedOffset += step;
|
|
317
|
-
if (baseOffset >= baseRun.text.length) {
|
|
318
|
-
baseRunIndex++;
|
|
319
|
-
baseOffset = 0;
|
|
320
|
-
}
|
|
321
|
-
if (revisedOffset >= revisedRun.text.length) {
|
|
322
|
-
revisedRunIndex++;
|
|
323
|
-
revisedOffset = 0;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return FORMAT_PROPERTIES.filter((property) => changed.has(property));
|
|
327
|
-
};
|
|
328
|
-
/**
|
|
329
|
-
* Floor for move detection: an added/deleted text must hold at least this
|
|
330
|
-
* many whitespace-separated words before an identical pair re-classifies as
|
|
331
|
-
* a move. Short boilerplate ("Confidential", a bare heading word) recurs
|
|
332
|
-
* throughout real documents and would otherwise pair as spurious moves.
|
|
333
|
-
*/
|
|
334
|
-
const MOVE_MINIMUM_WORD_COUNT = 3;
|
|
335
|
-
const meetsMoveWordCount = (text) => {
|
|
336
|
-
const words = text.matchAll(/\S+/gu);
|
|
337
|
-
let count = 0;
|
|
338
|
-
while (!words.next().done) if (++count >= MOVE_MINIMUM_WORD_COUNT) return true;
|
|
339
|
-
return false;
|
|
119
|
+
const legacySegments = (segments) => segments.map(({ type, text }) => ({
|
|
120
|
+
type,
|
|
121
|
+
text
|
|
122
|
+
}));
|
|
123
|
+
const ownStringKeys = (value) => Object.keys(value);
|
|
124
|
+
const legacyFormattingProperties = (formatting) => {
|
|
125
|
+
const properties = [];
|
|
126
|
+
for (const property of ownStringKeys(PARAGRAPH_FORMAT_PROPERTIES)) if (formatting.paragraph?.[property] !== void 0) properties.push(property);
|
|
127
|
+
for (const property of ownStringKeys(INLINE_FORMAT_PROPERTIES)) if (formatting.ranges.some(({ formatting: range }) => range[property] !== void 0)) properties.push(property);
|
|
128
|
+
return properties;
|
|
340
129
|
};
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
* list without bound; past this cap, further same-text deletions are simply
|
|
346
|
-
* left as `deleted` (never matched to a move) instead of queued.
|
|
347
|
-
*/
|
|
348
|
-
const MAX_MOVE_CANDIDATES_PER_TEXT = 1e4;
|
|
349
|
-
const dequeueDeletedIndex = (queue) => {
|
|
350
|
-
if (!queue || queue.head >= queue.items.length) return;
|
|
351
|
-
const index = queue.items[queue.head];
|
|
352
|
-
queue.head += 1;
|
|
353
|
-
return index;
|
|
354
|
-
};
|
|
355
|
-
/**
|
|
356
|
-
* Re-classify `deleted` + `added` pairs with identical text as
|
|
357
|
-
* `movedFrom` / `movedTo` entries sharing a `moveGroupId`, in place, so each
|
|
358
|
-
* side keeps its slot in the revised-side document order. Matching is FIFO
|
|
359
|
-
* per text, so duplicated boilerplate above the word floor pairs
|
|
360
|
-
* first-to-first rather than fanning out.
|
|
361
|
-
*/
|
|
362
|
-
const detectMoves = (changes, counts, firstMoveGroupId) => {
|
|
363
|
-
const deletedIndexesByText = /* @__PURE__ */ new Map();
|
|
364
|
-
changes.forEach((change, index) => {
|
|
365
|
-
if (change.type !== "deleted" || !meetsMoveWordCount(change.text)) return;
|
|
366
|
-
const queue = deletedIndexesByText.get(change.text);
|
|
367
|
-
if (!queue) {
|
|
368
|
-
deletedIndexesByText.set(change.text, {
|
|
369
|
-
items: [index],
|
|
370
|
-
head: 0
|
|
371
|
-
});
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (queue.items.length < MAX_MOVE_CANDIDATES_PER_TEXT) queue.items.push(index);
|
|
375
|
-
});
|
|
376
|
-
if (deletedIndexesByText.size === 0) return;
|
|
377
|
-
let moveGroupId = firstMoveGroupId - 1;
|
|
378
|
-
changes.forEach((change, index) => {
|
|
379
|
-
if (change.type !== "added") return;
|
|
380
|
-
const deletedIndex = dequeueDeletedIndex(deletedIndexesByText.get(change.text));
|
|
381
|
-
if (deletedIndex === void 0) return;
|
|
382
|
-
const deleted = changes[deletedIndex];
|
|
383
|
-
if (!deleted || deleted.type !== "deleted") return;
|
|
384
|
-
moveGroupId++;
|
|
385
|
-
changes[deletedIndex] = {
|
|
386
|
-
type: "movedFrom",
|
|
387
|
-
blockId: deleted.blockId,
|
|
388
|
-
kind: deleted.kind,
|
|
389
|
-
text: deleted.text,
|
|
390
|
-
moveGroupId,
|
|
391
|
-
baseHandle: deleted.baseHandle
|
|
392
|
-
};
|
|
393
|
-
changes[index] = {
|
|
394
|
-
type: "movedTo",
|
|
395
|
-
blockId: change.blockId,
|
|
396
|
-
kind: change.kind,
|
|
397
|
-
text: change.text,
|
|
398
|
-
moveGroupId,
|
|
399
|
-
revisedHandle: change.revisedHandle
|
|
400
|
-
};
|
|
401
|
-
counts.deleted--;
|
|
402
|
-
counts.added--;
|
|
403
|
-
counts.moved++;
|
|
404
|
-
});
|
|
130
|
+
const legacyVersionChangeProperties = ({ blockProperties, formatting }) => {
|
|
131
|
+
const properties = [...blockProperties];
|
|
132
|
+
if (formatting) properties.push(...legacyFormattingProperties(formatting));
|
|
133
|
+
return properties;
|
|
405
134
|
};
|
|
406
135
|
const createSummaryCounts = () => ({
|
|
407
136
|
added: 0,
|
|
@@ -421,85 +150,227 @@ const addSummaryCounts = (target, source) => {
|
|
|
421
150
|
target.metadataChanged += source.metadataChanged;
|
|
422
151
|
target.unchanged += source.unchanged;
|
|
423
152
|
};
|
|
424
|
-
|
|
153
|
+
/**
|
|
154
|
+
* A disabled text scope reclassifies text-only comparison units as unchanged;
|
|
155
|
+
* it never removes them from the summary. Moves count once at their target,
|
|
156
|
+
* while a split or merge retains its legacy two-unit accounting.
|
|
157
|
+
*/
|
|
158
|
+
const excludedTextEventUnitCount = (event) => {
|
|
159
|
+
switch (event.type) {
|
|
160
|
+
case "unchanged":
|
|
161
|
+
case "formatting":
|
|
162
|
+
case "modified": return null;
|
|
163
|
+
case "movedFrom": return 0;
|
|
164
|
+
case "deleted":
|
|
165
|
+
case "inserted":
|
|
166
|
+
case "movedTo": return 1;
|
|
167
|
+
case "split": return event.revisedBlocks.length;
|
|
168
|
+
case "merge": return event.baseBlocks.length;
|
|
169
|
+
default: return panic("Unhandled scoped neutral comparison event", { event });
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const compareStoryBlocks = ({ baseStory, revisedStory, baseBlocks, revisedBlocks, firstMoveGroupId, includeText, includeFormatting, workSession }) => {
|
|
173
|
+
const compared = compareAlignedFolioContent({
|
|
174
|
+
baseBlocks,
|
|
175
|
+
revisedBlocks,
|
|
176
|
+
steps: alignFolioContentStructure({
|
|
177
|
+
baseBlocks,
|
|
178
|
+
revisedBlocks,
|
|
179
|
+
workSession: workSession.alignment,
|
|
180
|
+
stableIdMismatch: "pair",
|
|
181
|
+
idStability: folioAIBlockIdStability
|
|
182
|
+
}),
|
|
183
|
+
workSession,
|
|
184
|
+
idStability: folioAIBlockIdStability,
|
|
185
|
+
maxChanges: Number.MAX_SAFE_INTEGER
|
|
186
|
+
});
|
|
187
|
+
if (compared.isErr()) return panic("A version comparison exceeded an unreachable internal result limit", { cause: compared.error });
|
|
425
188
|
const changes = [];
|
|
426
189
|
const counts = createSummaryCounts();
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
190
|
+
const baseBlockById = /* @__PURE__ */ new Map();
|
|
191
|
+
if (!includeText && includeFormatting) for (const block of baseBlocks) baseBlockById.set(block.id, block);
|
|
192
|
+
const baseHandle = (block) => {
|
|
193
|
+
if (!baseStory) return panic("A neutral comparison event requires a base story handle");
|
|
194
|
+
return {
|
|
195
|
+
story: baseStory,
|
|
196
|
+
blockId: block.id
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
const revisedHandle = (block) => {
|
|
200
|
+
if (!revisedStory) return panic("A neutral comparison event requires a revised story handle");
|
|
201
|
+
return {
|
|
202
|
+
story: revisedStory,
|
|
203
|
+
blockId: block.id
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
const addModified = ({ baseBlock, revisedBlock, segments, blockProperties, formatting }) => {
|
|
207
|
+
const changedProperties = legacyVersionChangeProperties({
|
|
208
|
+
blockProperties,
|
|
209
|
+
formatting: includeFormatting ? formatting : void 0
|
|
210
|
+
});
|
|
211
|
+
counts.modified++;
|
|
212
|
+
changes.push({
|
|
213
|
+
type: "modified",
|
|
214
|
+
blockId: revisedBlock.id,
|
|
215
|
+
kind: revisedBlock.kind,
|
|
216
|
+
segments: legacySegments(segments),
|
|
217
|
+
...changedProperties.length > 0 && { changedProperties },
|
|
218
|
+
baseHandle: baseHandle(baseBlock),
|
|
219
|
+
revisedHandle: revisedHandle(revisedBlock)
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
const addFormattingOrUnchanged = ({ baseBlock, revisedBlock, formatting }) => {
|
|
223
|
+
const changedProperties = includeFormatting && formatting ? legacyFormattingProperties(formatting) : [];
|
|
224
|
+
if (changedProperties.length === 0) {
|
|
225
|
+
counts.unchanged++;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
counts.formatChanged++;
|
|
229
|
+
changes.push({
|
|
230
|
+
type: "formatChanged",
|
|
231
|
+
blockId: revisedBlock.id,
|
|
232
|
+
kind: revisedBlock.kind,
|
|
233
|
+
text: revisedBlock.text,
|
|
234
|
+
changedProperties,
|
|
235
|
+
baseHandle: baseHandle(baseBlock),
|
|
236
|
+
revisedHandle: revisedHandle(revisedBlock)
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
const addDeleted = (block) => {
|
|
240
|
+
counts.deleted++;
|
|
241
|
+
changes.push({
|
|
242
|
+
type: "deleted",
|
|
243
|
+
blockId: block.id,
|
|
244
|
+
kind: block.kind,
|
|
245
|
+
text: block.text,
|
|
246
|
+
baseHandle: baseHandle(block)
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
const addInserted = (block) => {
|
|
250
|
+
counts.added++;
|
|
251
|
+
changes.push({
|
|
252
|
+
type: "added",
|
|
253
|
+
blockId: block.id,
|
|
254
|
+
kind: block.kind,
|
|
255
|
+
text: block.text,
|
|
256
|
+
revisedHandle: revisedHandle(block)
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
for (const event of compared.value.events) {
|
|
260
|
+
if (!includeText && includeFormatting && event.type === "movedTo" && event.formatting) {
|
|
261
|
+
const baseBlock = baseBlockById.get(event.baseBlockId);
|
|
262
|
+
if (!baseBlock) return panic("A moved comparison event requires its base block", { blockId: event.baseBlockId });
|
|
263
|
+
addFormattingOrUnchanged({
|
|
264
|
+
baseBlock,
|
|
265
|
+
revisedBlock: event.revisedBlocks[0],
|
|
266
|
+
formatting: event.formatting
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (!includeText) {
|
|
271
|
+
const excludedUnitCount = excludedTextEventUnitCount(event);
|
|
272
|
+
if (excludedUnitCount !== null) {
|
|
273
|
+
counts.unchanged += excludedUnitCount;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
switch (event.type) {
|
|
278
|
+
case "unchanged":
|
|
279
|
+
counts.unchanged++;
|
|
280
|
+
break;
|
|
281
|
+
case "formatting":
|
|
282
|
+
addFormattingOrUnchanged({
|
|
283
|
+
baseBlock: event.baseBlocks[0],
|
|
284
|
+
revisedBlock: event.revisedBlocks[0],
|
|
285
|
+
formatting: event.formatting
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
case "modified": {
|
|
289
|
+
const baseBlock = event.baseBlocks[0];
|
|
290
|
+
const revisedBlock = event.revisedBlocks[0];
|
|
291
|
+
if (includeText && (baseBlock.text !== revisedBlock.text || event.changedProperties.length)) {
|
|
292
|
+
addModified({
|
|
293
|
+
baseBlock,
|
|
294
|
+
revisedBlock,
|
|
295
|
+
segments: event.segments,
|
|
296
|
+
blockProperties: event.changedProperties,
|
|
297
|
+
formatting: event.formatting
|
|
298
|
+
});
|
|
299
|
+
break;
|
|
443
300
|
}
|
|
444
|
-
|
|
301
|
+
addFormattingOrUnchanged({
|
|
302
|
+
baseBlock,
|
|
303
|
+
revisedBlock,
|
|
304
|
+
formatting: event.formatting
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "deleted":
|
|
309
|
+
addDeleted(event.baseBlocks[0]);
|
|
310
|
+
break;
|
|
311
|
+
case "inserted":
|
|
312
|
+
addInserted(event.revisedBlocks[0]);
|
|
313
|
+
break;
|
|
314
|
+
case "movedFrom": {
|
|
315
|
+
const block = event.baseBlocks[0];
|
|
445
316
|
changes.push({
|
|
446
|
-
type: "
|
|
447
|
-
blockId:
|
|
448
|
-
kind:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
317
|
+
type: "movedFrom",
|
|
318
|
+
blockId: block.id,
|
|
319
|
+
kind: block.kind,
|
|
320
|
+
text: block.text,
|
|
321
|
+
moveGroupId: firstMoveGroupId + event.moveId - 1,
|
|
322
|
+
baseHandle: baseHandle(block)
|
|
452
323
|
});
|
|
453
|
-
|
|
324
|
+
break;
|
|
454
325
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
326
|
+
case "movedTo": {
|
|
327
|
+
const block = event.revisedBlocks[0];
|
|
328
|
+
const changedProperties = legacyVersionChangeProperties({
|
|
329
|
+
blockProperties: event.changedProperties ?? [],
|
|
330
|
+
formatting: includeFormatting ? event.formatting : void 0
|
|
331
|
+
});
|
|
458
332
|
changes.push({
|
|
459
|
-
type: "
|
|
460
|
-
blockId:
|
|
461
|
-
kind:
|
|
462
|
-
text:
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
333
|
+
type: "movedTo",
|
|
334
|
+
blockId: block.id,
|
|
335
|
+
kind: block.kind,
|
|
336
|
+
text: block.text,
|
|
337
|
+
moveGroupId: firstMoveGroupId + event.moveId - 1,
|
|
338
|
+
...event.segments && { segments: legacySegments(event.segments) },
|
|
339
|
+
...changedProperties.length > 0 && { changedProperties },
|
|
340
|
+
revisedHandle: revisedHandle(block)
|
|
466
341
|
});
|
|
467
|
-
|
|
342
|
+
counts.moved++;
|
|
343
|
+
break;
|
|
468
344
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
baseHandle: {
|
|
482
|
-
story: baseStory,
|
|
483
|
-
blockId: event.block.id
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
if (!includeText) continue;
|
|
489
|
-
if (!revisedStory) panic("A revised-only comparison event requires a revised story handle");
|
|
490
|
-
counts.added++;
|
|
491
|
-
changes.push({
|
|
492
|
-
type: "added",
|
|
493
|
-
blockId: event.block.id,
|
|
494
|
-
kind: event.block.kind,
|
|
495
|
-
text: event.block.text,
|
|
496
|
-
revisedHandle: {
|
|
497
|
-
story: revisedStory,
|
|
498
|
-
blockId: event.block.id
|
|
345
|
+
case "split": {
|
|
346
|
+
const baseBlock = event.baseBlocks[0];
|
|
347
|
+
const [firstRevised, secondRevised] = event.revisedBlocks;
|
|
348
|
+
addModified({
|
|
349
|
+
baseBlock,
|
|
350
|
+
revisedBlock: firstRevised,
|
|
351
|
+
segments: workSession.diffText(baseBlock.text, firstRevised.text),
|
|
352
|
+
blockProperties: [],
|
|
353
|
+
formatting: void 0
|
|
354
|
+
});
|
|
355
|
+
addInserted(secondRevised);
|
|
356
|
+
break;
|
|
499
357
|
}
|
|
500
|
-
|
|
358
|
+
case "merge": {
|
|
359
|
+
const [firstBase, secondBase] = event.baseBlocks;
|
|
360
|
+
const revisedBlock = event.revisedBlocks[0];
|
|
361
|
+
addModified({
|
|
362
|
+
baseBlock: firstBase,
|
|
363
|
+
revisedBlock,
|
|
364
|
+
segments: workSession.diffText(firstBase.text, revisedBlock.text),
|
|
365
|
+
blockProperties: [],
|
|
366
|
+
formatting: void 0
|
|
367
|
+
});
|
|
368
|
+
addDeleted(secondBase);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
default: panic("Unhandled neutral content comparison event", { event });
|
|
372
|
+
}
|
|
501
373
|
}
|
|
502
|
-
detectMoves(changes, counts, firstMoveGroupId);
|
|
503
374
|
return {
|
|
504
375
|
baseStory,
|
|
505
376
|
revisedStory,
|
|
@@ -581,8 +452,7 @@ const compareDocxVersions = async (base, revised, options = {}) => {
|
|
|
581
452
|
const baseStories = baseReviewer.listStories().map(({ handle }) => handle);
|
|
582
453
|
const revisedStories = revisedReviewer.listStories().map(({ handle }) => handle);
|
|
583
454
|
let nextMoveGroupId = 1;
|
|
584
|
-
const
|
|
585
|
-
const wordDiffSession = createWordDiffSession();
|
|
455
|
+
const comparisonWorkSession = createContentComparisonWorkSession();
|
|
586
456
|
for (const pair of pairFolioDocumentStories(baseStories, revisedStories)) {
|
|
587
457
|
const baseBlocks = pair.baseStory ? baseReviewer.readReviewedStory({
|
|
588
458
|
story: pair.baseStory,
|
|
@@ -599,8 +469,7 @@ const compareDocxVersions = async (base, revised, options = {}) => {
|
|
|
599
469
|
firstMoveGroupId: nextMoveGroupId,
|
|
600
470
|
includeText: scopes.has("text"),
|
|
601
471
|
includeFormatting: scopes.has("formatting"),
|
|
602
|
-
|
|
603
|
-
diffText: wordDiffSession.diff
|
|
472
|
+
workSession: comparisonWorkSession
|
|
604
473
|
});
|
|
605
474
|
stories.push(storyDiff);
|
|
606
475
|
for (const change of storyDiff.changes) changes.push(change);
|