@stll/folio-core 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/dist/ai-edits/apply.js +545 -296
- 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 +45 -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 +169 -567
- 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 +6 -4
- package/dist/document-operations.js +77 -12
- 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 +155 -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 +190 -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
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import { WORD_DIFF_GRANULARITIES, createWordDiffSession } from "../ai-edits/word-diff.js";
|
|
2
|
+
import { alignFolioContentStructure, contentBlocksShareContainer, createFolioContentAlignmentWorkSession } from "./content-alignment.js";
|
|
3
|
+
import { inlineFormattingSegments } from "./formatting.js";
|
|
4
|
+
import { Result, TaggedError, panic } from "better-result";
|
|
5
|
+
//#region src/compare/content.ts
|
|
6
|
+
/**
|
|
7
|
+
* Pure comparison of two ordered, representation-neutral content snapshots.
|
|
8
|
+
*
|
|
9
|
+
* Every event carries exact base and revised block tuples. Concatenating the
|
|
10
|
+
* base tuples reconstructs the base snapshot; concatenating the revised tuples
|
|
11
|
+
* reconstructs the revised snapshot. This remains true for moves, paragraph
|
|
12
|
+
* splits/merges, and table-column changes whose cells are not contiguous.
|
|
13
|
+
*/
|
|
14
|
+
/** Hard resource ceilings for one representation-neutral comparison. */
|
|
15
|
+
const FOLIO_CONTENT_COMPARISON_LIMITS = Object.freeze({
|
|
16
|
+
blocksPerSnapshot: 1e5,
|
|
17
|
+
changes: 1e4,
|
|
18
|
+
blockCodeUnits: 1048576,
|
|
19
|
+
textCodeUnitsPerSnapshot: 8e6,
|
|
20
|
+
previewRunsPerBlock: 65536,
|
|
21
|
+
previewRunsPerSnapshot: 1e6,
|
|
22
|
+
containerDepth: 64,
|
|
23
|
+
containerEntriesPerSnapshot: 1e6,
|
|
24
|
+
attributeCodeUnits: 16384,
|
|
25
|
+
attributeCodeUnitsPerSnapshot: 8e6
|
|
26
|
+
});
|
|
27
|
+
/** Words a one-sided block needs before it may be classified as a move. */
|
|
28
|
+
const MOVE_MINIMUM_WORD_COUNT = 3;
|
|
29
|
+
/** Same-text move candidates retained for one repeated value. */
|
|
30
|
+
const MAX_MOVE_CANDIDATES_PER_TEXT = 64;
|
|
31
|
+
/** Pairwise comparisons allowed for edited-move discovery. */
|
|
32
|
+
const MAX_MOVE_SIMILARITY_COMPARISONS = 2e4;
|
|
33
|
+
/** Map lookups allowed across edited-move similarity scoring. */
|
|
34
|
+
const MAX_MOVE_SIMILARITY_TOKEN_LOOKUPS = 4e6;
|
|
35
|
+
/** Minimum multiset Dice similarity for an edited relocation. */
|
|
36
|
+
const MOVE_SIMILARITY_THRESHOLD = .8;
|
|
37
|
+
/** Bound token material retained for one edited-move candidate. */
|
|
38
|
+
const MAX_MOVE_PROFILE_TOKENS = 16384;
|
|
39
|
+
/** Bound source code units copied into one edited-move token profile. */
|
|
40
|
+
const MAX_MOVE_PROFILE_CODE_UNITS = 1048576;
|
|
41
|
+
var InvalidFolioContentComparisonError = class extends TaggedError("InvalidFolioContentComparisonError") {};
|
|
42
|
+
var FolioContentComparisonLimitError = class extends TaggedError("FolioContentComparisonLimitError") {};
|
|
43
|
+
const createContentComparisonWorkSession = (granularity) => ({
|
|
44
|
+
alignment: createFolioContentAlignmentWorkSession(),
|
|
45
|
+
remainingMoveComparisons: MAX_MOVE_SIMILARITY_COMPARISONS,
|
|
46
|
+
remainingMoveTokenLookups: MAX_MOVE_SIMILARITY_TOKEN_LOOKUPS,
|
|
47
|
+
diffText: createWordDiffSession({ ...granularity && { granularity } }).diff
|
|
48
|
+
});
|
|
49
|
+
const invalidInput = (input, field, message, blockIndex) => new InvalidFolioContentComparisonError({
|
|
50
|
+
message,
|
|
51
|
+
input,
|
|
52
|
+
field,
|
|
53
|
+
...blockIndex !== void 0 && { blockIndex }
|
|
54
|
+
});
|
|
55
|
+
const limitExceeded = ({ input, limit, maximum, actual, blockIndex, field }) => new FolioContentComparisonLimitError({
|
|
56
|
+
message: `The ${input} content exceeds the ${limit} comparison limit.`,
|
|
57
|
+
input,
|
|
58
|
+
limit,
|
|
59
|
+
maximum,
|
|
60
|
+
actual,
|
|
61
|
+
...blockIndex !== void 0 && { blockIndex },
|
|
62
|
+
...field !== void 0 && { field }
|
|
63
|
+
});
|
|
64
|
+
const isFiniteInteger = (value) => typeof value === "number" && Number.isSafeInteger(value);
|
|
65
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
66
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
const hasRecordShape = (value) => isRecord(value);
|
|
68
|
+
const PARAGRAPH_ALIGNMENTS = /* @__PURE__ */ new Set([
|
|
69
|
+
"left",
|
|
70
|
+
"center",
|
|
71
|
+
"right",
|
|
72
|
+
"both",
|
|
73
|
+
"distribute",
|
|
74
|
+
"mediumKashida",
|
|
75
|
+
"highKashida",
|
|
76
|
+
"lowKashida",
|
|
77
|
+
"thaiDistribute"
|
|
78
|
+
]);
|
|
79
|
+
const validateRunFormatting = (block, side, blockIndex) => {
|
|
80
|
+
for (const [runIndex, run] of (block.previewRuns ?? []).entries()) {
|
|
81
|
+
if (run.directFormatting !== void 0 && !isRecord(run.directFormatting)) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].directFormatting`, "Direct inline formatting must be an object.", blockIndex);
|
|
82
|
+
for (const property of [
|
|
83
|
+
"bold",
|
|
84
|
+
"italic",
|
|
85
|
+
"underline",
|
|
86
|
+
"strike"
|
|
87
|
+
]) {
|
|
88
|
+
if (run[property] !== void 0 && typeof run[property] !== "boolean") return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].${property}`, "Inline boolean formatting must be boolean.", blockIndex);
|
|
89
|
+
const direct = run.directFormatting?.[property];
|
|
90
|
+
if (direct !== void 0 && typeof direct !== "boolean") return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].directFormatting.${property}`, "Direct inline boolean formatting must be boolean.", blockIndex);
|
|
91
|
+
}
|
|
92
|
+
for (const property of ["fontFamily", "color"]) {
|
|
93
|
+
const effective = run[property];
|
|
94
|
+
if (effective !== void 0 && typeof effective !== "string") return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].${property}`, "Inline string formatting must be a string.", blockIndex);
|
|
95
|
+
const direct = run.directFormatting?.[property];
|
|
96
|
+
if (direct !== void 0 && direct !== null && typeof direct !== "string") return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].directFormatting.${property}`, "Direct inline string formatting must be a string or null.", blockIndex);
|
|
97
|
+
}
|
|
98
|
+
if (run.fontSizePt !== void 0 && (!isFiniteNumber(run.fontSizePt) || run.fontSizePt < 0)) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].fontSizePt`, "Inline font size must be a non-negative finite number.", blockIndex);
|
|
99
|
+
const directSize = run.directFormatting?.fontSizePt;
|
|
100
|
+
if (directSize !== void 0 && directSize !== null && (!isFiniteNumber(directSize) || directSize < 0)) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].directFormatting.fontSizePt`, "Direct inline font size must be a non-negative finite number or null.", blockIndex);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
const validateParagraphFormatting = (block, side, blockIndex) => {
|
|
105
|
+
for (const property of ["styleId", "displayLabel"]) if (block[property] !== void 0 && typeof block[property] !== "string") return invalidInput(side, `blocks[${String(blockIndex)}].${property}`, "Optional block labels and style ids must be strings.", blockIndex);
|
|
106
|
+
if (block.headingLevel !== void 0 && (!isFiniteInteger(block.headingLevel) || block.headingLevel < 1)) return invalidInput(side, `blocks[${String(blockIndex)}].headingLevel`, "Heading levels must be positive integers.", blockIndex);
|
|
107
|
+
if (block.listLevel !== void 0 && (!isFiniteInteger(block.listLevel) || block.listLevel < 0)) return invalidInput(side, `blocks[${String(blockIndex)}].listLevel`, "List levels must be non-negative integers.", blockIndex);
|
|
108
|
+
if (block.directAlignment !== void 0 && !PARAGRAPH_ALIGNMENTS.has(block.directAlignment)) return invalidInput(side, `blocks[${String(blockIndex)}].directAlignment`, "Paragraph alignment is not recognized.", blockIndex);
|
|
109
|
+
const spacing = block.directSpacing;
|
|
110
|
+
if (spacing === void 0) return null;
|
|
111
|
+
if (!isRecord(spacing)) return invalidInput(side, `blocks[${String(blockIndex)}].directSpacing`, "Direct paragraph spacing must be an object.", blockIndex);
|
|
112
|
+
for (const property of [
|
|
113
|
+
"spaceBefore",
|
|
114
|
+
"spaceAfter",
|
|
115
|
+
"lineSpacing"
|
|
116
|
+
]) if (spacing[property] !== void 0 && !isFiniteNumber(spacing[property])) return invalidInput(side, `blocks[${String(blockIndex)}].directSpacing.${property}`, "Paragraph spacing values must be finite numbers.", blockIndex);
|
|
117
|
+
for (const property of ["beforeAutospacing", "afterAutospacing"]) if (spacing[property] !== void 0 && typeof spacing[property] !== "boolean") return invalidInput(side, `blocks[${String(blockIndex)}].directSpacing.${property}`, "Paragraph auto-spacing values must be boolean.", blockIndex);
|
|
118
|
+
if (spacing.lineSpacingRule !== void 0 && spacing.lineSpacingRule !== "auto" && spacing.lineSpacingRule !== "exact" && spacing.lineSpacingRule !== "atLeast") return invalidInput(side, `blocks[${String(blockIndex)}].directSpacing.lineSpacingRule`, "Paragraph line-spacing rule is not recognized.", blockIndex);
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
const validateTableLocation = (table, side, blockIndex) => {
|
|
122
|
+
if (!isRecord(table)) return invalidInput(side, `blocks[${String(blockIndex)}].table`, "A table location must be an object.", blockIndex);
|
|
123
|
+
for (const field of [
|
|
124
|
+
"outerTableIndex",
|
|
125
|
+
"tableIndex",
|
|
126
|
+
"rowIndex",
|
|
127
|
+
"cellIndex",
|
|
128
|
+
"gridColumnIndex",
|
|
129
|
+
"paragraphIndex"
|
|
130
|
+
]) if (!isFiniteInteger(table[field]) || table[field] < 0) return invalidInput(side, `blocks[${String(blockIndex)}].table.${field}`, "Table indexes must be non-negative integers.", blockIndex);
|
|
131
|
+
for (const field of ["columnSpan", "rowSpan"]) if (!isFiniteInteger(table[field]) || table[field] < 1) return invalidInput(side, `blocks[${String(blockIndex)}].table.${field}`, "Table spans must be positive integers.", blockIndex);
|
|
132
|
+
const gridColumnIndex = table["gridColumnIndex"];
|
|
133
|
+
const columnSpan = table["columnSpan"];
|
|
134
|
+
const right = typeof gridColumnIndex === "number" && typeof columnSpan === "number" ? gridColumnIndex + columnSpan : NaN;
|
|
135
|
+
if (!Number.isSafeInteger(right)) return invalidInput(side, `blocks[${String(blockIndex)}].table.columnSpan`, "A table cell's ending grid column must be a safe integer.", blockIndex);
|
|
136
|
+
const rowIndex = table["rowIndex"];
|
|
137
|
+
const rowSpan = table["rowSpan"];
|
|
138
|
+
const bottom = typeof rowIndex === "number" && typeof rowSpan === "number" ? rowIndex + rowSpan : NaN;
|
|
139
|
+
if (!Number.isSafeInteger(bottom)) return invalidInput(side, `blocks[${String(blockIndex)}].table.rowSpan`, "A table cell's ending row must be a safe integer.", blockIndex);
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
const overlappingTableCellBlockIndex = (cells) => {
|
|
143
|
+
if (cells.length < 2) return null;
|
|
144
|
+
const columns = [...new Set(cells.flatMap(({ left, right }) => [left, right]))].toSorted((left, right) => left - right);
|
|
145
|
+
if (columns.length < 2) return null;
|
|
146
|
+
const columnIndex = new Map(columns.map((column, index) => [column, index]));
|
|
147
|
+
const intervalCount = columns.length - 1;
|
|
148
|
+
const maximum = new Int32Array(intervalCount * 4);
|
|
149
|
+
const pending = new Int32Array(intervalCount * 4);
|
|
150
|
+
const add = (node, nodeLeft, nodeRight, rangeLeft, rangeRight, amount) => {
|
|
151
|
+
if (rangeLeft <= nodeLeft && nodeRight <= rangeRight) {
|
|
152
|
+
maximum[node] = (maximum[node] ?? 0) + amount;
|
|
153
|
+
pending[node] = (pending[node] ?? 0) + amount;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const middle = nodeLeft + Math.floor((nodeRight - nodeLeft) / 2);
|
|
157
|
+
if (rangeLeft <= middle) add(node * 2, nodeLeft, middle, rangeLeft, rangeRight, amount);
|
|
158
|
+
if (rangeRight > middle) add(node * 2 + 1, middle + 1, nodeRight, rangeLeft, rangeRight, amount);
|
|
159
|
+
maximum[node] = (pending[node] ?? 0) + Math.max(maximum[node * 2] ?? 0, maximum[node * 2 + 1] ?? 0);
|
|
160
|
+
};
|
|
161
|
+
const events = cells.flatMap((cell) => [{
|
|
162
|
+
row: cell.top,
|
|
163
|
+
amount: 1,
|
|
164
|
+
cell
|
|
165
|
+
}, {
|
|
166
|
+
row: cell.bottom,
|
|
167
|
+
amount: -1,
|
|
168
|
+
cell
|
|
169
|
+
}]);
|
|
170
|
+
events.sort((left, right) => left.row - right.row || left.amount - right.amount || left.cell.left - right.cell.left || left.cell.blockIndex - right.cell.blockIndex);
|
|
171
|
+
for (const { amount, cell } of events) {
|
|
172
|
+
const left = columnIndex.get(cell.left);
|
|
173
|
+
const right = columnIndex.get(cell.right);
|
|
174
|
+
if (left === void 0 || right === void 0 || left >= right) return panic("A validated table cell has no coordinate-compression interval");
|
|
175
|
+
add(1, 0, intervalCount - 1, left, right - 1, amount);
|
|
176
|
+
if (amount > 0 && (maximum[1] ?? 0) > 1) return cell.blockIndex;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
};
|
|
180
|
+
const chargeAttributeString = ({ value, side, blockIndex, field, usage }) => {
|
|
181
|
+
if (value === void 0 || value === null) return null;
|
|
182
|
+
if (value.length > FOLIO_CONTENT_COMPARISON_LIMITS.attributeCodeUnits) return limitExceeded({
|
|
183
|
+
input: side,
|
|
184
|
+
limit: "attributeCodeUnits",
|
|
185
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.attributeCodeUnits,
|
|
186
|
+
actual: value.length,
|
|
187
|
+
blockIndex,
|
|
188
|
+
field
|
|
189
|
+
});
|
|
190
|
+
usage.attributeCodeUnits += value.length;
|
|
191
|
+
if (usage.attributeCodeUnits > FOLIO_CONTENT_COMPARISON_LIMITS.attributeCodeUnitsPerSnapshot) return limitExceeded({
|
|
192
|
+
input: side,
|
|
193
|
+
limit: "attributeCodeUnitsPerSnapshot",
|
|
194
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.attributeCodeUnitsPerSnapshot,
|
|
195
|
+
actual: usage.attributeCodeUnits,
|
|
196
|
+
blockIndex,
|
|
197
|
+
field
|
|
198
|
+
});
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
const validateSnapshot = (snapshot, side) => {
|
|
202
|
+
if (!snapshot || !Array.isArray(snapshot.blocks)) return invalidInput(side, "blocks", "A content snapshot must contain an ordered blocks array.");
|
|
203
|
+
if (snapshot.blocks.length > FOLIO_CONTENT_COMPARISON_LIMITS.blocksPerSnapshot) return limitExceeded({
|
|
204
|
+
input: side,
|
|
205
|
+
limit: "blocksPerSnapshot",
|
|
206
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.blocksPerSnapshot,
|
|
207
|
+
actual: snapshot.blocks.length
|
|
208
|
+
});
|
|
209
|
+
const usage = {
|
|
210
|
+
textCodeUnits: 0,
|
|
211
|
+
previewRuns: 0,
|
|
212
|
+
containerEntries: 0,
|
|
213
|
+
attributeCodeUnits: 0
|
|
214
|
+
};
|
|
215
|
+
const ids = /* @__PURE__ */ new Set();
|
|
216
|
+
const lastCoordinateByTable = /* @__PURE__ */ new Map();
|
|
217
|
+
const geometryByCell = /* @__PURE__ */ new Map();
|
|
218
|
+
const cellsByTable = /* @__PURE__ */ new Map();
|
|
219
|
+
const outerTableByTableIndex = /* @__PURE__ */ new Map();
|
|
220
|
+
let lastOuterTableIndex = -1;
|
|
221
|
+
let activeOuterTableIndex = null;
|
|
222
|
+
for (const [blockIndex, block] of snapshot.blocks.entries()) {
|
|
223
|
+
if (!hasRecordShape(block)) return invalidInput(side, `blocks[${String(blockIndex)}]`, "Every content block must be an object.", blockIndex);
|
|
224
|
+
if (typeof block.id !== "string" || block.id.length === 0) return invalidInput(side, `blocks[${String(blockIndex)}].id`, "Every content block needs a non-empty id.", blockIndex);
|
|
225
|
+
const idLimit = chargeAttributeString({
|
|
226
|
+
value: block.id,
|
|
227
|
+
side,
|
|
228
|
+
blockIndex,
|
|
229
|
+
field: `blocks[${String(blockIndex)}].id`,
|
|
230
|
+
usage
|
|
231
|
+
});
|
|
232
|
+
if (idLimit) return idLimit;
|
|
233
|
+
if (ids.has(block.id)) return invalidInput(side, `blocks[${String(blockIndex)}].id`, "Content block ids must be unique within a snapshot.", blockIndex);
|
|
234
|
+
ids.add(block.id);
|
|
235
|
+
if (typeof block.kind !== "string" || block.kind.length === 0) return invalidInput(side, `blocks[${String(blockIndex)}].kind`, "Every content block needs a non-empty kind.", blockIndex);
|
|
236
|
+
const kindLimit = chargeAttributeString({
|
|
237
|
+
value: block.kind,
|
|
238
|
+
side,
|
|
239
|
+
blockIndex,
|
|
240
|
+
field: `blocks[${String(blockIndex)}].kind`,
|
|
241
|
+
usage
|
|
242
|
+
});
|
|
243
|
+
if (kindLimit) return kindLimit;
|
|
244
|
+
if (typeof block.text !== "string") return invalidInput(side, `blocks[${String(blockIndex)}].text`, "Content block text must be a string.", blockIndex);
|
|
245
|
+
if (block.text.length > FOLIO_CONTENT_COMPARISON_LIMITS.blockCodeUnits) return limitExceeded({
|
|
246
|
+
input: side,
|
|
247
|
+
limit: "blockCodeUnits",
|
|
248
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.blockCodeUnits,
|
|
249
|
+
actual: block.text.length,
|
|
250
|
+
blockIndex,
|
|
251
|
+
field: `blocks[${String(blockIndex)}].text`
|
|
252
|
+
});
|
|
253
|
+
usage.textCodeUnits += block.text.length;
|
|
254
|
+
if (usage.textCodeUnits > FOLIO_CONTENT_COMPARISON_LIMITS.textCodeUnitsPerSnapshot) return limitExceeded({
|
|
255
|
+
input: side,
|
|
256
|
+
limit: "textCodeUnitsPerSnapshot",
|
|
257
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.textCodeUnitsPerSnapshot,
|
|
258
|
+
actual: usage.textCodeUnits,
|
|
259
|
+
blockIndex,
|
|
260
|
+
field: `blocks[${String(blockIndex)}].text`
|
|
261
|
+
});
|
|
262
|
+
if (block.idStability !== void 0 && block.idStability !== "stable" && block.idStability !== "positional") return invalidInput(side, `blocks[${String(blockIndex)}].idStability`, "Block id stability must be stable or positional.", blockIndex);
|
|
263
|
+
if (block.previewRuns !== void 0) {
|
|
264
|
+
if (!Array.isArray(block.previewRuns)) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns`, "Preview runs must be an array of text runs.", blockIndex);
|
|
265
|
+
if (block.previewRuns.length > FOLIO_CONTENT_COMPARISON_LIMITS.previewRunsPerBlock) return limitExceeded({
|
|
266
|
+
input: side,
|
|
267
|
+
limit: "previewRunsPerBlock",
|
|
268
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.previewRunsPerBlock,
|
|
269
|
+
actual: block.previewRuns.length,
|
|
270
|
+
blockIndex,
|
|
271
|
+
field: `blocks[${String(blockIndex)}].previewRuns`
|
|
272
|
+
});
|
|
273
|
+
usage.previewRuns += block.previewRuns.length;
|
|
274
|
+
if (usage.previewRuns > FOLIO_CONTENT_COMPARISON_LIMITS.previewRunsPerSnapshot) return limitExceeded({
|
|
275
|
+
input: side,
|
|
276
|
+
limit: "previewRunsPerSnapshot",
|
|
277
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.previewRunsPerSnapshot,
|
|
278
|
+
actual: usage.previewRuns,
|
|
279
|
+
blockIndex,
|
|
280
|
+
field: `blocks[${String(blockIndex)}].previewRuns`
|
|
281
|
+
});
|
|
282
|
+
let runTextOffset = 0;
|
|
283
|
+
for (const [runIndex, run] of block.previewRuns.entries()) {
|
|
284
|
+
if (!hasRecordShape(run) || typeof run.text !== "string") return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns`, "Preview runs must be an array of text runs.", blockIndex);
|
|
285
|
+
if (!block.text.startsWith(run.text, runTextOffset)) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns`, "Preview-run text must reconstruct the block text exactly.", blockIndex);
|
|
286
|
+
runTextOffset += run.text.length;
|
|
287
|
+
for (const property of ["fontFamily", "color"]) for (const [suffix, value] of [[property, run[property]], [`directFormatting.${property}`, run.directFormatting?.[property]]]) {
|
|
288
|
+
const formattingLimit = chargeAttributeString({
|
|
289
|
+
value: typeof value === "string" ? value : void 0,
|
|
290
|
+
side,
|
|
291
|
+
blockIndex,
|
|
292
|
+
field: `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].${suffix}`,
|
|
293
|
+
usage
|
|
294
|
+
});
|
|
295
|
+
if (formattingLimit) return formattingLimit;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (runTextOffset !== block.text.length) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns`, "Preview-run text must reconstruct the block text exactly.", blockIndex);
|
|
299
|
+
}
|
|
300
|
+
const paragraphError = validateParagraphFormatting(block, side, blockIndex);
|
|
301
|
+
if (paragraphError) return paragraphError;
|
|
302
|
+
const runError = validateRunFormatting(block, side, blockIndex);
|
|
303
|
+
if (runError) return runError;
|
|
304
|
+
for (const property of ["styleId", "displayLabel"]) {
|
|
305
|
+
const propertyLimit = chargeAttributeString({
|
|
306
|
+
value: block[property],
|
|
307
|
+
side,
|
|
308
|
+
blockIndex,
|
|
309
|
+
field: `blocks[${String(blockIndex)}].${property}`,
|
|
310
|
+
usage
|
|
311
|
+
});
|
|
312
|
+
if (propertyLimit) return propertyLimit;
|
|
313
|
+
}
|
|
314
|
+
if (block.containerPath !== void 0) {
|
|
315
|
+
if (!Array.isArray(block.containerPath)) return invalidInput(side, `blocks[${String(blockIndex)}].containerPath`, "Container paths require non-empty kind and id values.", blockIndex);
|
|
316
|
+
if (block.containerPath.length > FOLIO_CONTENT_COMPARISON_LIMITS.containerDepth) return limitExceeded({
|
|
317
|
+
input: side,
|
|
318
|
+
limit: "containerDepth",
|
|
319
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.containerDepth,
|
|
320
|
+
actual: block.containerPath.length,
|
|
321
|
+
blockIndex,
|
|
322
|
+
field: `blocks[${String(blockIndex)}].containerPath`
|
|
323
|
+
});
|
|
324
|
+
usage.containerEntries += block.containerPath.length;
|
|
325
|
+
if (usage.containerEntries > FOLIO_CONTENT_COMPARISON_LIMITS.containerEntriesPerSnapshot) return limitExceeded({
|
|
326
|
+
input: side,
|
|
327
|
+
limit: "containerEntriesPerSnapshot",
|
|
328
|
+
maximum: FOLIO_CONTENT_COMPARISON_LIMITS.containerEntriesPerSnapshot,
|
|
329
|
+
actual: usage.containerEntries,
|
|
330
|
+
blockIndex,
|
|
331
|
+
field: `blocks[${String(blockIndex)}].containerPath`
|
|
332
|
+
});
|
|
333
|
+
for (const [pathIndex, entry] of block.containerPath.entries()) {
|
|
334
|
+
if (!hasRecordShape(entry) || typeof entry.kind !== "string" || entry.kind.length === 0 || typeof entry.id !== "string" || entry.id.length === 0) return invalidInput(side, `blocks[${String(blockIndex)}].containerPath`, "Container paths require non-empty kind and id values.", blockIndex);
|
|
335
|
+
for (const property of ["kind", "id"]) {
|
|
336
|
+
const pathLimit = chargeAttributeString({
|
|
337
|
+
value: entry[property],
|
|
338
|
+
side,
|
|
339
|
+
blockIndex,
|
|
340
|
+
field: `blocks[${String(blockIndex)}].containerPath[${String(pathIndex)}].${property}`,
|
|
341
|
+
usage
|
|
342
|
+
});
|
|
343
|
+
if (pathLimit) return pathLimit;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (block.table !== void 0) {
|
|
348
|
+
const error = validateTableLocation(block.table, side, blockIndex);
|
|
349
|
+
if (error) return error;
|
|
350
|
+
const table = block.table;
|
|
351
|
+
if (table.tableIndex < table.outerTableIndex) return invalidInput(side, `blocks[${String(blockIndex)}].table.tableIndex`, "An inner table cannot precede its outer table in document order.", blockIndex);
|
|
352
|
+
const knownOuterTableIndex = outerTableByTableIndex.get(table.tableIndex);
|
|
353
|
+
if (knownOuterTableIndex !== void 0 && knownOuterTableIndex !== table.outerTableIndex) return invalidInput(side, `blocks[${String(blockIndex)}].table.tableIndex`, "A table index must identify only one outer table.", blockIndex);
|
|
354
|
+
outerTableByTableIndex.set(table.tableIndex, table.outerTableIndex);
|
|
355
|
+
if (table.outerTableIndex < lastOuterTableIndex || table.outerTableIndex === lastOuterTableIndex && activeOuterTableIndex !== table.outerTableIndex) return invalidInput(side, `blocks[${String(blockIndex)}].table`, "Each outer table must occupy one contiguous position in document order.", blockIndex);
|
|
356
|
+
lastOuterTableIndex = table.outerTableIndex;
|
|
357
|
+
activeOuterTableIndex = table.outerTableIndex;
|
|
358
|
+
const tableKey = `${String(table.outerTableIndex)}:${String(table.tableIndex)}`;
|
|
359
|
+
const cellKey = `${tableKey}:${String(table.rowIndex)}:${String(table.cellIndex)}`;
|
|
360
|
+
const geometry = [
|
|
361
|
+
table.gridColumnIndex,
|
|
362
|
+
table.columnSpan,
|
|
363
|
+
table.rowSpan
|
|
364
|
+
];
|
|
365
|
+
const priorGeometry = geometryByCell.get(cellKey);
|
|
366
|
+
if (priorGeometry !== void 0 && (geometry[0] !== priorGeometry[0] || geometry[1] !== priorGeometry[1] || geometry[2] !== priorGeometry[2])) return invalidInput(side, `blocks[${String(blockIndex)}].table`, "Every block in one physical table cell must carry the same geometry.", blockIndex);
|
|
367
|
+
geometryByCell.set(cellKey, geometry);
|
|
368
|
+
if (priorGeometry === void 0) {
|
|
369
|
+
const cells = cellsByTable.get(tableKey);
|
|
370
|
+
const rectangle = {
|
|
371
|
+
blockIndex,
|
|
372
|
+
left: table.gridColumnIndex,
|
|
373
|
+
right: table.gridColumnIndex + table.columnSpan,
|
|
374
|
+
top: table.rowIndex,
|
|
375
|
+
bottom: table.rowIndex + table.rowSpan
|
|
376
|
+
};
|
|
377
|
+
if (cells) cells.push(rectangle);
|
|
378
|
+
else cellsByTable.set(tableKey, [rectangle]);
|
|
379
|
+
}
|
|
380
|
+
const coordinate = [
|
|
381
|
+
table.rowIndex,
|
|
382
|
+
table.cellIndex,
|
|
383
|
+
table.paragraphIndex
|
|
384
|
+
];
|
|
385
|
+
const previous = lastCoordinateByTable.get(tableKey);
|
|
386
|
+
if (previous && (coordinate[0] < previous[0] || coordinate[0] === previous[0] && coordinate[1] < previous[1] || coordinate[0] === previous[0] && coordinate[1] === previous[1] && coordinate[2] <= previous[2])) return invalidInput(side, `blocks[${String(blockIndex)}].table`, "Blocks in one table must use row-major coordinates.", blockIndex);
|
|
387
|
+
lastCoordinateByTable.set(tableKey, coordinate);
|
|
388
|
+
} else activeOuterTableIndex = null;
|
|
389
|
+
}
|
|
390
|
+
for (const cells of cellsByTable.values()) {
|
|
391
|
+
const overlappingBlockIndex = overlappingTableCellBlockIndex(cells);
|
|
392
|
+
if (overlappingBlockIndex !== null) return invalidInput(side, `blocks[${String(overlappingBlockIndex)}].table`, "Physical table cells must not overlap.", overlappingBlockIndex);
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
};
|
|
396
|
+
const withTextOffsets = (segments) => {
|
|
397
|
+
const positioned = [];
|
|
398
|
+
let baseOffset = 0;
|
|
399
|
+
let revisedOffset = 0;
|
|
400
|
+
for (const segment of segments) {
|
|
401
|
+
const baseLength = segment.type === "ins" ? 0 : segment.text.length;
|
|
402
|
+
const revisedLength = segment.type === "del" ? 0 : segment.text.length;
|
|
403
|
+
positioned.push({
|
|
404
|
+
...segment,
|
|
405
|
+
baseStart: baseOffset,
|
|
406
|
+
baseEnd: baseOffset + baseLength,
|
|
407
|
+
revisedStart: revisedOffset,
|
|
408
|
+
revisedEnd: revisedOffset + revisedLength
|
|
409
|
+
});
|
|
410
|
+
baseOffset += baseLength;
|
|
411
|
+
revisedOffset += revisedLength;
|
|
412
|
+
}
|
|
413
|
+
return positioned;
|
|
414
|
+
};
|
|
415
|
+
const paragraphSpacingEqual = (base, revised) => base?.spaceBefore === revised?.spaceBefore && base?.spaceAfter === revised?.spaceAfter && base?.lineSpacing === revised?.lineSpacing && base?.lineSpacingRule === revised?.lineSpacingRule && base?.beforeAutospacing === revised?.beforeAutospacing && base?.afterAutospacing === revised?.afterAutospacing;
|
|
416
|
+
const changedFolioContentParagraphFormatting = (base, revised) => {
|
|
417
|
+
const patch = {};
|
|
418
|
+
if ((base.styleId ?? null) !== (revised.styleId ?? null)) patch.styleId = revised.styleId ?? null;
|
|
419
|
+
if (base.listLevel !== revised.listLevel) patch.listLevel = revised.listLevel ?? null;
|
|
420
|
+
if (base.directAlignment !== revised.directAlignment) patch.alignment = revised.directAlignment ?? null;
|
|
421
|
+
if (!paragraphSpacingEqual(base.directSpacing, revised.directSpacing)) patch.spacing = revised.directSpacing ?? null;
|
|
422
|
+
return Object.keys(patch).length > 0 ? patch : null;
|
|
423
|
+
};
|
|
424
|
+
const changedBlockProperties = (base, revised) => {
|
|
425
|
+
const changed = [];
|
|
426
|
+
if (base.kind !== revised.kind) changed.push("kind");
|
|
427
|
+
if (base.headingLevel !== revised.headingLevel) changed.push("headingLevel");
|
|
428
|
+
if (base.displayLabel !== revised.displayLabel) changed.push("displayLabel");
|
|
429
|
+
return changed;
|
|
430
|
+
};
|
|
431
|
+
const separatorBetween = (whole, head, tail) => {
|
|
432
|
+
if (head.length === 0 || tail.length === 0 || whole.length < head.length + tail.length) return null;
|
|
433
|
+
if (!whole.startsWith(head) || !whole.endsWith(tail)) return null;
|
|
434
|
+
const separator = whole.slice(head.length, whole.length - tail.length);
|
|
435
|
+
return separator.length === 0 || /^\s+$/u.test(separator) ? separator : null;
|
|
436
|
+
};
|
|
437
|
+
const detectFolioContentParagraphMarkPlans = (steps) => {
|
|
438
|
+
const plans = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const [index, step] of steps.entries()) {
|
|
440
|
+
const next = steps[index + 1];
|
|
441
|
+
if (step.type !== "pair" || next === void 0) continue;
|
|
442
|
+
if (next.type === "revisedOnly") {
|
|
443
|
+
const separator = separatorBetween(step.baseBlock.text, step.revisedBlock.text, next.block.text);
|
|
444
|
+
if (separator !== null && contentBlocksShareContainer(step.revisedBlock, next.block)) plans.set(index, {
|
|
445
|
+
type: "split",
|
|
446
|
+
baseBlock: step.baseBlock,
|
|
447
|
+
revisedBlocks: [step.revisedBlock, next.block],
|
|
448
|
+
offset: step.revisedBlock.text.length,
|
|
449
|
+
separator
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (next.type !== "baseOnly") continue;
|
|
454
|
+
const separator = separatorBetween(step.revisedBlock.text, step.baseBlock.text, next.block.text);
|
|
455
|
+
if (separator !== null && contentBlocksShareContainer(step.baseBlock, next.block)) plans.set(index, {
|
|
456
|
+
type: "merge",
|
|
457
|
+
baseBlocks: [step.baseBlock, next.block],
|
|
458
|
+
revisedBlock: step.revisedBlock,
|
|
459
|
+
separator
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return plans;
|
|
463
|
+
};
|
|
464
|
+
const tokenProfile = (text) => {
|
|
465
|
+
if (text.length > MAX_MOVE_PROFILE_CODE_UNITS) return null;
|
|
466
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
467
|
+
let count = 0;
|
|
468
|
+
for (const match of text.matchAll(/\S+/gu)) {
|
|
469
|
+
count++;
|
|
470
|
+
if (count > MAX_MOVE_PROFILE_TOKENS) return null;
|
|
471
|
+
const token = match[0];
|
|
472
|
+
occurrences.set(token, (occurrences.get(token) ?? 0) + 1);
|
|
473
|
+
}
|
|
474
|
+
return count >= MOVE_MINIMUM_WORD_COUNT ? {
|
|
475
|
+
count,
|
|
476
|
+
occurrences
|
|
477
|
+
} : null;
|
|
478
|
+
};
|
|
479
|
+
const tokenSimilarity = (base, revised, maximumLookups) => {
|
|
480
|
+
const [smaller, larger] = base.occurrences.size <= revised.occurrences.size ? [base.occurrences, revised.occurrences] : [revised.occurrences, base.occurrences];
|
|
481
|
+
if (smaller.size > maximumLookups) return null;
|
|
482
|
+
let shared = 0;
|
|
483
|
+
for (const [token, count] of smaller) shared += Math.min(larger.get(token) ?? 0, count);
|
|
484
|
+
return {
|
|
485
|
+
similarity: 2 * shared / (base.count + revised.count),
|
|
486
|
+
lookups: smaller.size
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
const detectFolioContentMoves = ({ steps, consumedStepIndexes, workSession, idStability = (block) => block.idStability ?? "stable" }) => {
|
|
490
|
+
const stableBaseById = /* @__PURE__ */ new Map();
|
|
491
|
+
const exactCandidatesByBucket = /* @__PURE__ */ new Map();
|
|
492
|
+
const similarityCandidatesByBucket = /* @__PURE__ */ new Map();
|
|
493
|
+
let candidateOrder = 0;
|
|
494
|
+
for (const [index, step] of steps.entries()) {
|
|
495
|
+
if (consumedStepIndexes.has(index) || step.type !== "baseOnly") continue;
|
|
496
|
+
if (idStability(step.block) === "stable") stableBaseById.set(step.block.id, {
|
|
497
|
+
block: step.block,
|
|
498
|
+
moveScope: step.moveScope
|
|
499
|
+
});
|
|
500
|
+
const profile = tokenProfile(step.block.text);
|
|
501
|
+
if (!profile) continue;
|
|
502
|
+
const candidate = {
|
|
503
|
+
block: step.block,
|
|
504
|
+
moveScope: step.moveScope,
|
|
505
|
+
profile,
|
|
506
|
+
order: candidateOrder++
|
|
507
|
+
};
|
|
508
|
+
let candidatesByText = exactCandidatesByBucket.get(step.moveScope.bucket);
|
|
509
|
+
if (!candidatesByText) {
|
|
510
|
+
candidatesByText = /* @__PURE__ */ new Map();
|
|
511
|
+
exactCandidatesByBucket.set(step.moveScope.bucket, candidatesByText);
|
|
512
|
+
}
|
|
513
|
+
const queue = candidatesByText.get(step.block.text);
|
|
514
|
+
if (!queue) candidatesByText.set(step.block.text, [candidate]);
|
|
515
|
+
else if (queue.length < MAX_MOVE_CANDIDATES_PER_TEXT) queue.push(candidate);
|
|
516
|
+
let candidatesByGap = similarityCandidatesByBucket.get(step.moveScope.bucket);
|
|
517
|
+
if (!candidatesByGap) {
|
|
518
|
+
candidatesByGap = /* @__PURE__ */ new Map();
|
|
519
|
+
similarityCandidatesByBucket.set(step.moveScope.bucket, candidatesByGap);
|
|
520
|
+
}
|
|
521
|
+
let gapCandidates = candidatesByGap.get(step.moveScope.gap);
|
|
522
|
+
if (!gapCandidates) {
|
|
523
|
+
gapCandidates = /* @__PURE__ */ new Map();
|
|
524
|
+
candidatesByGap.set(step.moveScope.gap, gapCandidates);
|
|
525
|
+
}
|
|
526
|
+
gapCandidates.set(step.block.id, candidate);
|
|
527
|
+
}
|
|
528
|
+
const taken = /* @__PURE__ */ new Set();
|
|
529
|
+
const takenRevised = /* @__PURE__ */ new Set();
|
|
530
|
+
const moves = [];
|
|
531
|
+
const removeSimilarityCandidate = (candidate) => {
|
|
532
|
+
const candidatesByGap = similarityCandidatesByBucket.get(candidate.moveScope.bucket);
|
|
533
|
+
const gapCandidates = candidatesByGap?.get(candidate.moveScope.gap);
|
|
534
|
+
gapCandidates?.delete(candidate.block.id);
|
|
535
|
+
if (gapCandidates?.size === 0) candidatesByGap?.delete(candidate.moveScope.gap);
|
|
536
|
+
if (candidatesByGap?.size === 0) similarityCandidatesByBucket.delete(candidate.moveScope.bucket);
|
|
537
|
+
};
|
|
538
|
+
for (const [index, step] of steps.entries()) {
|
|
539
|
+
if (consumedStepIndexes.has(index) || step.type !== "revisedOnly") continue;
|
|
540
|
+
const candidate = idStability(step.block) === "stable" ? stableBaseById.get(step.block.id) : void 0;
|
|
541
|
+
if (!candidate || taken.has(candidate.block.id)) continue;
|
|
542
|
+
taken.add(candidate.block.id);
|
|
543
|
+
takenRevised.add(step.block.id);
|
|
544
|
+
removeSimilarityCandidate(candidate);
|
|
545
|
+
if (candidate.moveScope.bucket === step.moveScope.bucket) moves.push({
|
|
546
|
+
baseBlock: candidate.block,
|
|
547
|
+
revisedBlock: step.block
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
for (const [index, step] of steps.entries()) {
|
|
551
|
+
if (consumedStepIndexes.has(index) || step.type !== "revisedOnly" || takenRevised.has(step.block.id)) continue;
|
|
552
|
+
const exact = (exactCandidatesByBucket.get(step.moveScope.bucket)?.get(step.block.text))?.find((candidate) => !taken.has(candidate.block.id) && candidate.moveScope.gap !== step.moveScope.gap);
|
|
553
|
+
if (exact) {
|
|
554
|
+
taken.add(exact.block.id);
|
|
555
|
+
takenRevised.add(step.block.id);
|
|
556
|
+
removeSimilarityCandidate(exact);
|
|
557
|
+
moves.push({
|
|
558
|
+
baseBlock: exact.block,
|
|
559
|
+
revisedBlock: step.block
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
for (const [index, step] of steps.entries()) {
|
|
564
|
+
if (consumedStepIndexes.has(index) || step.type !== "revisedOnly" || takenRevised.has(step.block.id)) continue;
|
|
565
|
+
const revisedProfile = tokenProfile(step.block.text);
|
|
566
|
+
if (!revisedProfile) continue;
|
|
567
|
+
let best = null;
|
|
568
|
+
const candidatesByGap = similarityCandidatesByBucket.get(step.moveScope.bucket);
|
|
569
|
+
candidateGroups: for (const [gap, candidates] of candidatesByGap ?? []) {
|
|
570
|
+
if (gap === step.moveScope.gap) continue;
|
|
571
|
+
for (const candidate of candidates.values()) {
|
|
572
|
+
if (workSession.remainingMoveComparisons <= 0) break candidateGroups;
|
|
573
|
+
workSession.remainingMoveComparisons--;
|
|
574
|
+
const scored = tokenSimilarity(candidate.profile, revisedProfile, workSession.remainingMoveTokenLookups);
|
|
575
|
+
if (!scored) continue;
|
|
576
|
+
workSession.remainingMoveTokenLookups -= scored.lookups;
|
|
577
|
+
const { similarity } = scored;
|
|
578
|
+
if (similarity >= MOVE_SIMILARITY_THRESHOLD && (best === null || similarity > best.similarity || similarity === best.similarity && candidate.order < best.candidate.order)) best = {
|
|
579
|
+
candidate,
|
|
580
|
+
similarity
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (best) {
|
|
585
|
+
taken.add(best.candidate.block.id);
|
|
586
|
+
takenRevised.add(step.block.id);
|
|
587
|
+
removeSimilarityCandidate(best.candidate);
|
|
588
|
+
moves.push({
|
|
589
|
+
baseBlock: best.candidate.block,
|
|
590
|
+
revisedBlock: step.block
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return moves;
|
|
595
|
+
};
|
|
596
|
+
const structuralChangeForStep = (step, id) => {
|
|
597
|
+
switch (step.type) {
|
|
598
|
+
case "baseTable": return {
|
|
599
|
+
id,
|
|
600
|
+
type: "table-delete",
|
|
601
|
+
tableIndex: step.location.tableIndex,
|
|
602
|
+
baseBlockIds: step.blocks.map(({ id: blockId }) => blockId)
|
|
603
|
+
};
|
|
604
|
+
case "revisedTable": return {
|
|
605
|
+
id,
|
|
606
|
+
type: "table-insert",
|
|
607
|
+
tableIndex: step.location.tableIndex,
|
|
608
|
+
revisedBlockIds: step.blocks.map(({ id: blockId }) => blockId)
|
|
609
|
+
};
|
|
610
|
+
case "baseRow": return {
|
|
611
|
+
id,
|
|
612
|
+
type: "table-row-delete",
|
|
613
|
+
tableIndex: step.location.tableIndex,
|
|
614
|
+
rowIndex: step.location.rowIndex,
|
|
615
|
+
baseBlockIds: step.blocks.map(({ id: blockId }) => blockId)
|
|
616
|
+
};
|
|
617
|
+
case "revisedRow": return {
|
|
618
|
+
id,
|
|
619
|
+
type: "table-row-insert",
|
|
620
|
+
tableIndex: step.location.tableIndex,
|
|
621
|
+
rowIndex: step.location.rowIndex,
|
|
622
|
+
revisedBlockIds: step.blocks.map(({ id: blockId }) => blockId)
|
|
623
|
+
};
|
|
624
|
+
case "baseColumn": return {
|
|
625
|
+
id,
|
|
626
|
+
type: "table-column-delete",
|
|
627
|
+
tableIndex: step.location.tableIndex,
|
|
628
|
+
columnIndex: step.columnIndex,
|
|
629
|
+
baseBlockIds: step.blocks.map(({ id: blockId }) => blockId)
|
|
630
|
+
};
|
|
631
|
+
case "revisedColumn": return {
|
|
632
|
+
id,
|
|
633
|
+
type: "table-column-insert",
|
|
634
|
+
tableIndex: step.location.tableIndex,
|
|
635
|
+
columnIndex: step.columnIndex,
|
|
636
|
+
revisedBlockIds: step.blocks.map(({ id: blockId }) => blockId)
|
|
637
|
+
};
|
|
638
|
+
default: return null;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const formattingChange = (base, revised, maxRanges) => {
|
|
642
|
+
const paragraph = changedFolioContentParagraphFormatting(base, revised);
|
|
643
|
+
const ranges = base.text === revised.text ? inlineFormattingSegments({
|
|
644
|
+
baseBlock: base,
|
|
645
|
+
targetBlock: revised,
|
|
646
|
+
maxSegments: maxRanges
|
|
647
|
+
}) : [];
|
|
648
|
+
if (ranges === null) return "limit";
|
|
649
|
+
return paragraph || ranges.length > 0 ? {
|
|
650
|
+
...paragraph && { paragraph },
|
|
651
|
+
ranges
|
|
652
|
+
} : null;
|
|
653
|
+
};
|
|
654
|
+
const compareAlignedFolioContent = ({ baseBlocks, revisedBlocks, steps, workSession, maxChanges, idStability }) => {
|
|
655
|
+
const paragraphPlans = detectFolioContentParagraphMarkPlans(steps);
|
|
656
|
+
const consumed = new Set([...paragraphPlans.keys()].map((index) => index + 1));
|
|
657
|
+
const moves = detectFolioContentMoves({
|
|
658
|
+
steps,
|
|
659
|
+
consumedStepIndexes: consumed,
|
|
660
|
+
workSession,
|
|
661
|
+
...idStability && { idStability }
|
|
662
|
+
});
|
|
663
|
+
const moveByBaseId = new Map(moves.map((move, index) => [move.baseBlock.id, {
|
|
664
|
+
...move,
|
|
665
|
+
moveId: index + 1
|
|
666
|
+
}]));
|
|
667
|
+
const moveByRevisedId = new Map(moves.map((move, index) => [move.revisedBlock.id, {
|
|
668
|
+
...move,
|
|
669
|
+
moveId: index + 1
|
|
670
|
+
}]));
|
|
671
|
+
const { diffText } = workSession;
|
|
672
|
+
const relations = [];
|
|
673
|
+
const baseRelation = /* @__PURE__ */ new Map();
|
|
674
|
+
const revisedRelation = /* @__PURE__ */ new Map();
|
|
675
|
+
const structuralChanges = [];
|
|
676
|
+
let nextRelationId = 0;
|
|
677
|
+
let nextStructuralId = 0;
|
|
678
|
+
let remainingFormattingRanges = maxChanges;
|
|
679
|
+
let changeCount = 0;
|
|
680
|
+
const addRelation = (event, relationBaseBlocks, relationRevisedBlocks) => {
|
|
681
|
+
if (event.type !== "unchanged") {
|
|
682
|
+
changeCount++;
|
|
683
|
+
if (changeCount > maxChanges) return false;
|
|
684
|
+
}
|
|
685
|
+
const relation = {
|
|
686
|
+
id: nextRelationId++,
|
|
687
|
+
baseBlocks: relationBaseBlocks,
|
|
688
|
+
revisedBlocks: relationRevisedBlocks,
|
|
689
|
+
event
|
|
690
|
+
};
|
|
691
|
+
relations.push(relation);
|
|
692
|
+
for (const block of relationBaseBlocks) {
|
|
693
|
+
if (baseRelation.has(block.id)) panic("Content alignment assigned one base block more than once", { blockId: block.id });
|
|
694
|
+
baseRelation.set(block.id, relation);
|
|
695
|
+
}
|
|
696
|
+
for (const block of relationRevisedBlocks) {
|
|
697
|
+
if (revisedRelation.has(block.id)) panic("Content alignment assigned one revised block more than once", { blockId: block.id });
|
|
698
|
+
revisedRelation.set(block.id, relation);
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
};
|
|
702
|
+
const changeLimitExceeded = () => Result.err(limitExceeded({
|
|
703
|
+
input: "result",
|
|
704
|
+
limit: "changes",
|
|
705
|
+
maximum: maxChanges,
|
|
706
|
+
actual: maxChanges + 1
|
|
707
|
+
}));
|
|
708
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
709
|
+
if (consumed.has(stepIndex)) continue;
|
|
710
|
+
const paragraphPlan = paragraphPlans.get(stepIndex);
|
|
711
|
+
if (paragraphPlan?.type === "split") {
|
|
712
|
+
const event = {
|
|
713
|
+
type: "split",
|
|
714
|
+
baseBlocks: [paragraphPlan.baseBlock],
|
|
715
|
+
revisedBlocks: paragraphPlan.revisedBlocks,
|
|
716
|
+
offset: paragraphPlan.offset,
|
|
717
|
+
separator: paragraphPlan.separator
|
|
718
|
+
};
|
|
719
|
+
if (!addRelation(event, event.baseBlocks, event.revisedBlocks)) return changeLimitExceeded();
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (paragraphPlan?.type === "merge") {
|
|
723
|
+
const event = {
|
|
724
|
+
type: "merge",
|
|
725
|
+
baseBlocks: paragraphPlan.baseBlocks,
|
|
726
|
+
revisedBlocks: [paragraphPlan.revisedBlock],
|
|
727
|
+
separator: paragraphPlan.separator
|
|
728
|
+
};
|
|
729
|
+
if (!addRelation(event, event.baseBlocks, event.revisedBlocks)) return changeLimitExceeded();
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (step.type === "pair") {
|
|
733
|
+
const base = step.baseBlock;
|
|
734
|
+
const revised = step.revisedBlock;
|
|
735
|
+
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
|
+
}));
|
|
743
|
+
remainingFormattingRanges -= formatting?.ranges.length ?? 0;
|
|
744
|
+
let event;
|
|
745
|
+
if (base.text !== revised.text || properties.length > 0) event = {
|
|
746
|
+
type: "modified",
|
|
747
|
+
baseBlocks: [base],
|
|
748
|
+
revisedBlocks: [revised],
|
|
749
|
+
segments: withTextOffsets(diffText(base.text, revised.text)),
|
|
750
|
+
changedProperties: properties,
|
|
751
|
+
...formatting && { formatting }
|
|
752
|
+
};
|
|
753
|
+
else if (formatting) event = {
|
|
754
|
+
type: "formatting",
|
|
755
|
+
baseBlocks: [base],
|
|
756
|
+
revisedBlocks: [revised],
|
|
757
|
+
formatting
|
|
758
|
+
};
|
|
759
|
+
else event = {
|
|
760
|
+
type: "unchanged",
|
|
761
|
+
baseBlocks: [base],
|
|
762
|
+
revisedBlocks: [revised]
|
|
763
|
+
};
|
|
764
|
+
if (!addRelation(event, [base], [revised])) return changeLimitExceeded();
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (step.type === "baseOnly") {
|
|
768
|
+
const move = moveByBaseId.get(step.block.id);
|
|
769
|
+
if (!addRelation(move ? {
|
|
770
|
+
type: "movedFrom",
|
|
771
|
+
baseBlocks: [step.block],
|
|
772
|
+
revisedBlocks: [],
|
|
773
|
+
moveId: move.moveId
|
|
774
|
+
} : {
|
|
775
|
+
type: "deleted",
|
|
776
|
+
baseBlocks: [step.block],
|
|
777
|
+
revisedBlocks: []
|
|
778
|
+
}, [step.block], [])) return changeLimitExceeded();
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (step.type === "revisedOnly") {
|
|
782
|
+
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
|
+
}));
|
|
790
|
+
remainingFormattingRanges -= moveFormatting?.ranges.length ?? 0;
|
|
791
|
+
const moveProperties = move ? changedBlockProperties(move.baseBlock, step.block) : [];
|
|
792
|
+
if (!addRelation(move ? {
|
|
793
|
+
type: "movedTo",
|
|
794
|
+
baseBlocks: [],
|
|
795
|
+
revisedBlocks: [step.block],
|
|
796
|
+
moveId: move.moveId,
|
|
797
|
+
baseBlockId: move.baseBlock.id,
|
|
798
|
+
...move.baseBlock.text !== step.block.text && { segments: withTextOffsets(diffText(move.baseBlock.text, step.block.text)) },
|
|
799
|
+
...moveProperties.length > 0 && { changedProperties: moveProperties },
|
|
800
|
+
...moveFormatting && { formatting: moveFormatting }
|
|
801
|
+
} : {
|
|
802
|
+
type: "inserted",
|
|
803
|
+
baseBlocks: [],
|
|
804
|
+
revisedBlocks: [step.block]
|
|
805
|
+
}, [], [step.block])) return changeLimitExceeded();
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const structural = structuralChangeForStep(step, ++nextStructuralId);
|
|
809
|
+
if (!structural) return panic("Unhandled content alignment step", { step });
|
|
810
|
+
structuralChanges.push(structural);
|
|
811
|
+
if ("baseBlockIds" in structural) {
|
|
812
|
+
for (const block of step.blocks) if (!addRelation({
|
|
813
|
+
type: "deleted",
|
|
814
|
+
baseBlocks: [block],
|
|
815
|
+
revisedBlocks: [],
|
|
816
|
+
structuralChangeId: structural.id
|
|
817
|
+
}, [block], [])) return changeLimitExceeded();
|
|
818
|
+
} else for (const block of step.blocks) if (!addRelation({
|
|
819
|
+
type: "inserted",
|
|
820
|
+
baseBlocks: [],
|
|
821
|
+
revisedBlocks: [block],
|
|
822
|
+
structuralChangeId: structural.id
|
|
823
|
+
}, [], [block])) return changeLimitExceeded();
|
|
824
|
+
}
|
|
825
|
+
const ordered = [];
|
|
826
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
827
|
+
let baseIndex = 0;
|
|
828
|
+
let revisedIndex = 0;
|
|
829
|
+
while (baseIndex < baseBlocks.length || revisedIndex < revisedBlocks.length) {
|
|
830
|
+
const base = baseBlocks[baseIndex];
|
|
831
|
+
const revised = revisedBlocks[revisedIndex];
|
|
832
|
+
const fromBase = base ? baseRelation.get(base.id) : void 0;
|
|
833
|
+
const fromRevised = revised ? revisedRelation.get(revised.id) : void 0;
|
|
834
|
+
if (fromBase && emitted.has(fromBase.id)) {
|
|
835
|
+
baseIndex++;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (fromRevised && emitted.has(fromRevised.id)) {
|
|
839
|
+
revisedIndex++;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
let relation;
|
|
843
|
+
if (fromBase && fromRevised && fromBase.id === fromRevised.id) relation = fromBase;
|
|
844
|
+
else if (fromBase?.revisedBlocks.length === 0) relation = fromBase;
|
|
845
|
+
else if (fromRevised?.baseBlocks.length === 0) relation = fromRevised;
|
|
846
|
+
if (!relation) return panic("Content alignment did not produce one monotone projection", {
|
|
847
|
+
baseBlockId: base?.id,
|
|
848
|
+
revisedBlockId: revised?.id
|
|
849
|
+
});
|
|
850
|
+
emitted.add(relation.id);
|
|
851
|
+
ordered.push(relation.event);
|
|
852
|
+
baseIndex += relation.baseBlocks.length;
|
|
853
|
+
revisedIndex += relation.revisedBlocks.length;
|
|
854
|
+
}
|
|
855
|
+
if (emitted.size !== relations.length) return panic("Content alignment left relations outside the ordered projection");
|
|
856
|
+
return Result.ok({
|
|
857
|
+
events: ordered,
|
|
858
|
+
structuralChanges
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
/** Compare two representation-neutral ordered content snapshots. */
|
|
862
|
+
const compareContent = (options) => {
|
|
863
|
+
if (!isRecord(options)) return Result.err(invalidInput("options", "options", "Comparison options must be an object."));
|
|
864
|
+
const { base, revised, granularity } = options;
|
|
865
|
+
if (granularity !== void 0 && !WORD_DIFF_GRANULARITIES.some((candidate) => candidate === granularity)) return Result.err(invalidInput("options", "granularity", "Comparison granularity must be word or character."));
|
|
866
|
+
const baseError = validateSnapshot(base, "base");
|
|
867
|
+
if (baseError) return Result.err(baseError);
|
|
868
|
+
const revisedError = validateSnapshot(revised, "revised");
|
|
869
|
+
if (revisedError) return Result.err(revisedError);
|
|
870
|
+
const workSession = createContentComparisonWorkSession(granularity);
|
|
871
|
+
const steps = alignFolioContentStructure({
|
|
872
|
+
baseBlocks: base.blocks,
|
|
873
|
+
revisedBlocks: revised.blocks,
|
|
874
|
+
workSession: workSession.alignment
|
|
875
|
+
});
|
|
876
|
+
return compareAlignedFolioContent({
|
|
877
|
+
baseBlocks: base.blocks,
|
|
878
|
+
revisedBlocks: revised.blocks,
|
|
879
|
+
steps,
|
|
880
|
+
workSession,
|
|
881
|
+
maxChanges: FOLIO_CONTENT_COMPARISON_LIMITS.changes
|
|
882
|
+
});
|
|
883
|
+
};
|
|
884
|
+
//#endregion
|
|
885
|
+
export { FOLIO_CONTENT_COMPARISON_LIMITS, FolioContentComparisonLimitError, InvalidFolioContentComparisonError, changedFolioContentParagraphFormatting, compareAlignedFolioContent, compareContent, createContentComparisonWorkSession, detectFolioContentMoves, detectFolioContentParagraphMarkPlans };
|