@a3s-lab/office 0.31.0 → 0.32.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 +6 -3
- package/dist/0~4808.js +1 -1
- package/dist/0~4980.js +1 -1
- package/dist/0~7240.js +1 -1
- package/dist/0~7614.js +1 -1
- package/dist/0~document-editor.js +1085 -4
- package/dist/0~spreadsheet-editor.js +2 -1
- package/dist/0~work-docx-export.js +1 -1
- package/dist/0~work-docx-import.js +10 -27
- package/dist/0~work-docx-large-document-import.js +1 -1
- package/dist/0~work-office-diagnostics.js +3 -3
- package/dist/0~work-pptx-import.js +1 -1
- package/dist/{1544.js → 3266.js} +17 -1862
- package/dist/4121.js +13 -0
- package/dist/4166.js +3848 -0
- package/dist/{6282.js → 4174.js} +383 -10
- package/dist/9333.js +1849 -0
- package/dist/core.js +5259 -4
- package/dist/index.js +3 -3
- package/dist/internal/features/work/editors/document-compare-dialog.d.ts +14 -0
- package/dist/internal/features/work/editors/document-toolbar.d.ts +3 -1
- package/dist/internal/features/work/editors/use-document-comparison.d.ts +9 -0
- package/dist/internal/features/work/work-document-changes.d.ts +5 -0
- package/dist/internal/features/work/work-document-compare-diff.d.ts +38 -0
- package/dist/internal/features/work/work-document-compare-stability.d.ts +5 -0
- package/dist/internal/features/work/work-document-compare.d.ts +27 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/styles.css +194 -0
- package/docs/latest/en/browser-editor-architecture.md +23 -0
- package/package.json +6 -3
- package/dist/4104.js +0 -9336
|
@@ -6,8 +6,8 @@ import { Fragment as external_react_Fragment, forwardRef, useCallback, useEffect
|
|
|
6
6
|
import { Extension, createNodeFromContent } from "@tiptap/core";
|
|
7
7
|
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
8
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
9
|
-
import { AArrowDown, AArrowUp, ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowUp, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, BookMarked, BookOpen, Bookmark, CaseUpper, Check, CheckCheck, CheckCircle2, ChevronDown, ChevronRight, ChevronUp, ClipboardCopy, ClipboardPaste, Cloud, Columns3, Eraser, Eye, FileDiff, FilePlus2, FileText, Globe2, Grid2X2, Hash, Highlighter, Image, IndentDecrease, IndentIncrease, Italic, Languages, Link2, List, ListChecks, ListOrdered, ListTree, MessageSquarePlus, MessageSquareReply, MessagesSquare, MoveVertical, Paintbrush, Palette, PanelBottom, PanelBottomOpen, PanelLeftOpen, PanelTop, PanelTopOpen, Pilcrow, PilcrowLeft, PilcrowRight, Plus, Quote, Redo2, RefreshCw, Replace, ReplaceAll, RotateCcw, Rows3, Ruler, Scan, Search, Settings2, Square, StretchHorizontal, Strikethrough, Subscript, Superscript, Table2, TableCellsMerge, TableCellsSplit, TableProperties, Tags, TextWrap, Trash2, TriangleAlert, Type, Underline, Undo2, Unlink, Unlink2, X, XCircle, ZoomIn, ZoomOut } from "lucide-react";
|
|
10
|
-
import { Node as model_Node } from "@tiptap/pm/model";
|
|
9
|
+
import { AArrowDown, AArrowUp, ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowUp, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, BookMarked, BookOpen, Bookmark, CaseUpper, Check, CheckCheck, CheckCircle2, ChevronDown, ChevronRight, ChevronUp, ClipboardCopy, ClipboardPaste, Cloud, Columns3, Eraser, Eye, FileCheck2, FileDiff, FilePlus2, FileStack, FileText, FileUp, GitCompareArrows, Globe2, Grid2X2, Hash, Highlighter, Image, IndentDecrease, IndentIncrease, Italic, Languages, Link2, List, ListChecks, ListOrdered, ListTree, MessageSquarePlus, MessageSquareReply, MessagesSquare, MoveVertical, Paintbrush, Palette, PanelBottom, PanelBottomOpen, PanelLeftOpen, PanelTop, PanelTopOpen, Pilcrow, PilcrowLeft, PilcrowRight, Plus, Quote, Redo2, RefreshCw, Replace, ReplaceAll, RotateCcw, Rows3, Ruler, Scan, Search, Settings2, Square, StretchHorizontal, Strikethrough, Subscript, Superscript, Table2, TableCellsMerge, TableCellsSplit, TableProperties, Tags, TextWrap, Trash2, TriangleAlert, Type, Underline, Undo2, Unlink, Unlink2, X, XCircle, ZoomIn, ZoomOut } from "lucide-react";
|
|
10
|
+
import { Fragment as model_Fragment, Node as model_Node } from "@tiptap/pm/model";
|
|
11
11
|
import extension_color from "@tiptap/extension-color";
|
|
12
12
|
import { TableKit } from "@tiptap/extension-table";
|
|
13
13
|
import extension_text_align from "@tiptap/extension-text-align";
|
|
@@ -15,7 +15,8 @@ import font_size from "@tiptap/extension-text-style/font-size";
|
|
|
15
15
|
import starter_kit from "@tiptap/starter-kit";
|
|
16
16
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
17
17
|
import { TableMap, isInTable, selectedRect } from "@tiptap/pm/tables";
|
|
18
|
-
import {
|
|
18
|
+
import { closeHistory } from "@tiptap/pm/history";
|
|
19
|
+
import { documentTableSizing, transferChangedDocumentTextStatistics, documentPageBordersVisible, documentUnderlineStyle, DOCUMENT_PAGE_BORDER_EDGES, documentTableRowOptions, normalizeDocumentColumns, clampDocumentMargin, materializeLazyDocumentEditorRoot, documentKerningThresholdPoints, selectedDocumentIndexOptions, nextDocumentTabAlignment, DOCUMENT_BOOKMARK_DUPLICATE_MESSAGE, normalizedTabPosition, documentCitationCount, resolveDocumentPageBorders, documentLazyHtmlChunkFragment, DocumentFontFamily, normalizeDocumentHiddenText, documentLegacyTextEffectsConflict, DOCUMENT_CHUNK_VISIBLE_IDS_META, selectedDocumentIndexDraft, MAX_DOCUMENT_IMAGE_RELATIVE_HEIGHT, retainAnchoredDocumentComments, DocumentTableRowIdentity, work_document_page_margins_twipsToMillimeters, DEFAULT_DOCUMENT_TABLE_CELL_FORMAT, resolveWorkDocumentEditorInput, documentCharacterPositionPoints, documentBulletListStyle, collectDocumentTextLayoutParagraphs, readOfficeDocumentCollaboration as readWorkOfficeDocumentCollaboration, normalizeDocumentCharacterScalePercent, DOCUMENT_PARAGRAPH_FORMAT_ATTRIBUTES, documentOrderedListState, normalizeDocumentPageChrome, documentTextStatistics, documentPageMarginsForLayout, invalidateDocumentLazyHtmlProjection, documentTableCellFormat, normalizeDocumentPageGeometry, normalizeDocumentImageLayer, documentImageProperties, isValidDocumentCitationTag, sanitizeDocumentPageChromeHtml, pageTwipsToMillimeters, normalizeDocumentPageBorders, importWorkDocumentFile, DocumentParagraphFormatting, documentModelHasTrustedInitialIntegrityFeatures, normalizeDocumentHtml, documentInitialSectionLayout, activeDocumentSection, documentParagraphDirection, documentParagraphPagination, isDocumentCharacterFormatMark, documentModelUsesWindowing, documentPageChromeLegacyFields, documentChunkMountedIds, collectDocumentCommentAnchors, documentTextCaseKeyboardShortcuts, DocumentTextStyle, canChangeDocumentIndent, documentCharacterSpacingPoints, documentKerningIsEffective, DocumentSuperscript, documentSectionById, activeDocumentBookmark, DocumentEquation, documentLazyHtmlProjectionFingerprint, documentLazyHtmlProjection, normalizeDocumentParagraphIndent, updateDocumentGutterPosition, DocumentStrike, mountWorkLiveDocumentCapture, registerDocumentPageSurfaceGeometry, documentHiddenTextKeyboardShortcut, documentHasIndex, syncDocumentContentFromHtml, documentLegacyTextEffectsCss, editorDocumentCaptionTargets, resolveDocumentPageChrome, documentImageLayoutOptions, MAX_DOCUMENT_NUMBERING_START, resolveDocumentPageSize, editorDocumentBookmarkReferenceTargets, serializeWorkDocumentNode, DOCUMENT_CHUNK_PAGINATION_META, documentModelForContent, createOfficeDocumentCollaborationBinding as createWorkOfficeDocumentCollaborationBinding, documentLegacyTextEffectsFromTextStyleAttributes, normalizeDocumentEmphasisMark, selectedDocumentChunkId, documentCommentViews, documentParagraphSpacing, normalizeDocumentTabStops, patchDocumentLazyHtmlProjection, normalizeDocumentTextCase, documentUnderlineColor, documentBookmarkNameExists, collectDocumentChanges, DocumentImage, selectedDocumentTableOfContentsOptions, resolveDocumentPageMargins, updateDocumentPageMarginMode, serializeDocumentParagraphFormatting, canSetDocumentTableRowRepeatHeader, normalizeDocumentCharacterSpacingTwips, DocumentCharacterFormatting, millimetersToPixels as work_document_layout_millimetersToPixels, DOCUMENT_PARAGRAPH_CHANGE_ATTRIBUTES, serializeDocumentCharacterFormatting, DOCUMENT_CHUNK_HYDRATION_META, documentTabLeaderLabel, createSchemaDerivedWorkDocumentModel, updateDocumentCustomPageMillimeters, DocumentUnderline, documentCommentDraftRange, documentParagraphTabStops, documentTableHorizontalAlignment, documentUnderlineKeyboardShortcuts, documentParagraphIndent, DocumentScriptFontFormatting, canInsertDocumentComment, resolveAllDocumentChanges, documentHasTableOfContents, documentTextLayoutBatches, normalizeDocumentImageWrapSide, updateDocumentPageOrientation, documentCitationStyleDetails, validateDocumentBookmarkName, setCustomDocumentColumns, updateDocumentPageMarginMillimeters, documentCitationStyle, normalizeDocumentCharacterPositionHalfPoints, documentTransactionsOnlyHydrateChunks, normalizeDocumentKerningThresholdHalfPoints, forgetSourceBlob as forgetWorkSourceBlob, DEFAULT_DOCUMENT_TABLE_CELL_MARGINS, DOCUMENT_TABLE_STYLE_OPTIONS, updateDocumentColumnWidth, updateDocumentPaperSizePreset, documentPageMetrics, documentStrikeStyle, DocumentHighlight, createDocumentBibliography, DocumentSubscript, selectedDocumentIndexEntry, updateDocumentPageChromeVariant, updateDocumentMirrorMargins, activeDocumentTableStyle, measureDocumentLayoutBlocksIncrementally, documentWordLineHeightFactor, normalizeDocumentLegacyTextEffect, createWorkDocumentExtensions } from "./4174.js";
|
|
19
20
|
import { useOfficeTaskPaneEscape, normalizeOfficeFontFamily, OfficeCheckbox, useOfficeDialog, useOfficeTaskPaneModal, CommittedOfficeNumberField, officeFontFamilyLabel, officeFontFamilies, CollectionState, createOfficeKernelClient, OfficeNumberField } from "./0~7048.js";
|
|
20
21
|
import { moveOfficeMenuFocus, office_text_field_OfficeTextArea, Popover, officeOverlayPortalRoot, button_Button, useOfficePublishPresenceLocation, OfficeTiptapPresenceLayer, icon_button_IconButton, office_text_field_OfficeTextField, useDialogFocusScope, useOfficeEditorWheelZoom, office_text_field_OfficeFileInput } from "./0~6090.js";
|
|
21
22
|
import { WorkOfficePreviewBar, isWorkspaceContextMenuKeyboardEvent, Tabs, WorkOfficeZoomControls, WorkOfficeStatusBar, Dialog, WorkOfficeRibbon, workspaceContextMenuPosition, OfficeSelect, WorkspaceContextMenu, WorkOfficeRibbonGroup, WorkOfficeRibbonButton } from "./0~4595.js";
|
|
@@ -26,6 +27,7 @@ import { OfficeTableInsertPopover, readOfficeFileAsDataUrl } from "./0~9445.js";
|
|
|
26
27
|
import { mergeOfficeTiptapExtensions, documentHasRefreshableFields, documentCurrentPage, documentAgentMenuItems, documentPageCount, createWorkDocumentSelectionSnapshot, documentPlainTextAsHtml, createWorkDocumentSelectionAction, documentCustomSelectionMenuItems } from "./0~5050.js";
|
|
27
28
|
import { WorkEditorLoadingState, documentLayoutFontKey, useOfficeCollaborationLocationNavigator, OFFICE_DOCUMENT_LAYOUT_HEBREW_FONT_ID, useOfficeEditorInitialFocus, OFFICE_DOCUMENT_LAYOUT_FONT_ID, OFFICE_DOCUMENT_LAYOUT_ARABIC_FONT_ID, OFFICE_DOCUMENT_LAYOUT_LATIN_FONT_ID } from "./432.js";
|
|
28
29
|
import { useOfficeDraft, InlineNotice } from "./0~345.js";
|
|
30
|
+
import { showToast } from "./6489.js";
|
|
29
31
|
import { documentPageColor, normalizeDocumentPageColor } from "./0~work-document-page-color.js";
|
|
30
32
|
var document_editor_namespaceObject = {};
|
|
31
33
|
__webpack_require__.r(document_editor_namespaceObject);
|
|
@@ -15143,7 +15145,7 @@ function headingLevelForShortcut(code, key) {
|
|
|
15143
15145
|
if ('3' === value) return 3;
|
|
15144
15146
|
return null;
|
|
15145
15147
|
}
|
|
15146
|
-
function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly = false, suggestionOnly = false, history, layoutOpen, layout, layoutFonts = [], navigationOpen, pageColor, showPageNumbers, showHiddenText, showRulers, spellcheckEnabled, viewMode, zoom, pageChromeEditor, pageChromeEditingPart, pageChromeShowPageNumber, onRequestImage, onPageChromeEditingPartChange, onClosePageChrome, onTogglePageChromePageNumber, onToggleLayout, onLayoutChange, onOpenLayout, onToggleNavigation, onTogglePageNumbers, onToggleHiddenText, onToggleRulers, onPageColorChange, onToggleSpellcheck, onViewModeChange, onZoomChange, onZoomFit, onInsertSection, onInsertNote, onInsertCaption, onInsertCrossReference, onOpenTableOfContents, onOpenIndexEntry, onOpenIndex, citationsOpen, citationSourceCount, onToggleCitations, onInsertField, onRefreshFields, onRefreshIndex, onRefreshTableOfContents, canInsertComment, onInsertComment, commentsOpen, commentCount, onToggleComments, trackChanges, changesOpen, changeCount, findReplaceMode, fileActions, onRibbonTabChange, onToggleTrackChanges, onToggleChanges, onDecideChange, onOpenWordCount, onOpenFindReplace }) {
|
|
15148
|
+
function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly = false, suggestionOnly = false, history, layoutOpen, layout, layoutFonts = [], navigationOpen, pageColor, showPageNumbers, showHiddenText, showRulers, spellcheckEnabled, viewMode, zoom, pageChromeEditor, pageChromeEditingPart, pageChromeShowPageNumber, onRequestImage, onPageChromeEditingPartChange, onClosePageChrome, onTogglePageChromePageNumber, onToggleLayout, onLayoutChange, onOpenLayout, onToggleNavigation, onTogglePageNumbers, onToggleHiddenText, onToggleRulers, onPageColorChange, onToggleSpellcheck, onViewModeChange, onZoomChange, onZoomFit, onInsertSection, onInsertNote, onInsertCaption, onInsertCrossReference, onOpenTableOfContents, onOpenIndexEntry, onOpenIndex, citationsOpen, citationSourceCount, onToggleCitations, onInsertField, onRefreshFields, onRefreshIndex, onRefreshTableOfContents, canInsertComment, onInsertComment, commentsOpen, commentCount, onToggleComments, trackChanges, changesOpen, changeCount, findReplaceMode, fileActions, onRibbonTabChange, onToggleTrackChanges, onToggleChanges, onOpenComparison, onDecideChange, onOpenWordCount, onOpenFindReplace }) {
|
|
15147
15149
|
const [activeTab, setActiveTab] = useState(reviewOnly ? 'review' : 'home');
|
|
15148
15150
|
const [fontDialogRequest, setFontDialogRequest] = useState(null);
|
|
15149
15151
|
const [proofingDialogRequest, setProofingDialogRequest] = useState(null);
|
|
@@ -15705,6 +15707,30 @@ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly =
|
|
|
15705
15707
|
]
|
|
15706
15708
|
})
|
|
15707
15709
|
]
|
|
15710
|
+
}),
|
|
15711
|
+
!reviewOnly && !suggestionOnly && /*#__PURE__*/ jsxs(document_toolbar_RibbonGroup, {
|
|
15712
|
+
label: "比较",
|
|
15713
|
+
priority: "high",
|
|
15714
|
+
children: [
|
|
15715
|
+
/*#__PURE__*/ jsx(document_toolbar_ToolbarButton, {
|
|
15716
|
+
label: "比较文档",
|
|
15717
|
+
displayLabel: true,
|
|
15718
|
+
title: "将另一个版本转换为可接受或拒绝的修订",
|
|
15719
|
+
onClick: ()=>onOpenComparison('compare'),
|
|
15720
|
+
children: /*#__PURE__*/ jsx(GitCompareArrows, {
|
|
15721
|
+
size: 19
|
|
15722
|
+
})
|
|
15723
|
+
}),
|
|
15724
|
+
/*#__PURE__*/ jsx(document_toolbar_ToolbarButton, {
|
|
15725
|
+
label: "合并文档",
|
|
15726
|
+
displayLabel: true,
|
|
15727
|
+
title: "合入与当前基线一致的带修订审阅副本",
|
|
15728
|
+
onClick: ()=>onOpenComparison('combine'),
|
|
15729
|
+
children: /*#__PURE__*/ jsx(FileStack, {
|
|
15730
|
+
size: 19
|
|
15731
|
+
})
|
|
15732
|
+
})
|
|
15733
|
+
]
|
|
15708
15734
|
})
|
|
15709
15735
|
]
|
|
15710
15736
|
}),
|
|
@@ -16243,6 +16269,1052 @@ function commentActor(actor) {
|
|
|
16243
16269
|
author: CURRENT_COMMENT_AUTHOR
|
|
16244
16270
|
};
|
|
16245
16271
|
}
|
|
16272
|
+
const GAP_COST = 1;
|
|
16273
|
+
const ALIGNMENT_EPSILON = 1e-9;
|
|
16274
|
+
function boundedDocumentSequenceDiff(left, right, equal, maximumCells) {
|
|
16275
|
+
if ((left.length + 1) * (right.length + 1) > maximumCells) return null;
|
|
16276
|
+
const columns = right.length + 1;
|
|
16277
|
+
const CellArray = Math.max(left.length, right.length) <= 65535 ? Uint16Array : Uint32Array;
|
|
16278
|
+
const matrix = new CellArray((left.length + 1) * columns);
|
|
16279
|
+
for(let leftIndex = left.length - 1; leftIndex >= 0; leftIndex -= 1)for(let rightIndex = right.length - 1; rightIndex >= 0; rightIndex -= 1){
|
|
16280
|
+
const offset = leftIndex * columns + rightIndex;
|
|
16281
|
+
matrix[offset] = equal(left[leftIndex], right[rightIndex]) ? 1 + matrix[(leftIndex + 1) * columns + rightIndex + 1] : Math.max(matrix[(leftIndex + 1) * columns + rightIndex], matrix[leftIndex * columns + rightIndex + 1]);
|
|
16282
|
+
}
|
|
16283
|
+
const steps = [];
|
|
16284
|
+
let leftIndex = 0;
|
|
16285
|
+
let rightIndex = 0;
|
|
16286
|
+
while(leftIndex < left.length || rightIndex < right.length){
|
|
16287
|
+
if (leftIndex < left.length && rightIndex < right.length && equal(left[leftIndex], right[rightIndex])) {
|
|
16288
|
+
appendSequenceDiff(steps, 'equal', left[leftIndex], right[rightIndex]);
|
|
16289
|
+
leftIndex += 1;
|
|
16290
|
+
rightIndex += 1;
|
|
16291
|
+
continue;
|
|
16292
|
+
}
|
|
16293
|
+
const deleteScore = leftIndex < left.length ? matrix[(leftIndex + 1) * columns + rightIndex] : -1;
|
|
16294
|
+
const insertScore = rightIndex < right.length ? matrix[leftIndex * columns + rightIndex + 1] : -1;
|
|
16295
|
+
if (leftIndex < left.length && deleteScore >= insertScore) {
|
|
16296
|
+
appendSequenceDiff(steps, 'delete', left[leftIndex], null);
|
|
16297
|
+
leftIndex += 1;
|
|
16298
|
+
} else {
|
|
16299
|
+
appendSequenceDiff(steps, 'insert', null, right[rightIndex]);
|
|
16300
|
+
rightIndex += 1;
|
|
16301
|
+
}
|
|
16302
|
+
}
|
|
16303
|
+
return steps;
|
|
16304
|
+
}
|
|
16305
|
+
function boundedDocumentSequenceAlignment(left, right, substitutionCost, maximumCells) {
|
|
16306
|
+
if ((left.length + 1) * (right.length + 1) > maximumCells) return null;
|
|
16307
|
+
const columns = right.length + 1;
|
|
16308
|
+
const matrix = new Float64Array((left.length + 1) * columns);
|
|
16309
|
+
for(let leftIndex = 1; leftIndex <= left.length; leftIndex += 1)matrix[leftIndex * columns] = leftIndex * GAP_COST;
|
|
16310
|
+
for(let rightIndex = 1; rightIndex <= right.length; rightIndex += 1)matrix[rightIndex] = rightIndex * GAP_COST;
|
|
16311
|
+
for(let leftIndex = 1; leftIndex <= left.length; leftIndex += 1)for(let rightIndex = 1; rightIndex <= right.length; rightIndex += 1){
|
|
16312
|
+
const substitution = matrix[(leftIndex - 1) * columns + rightIndex - 1] + substitutionCost(left[leftIndex - 1], right[rightIndex - 1]);
|
|
16313
|
+
const deletion = matrix[(leftIndex - 1) * columns + rightIndex] + GAP_COST;
|
|
16314
|
+
const insertion = matrix[leftIndex * columns + rightIndex - 1] + GAP_COST;
|
|
16315
|
+
matrix[leftIndex * columns + rightIndex] = Math.min(substitution, deletion, insertion);
|
|
16316
|
+
}
|
|
16317
|
+
const reversed = [];
|
|
16318
|
+
let leftIndex = left.length;
|
|
16319
|
+
let rightIndex = right.length;
|
|
16320
|
+
while(leftIndex > 0 || rightIndex > 0){
|
|
16321
|
+
const current = matrix[leftIndex * columns + rightIndex];
|
|
16322
|
+
if (leftIndex > 0 && rightIndex > 0) {
|
|
16323
|
+
const cost = substitutionCost(left[leftIndex - 1], right[rightIndex - 1]);
|
|
16324
|
+
const substitution = matrix[(leftIndex - 1) * columns + rightIndex - 1] + cost;
|
|
16325
|
+
if (approximatelyEqual(current, substitution)) {
|
|
16326
|
+
reversed.push({
|
|
16327
|
+
kind: 0 === cost ? 'equal' : 'substitute',
|
|
16328
|
+
left: left[leftIndex - 1],
|
|
16329
|
+
right: right[rightIndex - 1]
|
|
16330
|
+
});
|
|
16331
|
+
leftIndex -= 1;
|
|
16332
|
+
rightIndex -= 1;
|
|
16333
|
+
continue;
|
|
16334
|
+
}
|
|
16335
|
+
}
|
|
16336
|
+
if (leftIndex > 0 && approximatelyEqual(current, matrix[(leftIndex - 1) * columns + rightIndex] + GAP_COST)) {
|
|
16337
|
+
reversed.push({
|
|
16338
|
+
kind: 'delete',
|
|
16339
|
+
left: left[leftIndex - 1],
|
|
16340
|
+
right: null
|
|
16341
|
+
});
|
|
16342
|
+
leftIndex -= 1;
|
|
16343
|
+
continue;
|
|
16344
|
+
}
|
|
16345
|
+
reversed.push({
|
|
16346
|
+
kind: 'insert',
|
|
16347
|
+
left: null,
|
|
16348
|
+
right: right[rightIndex - 1]
|
|
16349
|
+
});
|
|
16350
|
+
rightIndex -= 1;
|
|
16351
|
+
}
|
|
16352
|
+
return reversed.reverse();
|
|
16353
|
+
}
|
|
16354
|
+
function appendSequenceDiff(changes, kind, left, right) {
|
|
16355
|
+
const previous = changes.at(-1);
|
|
16356
|
+
if (previous?.kind === kind) {
|
|
16357
|
+
if ('equal' === previous.kind) {
|
|
16358
|
+
previous.left.push(left);
|
|
16359
|
+
previous.right.push(right);
|
|
16360
|
+
} else if ('delete' === previous.kind) previous.left.push(left);
|
|
16361
|
+
else previous.right.push(right);
|
|
16362
|
+
return;
|
|
16363
|
+
}
|
|
16364
|
+
if ('equal' === kind) changes.push({
|
|
16365
|
+
kind,
|
|
16366
|
+
left: [
|
|
16367
|
+
left
|
|
16368
|
+
],
|
|
16369
|
+
right: [
|
|
16370
|
+
right
|
|
16371
|
+
]
|
|
16372
|
+
});
|
|
16373
|
+
else if ('delete' === kind) changes.push({
|
|
16374
|
+
kind,
|
|
16375
|
+
left: [
|
|
16376
|
+
left
|
|
16377
|
+
],
|
|
16378
|
+
right: []
|
|
16379
|
+
});
|
|
16380
|
+
else changes.push({
|
|
16381
|
+
kind,
|
|
16382
|
+
left: [],
|
|
16383
|
+
right: [
|
|
16384
|
+
right
|
|
16385
|
+
]
|
|
16386
|
+
});
|
|
16387
|
+
}
|
|
16388
|
+
function approximatelyEqual(left, right) {
|
|
16389
|
+
return Math.abs(left - right) <= ALIGNMENT_EPSILON;
|
|
16390
|
+
}
|
|
16391
|
+
function stableJson(value) {
|
|
16392
|
+
return JSON.stringify(sortJsonValue(value));
|
|
16393
|
+
}
|
|
16394
|
+
function stableHash(value) {
|
|
16395
|
+
let hash = 0x811c9dc5;
|
|
16396
|
+
for(let index = 0; index < value.length; index += 1){
|
|
16397
|
+
hash ^= value.charCodeAt(index);
|
|
16398
|
+
hash = Math.imul(hash, 0x01000193);
|
|
16399
|
+
}
|
|
16400
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
16401
|
+
}
|
|
16402
|
+
function wordParagraphId(value) {
|
|
16403
|
+
const number = 0x7fffffff & Number.parseInt(stableHash(value), 16) || 1;
|
|
16404
|
+
return number.toString(16).toUpperCase().padStart(8, '0');
|
|
16405
|
+
}
|
|
16406
|
+
function normalizedDate(value) {
|
|
16407
|
+
const timestamp = Date.parse(value);
|
|
16408
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : '1970-01-01T00:00:00.000Z';
|
|
16409
|
+
}
|
|
16410
|
+
function boundedMetadata(value, maximum) {
|
|
16411
|
+
return value.trim().slice(0, maximum);
|
|
16412
|
+
}
|
|
16413
|
+
function sortJsonValue(value) {
|
|
16414
|
+
if (Array.isArray(value)) return value.map(sortJsonValue);
|
|
16415
|
+
if (!value || 'object' != typeof value) return value;
|
|
16416
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([name, child])=>[
|
|
16417
|
+
name,
|
|
16418
|
+
sortJsonValue(child)
|
|
16419
|
+
]));
|
|
16420
|
+
}
|
|
16421
|
+
const MAX_COMPARISON_BLOCKS = 1024;
|
|
16422
|
+
const MAX_COMPARISON_TEXT = 1000000;
|
|
16423
|
+
const MAX_BLOCK_ALIGNMENT_CELLS = 1100000;
|
|
16424
|
+
const MAX_INLINE_DIFF_CELLS = 1100000;
|
|
16425
|
+
const BLOCK_CHANGE_ATTRIBUTE_NAMES = [
|
|
16426
|
+
'blockChangeKind',
|
|
16427
|
+
'blockChangeId',
|
|
16428
|
+
'blockChangeActorId',
|
|
16429
|
+
'blockChangeAuthor',
|
|
16430
|
+
'blockChangeDate'
|
|
16431
|
+
];
|
|
16432
|
+
const PARAGRAPH_IDENTITY_ATTRIBUTE_NAMES = [
|
|
16433
|
+
'paragraphId',
|
|
16434
|
+
'textId'
|
|
16435
|
+
];
|
|
16436
|
+
const REVIEW_ONLY_MARK_NAMES = new Set([
|
|
16437
|
+
'documentChange',
|
|
16438
|
+
'documentComment'
|
|
16439
|
+
]);
|
|
16440
|
+
function applyDocumentComparison(editor, revisedContent, options) {
|
|
16441
|
+
const revisedDocument = parseComparisonDocument(editor, revisedContent);
|
|
16442
|
+
if (!revisedDocument) return unsupportedResult({
|
|
16443
|
+
code: 'invalid-revised-content',
|
|
16444
|
+
message: 'The selected file cannot be represented by the Writer schema.'
|
|
16445
|
+
});
|
|
16446
|
+
const plan = 'combine' === options.mode ? planDocumentCombine(editor.state.doc, revisedDocument) : planDocumentCompare(editor.state.doc, revisedDocument, options);
|
|
16447
|
+
if ('applied' !== plan.status || !plan.document) return plan;
|
|
16448
|
+
const transaction = editor.state.tr;
|
|
16449
|
+
closeHistory(transaction);
|
|
16450
|
+
transaction.replaceWith(0, editor.state.doc.content.size, plan.document.content).setMeta('documentComparison', {
|
|
16451
|
+
mode: options.mode,
|
|
16452
|
+
sourceName: boundedMetadata(options.sourceName, 512)
|
|
16453
|
+
});
|
|
16454
|
+
if (!transaction.docChanged) return {
|
|
16455
|
+
status: 'unchanged',
|
|
16456
|
+
summary: plan.summary,
|
|
16457
|
+
diagnostics: plan.diagnostics
|
|
16458
|
+
};
|
|
16459
|
+
editor.view.dispatch(transaction);
|
|
16460
|
+
return {
|
|
16461
|
+
status: 'applied',
|
|
16462
|
+
summary: plan.summary,
|
|
16463
|
+
diagnostics: plan.diagnostics
|
|
16464
|
+
};
|
|
16465
|
+
}
|
|
16466
|
+
function planDocumentCompare(current, revised, options) {
|
|
16467
|
+
if (collectDocumentChanges(current).length) return unsupportedResult({
|
|
16468
|
+
code: 'current-revisions-present',
|
|
16469
|
+
message: 'Resolve current revisions before comparing another version.'
|
|
16470
|
+
});
|
|
16471
|
+
if (collectDocumentChanges(revised).length) return unsupportedResult({
|
|
16472
|
+
code: 'revised-revisions-present',
|
|
16473
|
+
message: 'Use Combine for a reviewed copy that already contains revisions.'
|
|
16474
|
+
});
|
|
16475
|
+
const currentSections = documentSections(current);
|
|
16476
|
+
const revisedSections = documentSections(revised);
|
|
16477
|
+
if (!currentSections || !revisedSections || currentSections.length !== revisedSections.length) return unsupportedResult({
|
|
16478
|
+
code: 'section-layout-mismatch',
|
|
16479
|
+
message: 'Comparison requires the same bounded section structure.'
|
|
16480
|
+
});
|
|
16481
|
+
const limits = comparisonLimits(currentSections, revisedSections);
|
|
16482
|
+
if (limits) return unsupportedResult(limits);
|
|
16483
|
+
const diagnostics = [];
|
|
16484
|
+
const factory = comparisonIdentityFactory(current, revised, options);
|
|
16485
|
+
const comparedSections = [];
|
|
16486
|
+
for(let sectionIndex = 0; sectionIndex < currentSections.length; sectionIndex += 1){
|
|
16487
|
+
const currentSection = currentSections[sectionIndex];
|
|
16488
|
+
const revisedSection = revisedSections[sectionIndex];
|
|
16489
|
+
if (sectionLayoutSignature(currentSection) !== sectionLayoutSignature(revisedSection)) {
|
|
16490
|
+
diagnostics.push({
|
|
16491
|
+
code: 'section-layout-mismatch',
|
|
16492
|
+
message: 'Page and section layout differences are not approximated as text revisions.',
|
|
16493
|
+
section: sectionIndex
|
|
16494
|
+
});
|
|
16495
|
+
continue;
|
|
16496
|
+
}
|
|
16497
|
+
const blocks = compareSectionBlocks(currentSection, revisedSection, sectionIndex, factory, diagnostics);
|
|
16498
|
+
if (blocks) comparedSections.push(currentSection.type.create(currentSection.attrs, model_Fragment.fromArray(blocks)));
|
|
16499
|
+
}
|
|
16500
|
+
if (diagnostics.length) return {
|
|
16501
|
+
status: 'unsupported',
|
|
16502
|
+
summary: factory.summary,
|
|
16503
|
+
diagnostics
|
|
16504
|
+
};
|
|
16505
|
+
const document1 = current.type.create(current.attrs, comparedSections);
|
|
16506
|
+
if (document1.eq(current)) return {
|
|
16507
|
+
status: 'unchanged',
|
|
16508
|
+
summary: emptyComparisonSummary(),
|
|
16509
|
+
diagnostics: []
|
|
16510
|
+
};
|
|
16511
|
+
return {
|
|
16512
|
+
status: 'applied',
|
|
16513
|
+
document: document1,
|
|
16514
|
+
summary: factory.summary,
|
|
16515
|
+
diagnostics: []
|
|
16516
|
+
};
|
|
16517
|
+
}
|
|
16518
|
+
function planDocumentCombine(current, reviewed) {
|
|
16519
|
+
if (collectDocumentChanges(current).length) return unsupportedResult({
|
|
16520
|
+
code: 'current-revisions-present',
|
|
16521
|
+
message: 'Resolve current revisions before combining another reviewed copy.'
|
|
16522
|
+
});
|
|
16523
|
+
const reviewedChanges = collectDocumentChanges(reviewed);
|
|
16524
|
+
if (!reviewedChanges.length) return unsupportedResult({
|
|
16525
|
+
code: 'combine-without-revisions',
|
|
16526
|
+
message: 'The selected reviewed copy contains no revisions to combine.'
|
|
16527
|
+
});
|
|
16528
|
+
if (hasStructuralBlockRevisions(reviewed)) return unsupportedResult({
|
|
16529
|
+
code: 'combine-structural-revisions',
|
|
16530
|
+
message: 'This reviewed copy changes the paragraph tree; combine supports inline and formatting revisions.'
|
|
16531
|
+
});
|
|
16532
|
+
const baseline = resolveAllDocumentChanges(reviewed, 'reject');
|
|
16533
|
+
if (!baseline) return unsupportedResult({
|
|
16534
|
+
code: 'combine-resolution-invalid',
|
|
16535
|
+
message: 'The reviewed copy contains a malformed formatting revision.'
|
|
16536
|
+
});
|
|
16537
|
+
if (!simpleDocumentShape(current) || !simpleDocumentShape(baseline)) return unsupportedResult({
|
|
16538
|
+
code: 'changed-complex-structure',
|
|
16539
|
+
message: 'Combine currently accepts text-only paragraph and heading review copies.'
|
|
16540
|
+
});
|
|
16541
|
+
if (comparisonSemanticSignature(current) !== comparisonSemanticSignature(baseline)) return unsupportedResult({
|
|
16542
|
+
code: 'combine-baseline-mismatch',
|
|
16543
|
+
message: 'Rejecting the imported revisions does not reproduce the current document.'
|
|
16544
|
+
});
|
|
16545
|
+
const document1 = transferCombineIdentities(current, reviewed);
|
|
16546
|
+
if (!document1) return unsupportedResult({
|
|
16547
|
+
code: 'combine-structural-revisions',
|
|
16548
|
+
message: 'This reviewed copy changes the paragraph tree; combine supports inline and formatting revisions.'
|
|
16549
|
+
});
|
|
16550
|
+
return {
|
|
16551
|
+
status: 'applied',
|
|
16552
|
+
document: document1,
|
|
16553
|
+
summary: summarizeChanges(reviewedChanges),
|
|
16554
|
+
diagnostics: []
|
|
16555
|
+
};
|
|
16556
|
+
}
|
|
16557
|
+
function compareSectionBlocks(currentSection, revisedSection, sectionIndex, factory, diagnostics) {
|
|
16558
|
+
const currentBlocks = childNodes(currentSection);
|
|
16559
|
+
const revisedBlocks = childNodes(revisedSection);
|
|
16560
|
+
const alignment = boundedDocumentSequenceAlignment(currentBlocks, revisedBlocks, blockSubstitutionCost, MAX_BLOCK_ALIGNMENT_CELLS);
|
|
16561
|
+
if (!alignment) {
|
|
16562
|
+
diagnostics.push({
|
|
16563
|
+
code: 'comparison-limit-exceeded',
|
|
16564
|
+
message: 'The block-alignment matrix exceeds the bounded comparison limit.',
|
|
16565
|
+
section: sectionIndex
|
|
16566
|
+
});
|
|
16567
|
+
return null;
|
|
16568
|
+
}
|
|
16569
|
+
const result = [];
|
|
16570
|
+
for (const [blockIndex, step] of alignment.entries()){
|
|
16571
|
+
if ('equal' === step.kind) {
|
|
16572
|
+
result.push(step.left);
|
|
16573
|
+
continue;
|
|
16574
|
+
}
|
|
16575
|
+
if ('delete' === step.kind) {
|
|
16576
|
+
const deleted = structuralChangeBlock(step.left, 'deletion', factory, `section-${sectionIndex}-delete-${blockIndex}`);
|
|
16577
|
+
if (deleted) result.push(deleted);
|
|
16578
|
+
else diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
|
|
16579
|
+
continue;
|
|
16580
|
+
}
|
|
16581
|
+
if ('insert' === step.kind) {
|
|
16582
|
+
const inserted = structuralChangeBlock(step.right, 'insertion', factory, `section-${sectionIndex}-insert-${blockIndex}`);
|
|
16583
|
+
if (inserted) result.push(inserted);
|
|
16584
|
+
else diagnostics.push(structuralBlockDiagnostic(step.right, sectionIndex, blockIndex));
|
|
16585
|
+
continue;
|
|
16586
|
+
}
|
|
16587
|
+
const compared = comparePairedBlocks(step.left, step.right, factory);
|
|
16588
|
+
if (compared) {
|
|
16589
|
+
result.push(compared);
|
|
16590
|
+
continue;
|
|
16591
|
+
}
|
|
16592
|
+
const deleted = structuralChangeBlock(step.left, 'deletion', factory, `section-${sectionIndex}-replace-delete-${blockIndex}`);
|
|
16593
|
+
const inserted = structuralChangeBlock(step.right, 'insertion', factory, `section-${sectionIndex}-replace-insert-${blockIndex}`);
|
|
16594
|
+
if (deleted && inserted) result.push(deleted, inserted);
|
|
16595
|
+
else diagnostics.push(structuralBlockDiagnostic(step.left, sectionIndex, blockIndex));
|
|
16596
|
+
}
|
|
16597
|
+
return result;
|
|
16598
|
+
}
|
|
16599
|
+
function comparePairedBlocks(current, revised, factory) {
|
|
16600
|
+
if (!isSimpleTextBlock(current) || !isSimpleTextBlock(revised)) return null;
|
|
16601
|
+
if (current.type !== revised.type || blockStructuralSignature(current) !== blockStructuralSignature(revised)) return null;
|
|
16602
|
+
const inline = compareInlineContent(current, revised, factory);
|
|
16603
|
+
if (!inline) return null;
|
|
16604
|
+
const before = serializeDocumentParagraphFormatting(current.attrs);
|
|
16605
|
+
const after = serializeDocumentParagraphFormatting(revised.attrs);
|
|
16606
|
+
let attributes = {
|
|
16607
|
+
...current.attrs,
|
|
16608
|
+
...paragraphFormattingAttributes(revised.attrs)
|
|
16609
|
+
};
|
|
16610
|
+
if (before !== after) {
|
|
16611
|
+
const identity = factory.create('paragraph-formatting', before);
|
|
16612
|
+
attributes = {
|
|
16613
|
+
...attributes,
|
|
16614
|
+
paragraphChangeKind: 'paragraph-formatting',
|
|
16615
|
+
paragraphChangeId: identity.id,
|
|
16616
|
+
paragraphChangeActorId: identity.actorId ?? '',
|
|
16617
|
+
paragraphChangeAuthor: identity.author,
|
|
16618
|
+
paragraphChangeDate: identity.date,
|
|
16619
|
+
paragraphChangeBefore: before
|
|
16620
|
+
};
|
|
16621
|
+
}
|
|
16622
|
+
return current.type.create(attributes, inline);
|
|
16623
|
+
}
|
|
16624
|
+
function compareInlineContent(current, revised, factory) {
|
|
16625
|
+
const currentUnits = inlineUnits(current);
|
|
16626
|
+
const revisedUnits = inlineUnits(revised);
|
|
16627
|
+
if (!currentUnits || !revisedUnits) return null;
|
|
16628
|
+
const diff = boundedDocumentSequenceDiff(currentUnits, revisedUnits, (left, right)=>left.text === right.text, MAX_INLINE_DIFF_CELLS);
|
|
16629
|
+
const changes = diff ?? [
|
|
16630
|
+
{
|
|
16631
|
+
kind: 'delete',
|
|
16632
|
+
left: currentUnits,
|
|
16633
|
+
right: []
|
|
16634
|
+
},
|
|
16635
|
+
{
|
|
16636
|
+
kind: 'insert',
|
|
16637
|
+
left: [],
|
|
16638
|
+
right: revisedUnits
|
|
16639
|
+
}
|
|
16640
|
+
];
|
|
16641
|
+
const nodes = [];
|
|
16642
|
+
for (const change of changes){
|
|
16643
|
+
if ('delete' === change.kind) {
|
|
16644
|
+
appendRevisionUnits(nodes, change.left, 'deletion', factory.create('deletion'), current.type.schema);
|
|
16645
|
+
continue;
|
|
16646
|
+
}
|
|
16647
|
+
if ('insert' === change.kind) {
|
|
16648
|
+
appendRevisionUnits(nodes, change.right, 'insertion', factory.create('insertion'), current.type.schema);
|
|
16649
|
+
continue;
|
|
16650
|
+
}
|
|
16651
|
+
let formattingIdentity = null;
|
|
16652
|
+
let formattingSignature = '';
|
|
16653
|
+
for(let index = 0; index < change.left.length; index += 1){
|
|
16654
|
+
const left = change.left[index];
|
|
16655
|
+
const right = change.right[index];
|
|
16656
|
+
if (marksEqual(left.marks, right.marks)) {
|
|
16657
|
+
nodes.push(current.type.schema.text(left.text, [
|
|
16658
|
+
...left.marks
|
|
16659
|
+
]));
|
|
16660
|
+
formattingIdentity = null;
|
|
16661
|
+
formattingSignature = '';
|
|
16662
|
+
continue;
|
|
16663
|
+
}
|
|
16664
|
+
if (nonCharacterMarksEqual(left.marks, right.marks)) {
|
|
16665
|
+
const before = serializeDocumentCharacterFormatting(left.marks);
|
|
16666
|
+
const after = serializeDocumentCharacterFormatting(right.marks);
|
|
16667
|
+
const signature = `${before}\u0000${after}`;
|
|
16668
|
+
if (!formattingIdentity || formattingSignature !== signature) {
|
|
16669
|
+
formattingIdentity = factory.create('formatting', before);
|
|
16670
|
+
formattingSignature = signature;
|
|
16671
|
+
}
|
|
16672
|
+
nodes.push(current.type.schema.text(right.text, [
|
|
16673
|
+
...withoutReviewMarks(right.marks),
|
|
16674
|
+
comparisonMark(current, 'formatting', formattingIdentity, before)
|
|
16675
|
+
]));
|
|
16676
|
+
continue;
|
|
16677
|
+
}
|
|
16678
|
+
const deletion = factory.create('deletion');
|
|
16679
|
+
const insertion = factory.create('insertion');
|
|
16680
|
+
appendRevisionUnits(nodes, [
|
|
16681
|
+
left
|
|
16682
|
+
], 'deletion', deletion, current.type.schema);
|
|
16683
|
+
appendRevisionUnits(nodes, [
|
|
16684
|
+
right
|
|
16685
|
+
], 'insertion', insertion, current.type.schema);
|
|
16686
|
+
formattingIdentity = null;
|
|
16687
|
+
formattingSignature = '';
|
|
16688
|
+
}
|
|
16689
|
+
}
|
|
16690
|
+
return model_Fragment.fromArray(nodes);
|
|
16691
|
+
}
|
|
16692
|
+
function structuralChangeBlock(block, kind, factory, identityChannel) {
|
|
16693
|
+
if (!isSimpleTextBlock(block) || !block.textContent) return null;
|
|
16694
|
+
const identity = factory.create(kind);
|
|
16695
|
+
const paragraphIdentity = 'insertion' === kind ? factory.paragraphIdentity(identityChannel) : {
|
|
16696
|
+
paragraphId: block.attrs.paragraphId,
|
|
16697
|
+
textId: block.attrs.textId
|
|
16698
|
+
};
|
|
16699
|
+
const nodes = [];
|
|
16700
|
+
for (const unit of inlineUnits(block) ?? [])appendRevisionUnits(nodes, [
|
|
16701
|
+
unit
|
|
16702
|
+
], kind, identity, block.type.schema);
|
|
16703
|
+
return block.type.create({
|
|
16704
|
+
...block.attrs,
|
|
16705
|
+
...paragraphIdentity,
|
|
16706
|
+
blockChangeKind: kind,
|
|
16707
|
+
blockChangeId: identity.id,
|
|
16708
|
+
blockChangeActorId: identity.actorId ?? '',
|
|
16709
|
+
blockChangeAuthor: identity.author,
|
|
16710
|
+
blockChangeDate: identity.date
|
|
16711
|
+
}, model_Fragment.fromArray(nodes));
|
|
16712
|
+
}
|
|
16713
|
+
function appendRevisionUnits(target, units, kind, identity, schema) {
|
|
16714
|
+
for (const unit of units)target.push(schema.text(unit.text, [
|
|
16715
|
+
...withoutReviewMarks(unit.marks),
|
|
16716
|
+
schema.marks.documentChange.create({
|
|
16717
|
+
kind,
|
|
16718
|
+
id: identity.id,
|
|
16719
|
+
actorId: identity.actorId ?? '',
|
|
16720
|
+
author: identity.author,
|
|
16721
|
+
date: identity.date,
|
|
16722
|
+
before: ''
|
|
16723
|
+
})
|
|
16724
|
+
]));
|
|
16725
|
+
}
|
|
16726
|
+
function comparisonMark(node, kind, identity, before) {
|
|
16727
|
+
return node.type.schema.marks.documentChange.create({
|
|
16728
|
+
kind,
|
|
16729
|
+
id: identity.id,
|
|
16730
|
+
actorId: identity.actorId ?? '',
|
|
16731
|
+
author: identity.author,
|
|
16732
|
+
date: identity.date,
|
|
16733
|
+
before
|
|
16734
|
+
});
|
|
16735
|
+
}
|
|
16736
|
+
function parseComparisonDocument(editor, content) {
|
|
16737
|
+
try {
|
|
16738
|
+
const parsed = createNodeFromContent(content, editor.schema, {
|
|
16739
|
+
errorOnInvalidContent: true,
|
|
16740
|
+
slice: false
|
|
16741
|
+
});
|
|
16742
|
+
return parsed instanceof model_Node ? parsed : null;
|
|
16743
|
+
} catch {
|
|
16744
|
+
return null;
|
|
16745
|
+
}
|
|
16746
|
+
}
|
|
16747
|
+
function documentSections(document1) {
|
|
16748
|
+
const sections = childNodes(document1);
|
|
16749
|
+
return sections.every((node)=>'documentSection' === node.type.name) ? sections : null;
|
|
16750
|
+
}
|
|
16751
|
+
function comparisonLimits(current, revised) {
|
|
16752
|
+
const currentBlocks = current.reduce((sum, section)=>sum + section.childCount, 0);
|
|
16753
|
+
const revisedBlocks = revised.reduce((sum, section)=>sum + section.childCount, 0);
|
|
16754
|
+
const textLength = current.reduce((sum, section)=>sum + section.textContent.length, 0) + revised.reduce((sum, section)=>sum + section.textContent.length, 0);
|
|
16755
|
+
return currentBlocks > MAX_COMPARISON_BLOCKS || revisedBlocks > MAX_COMPARISON_BLOCKS || textLength > MAX_COMPARISON_TEXT ? {
|
|
16756
|
+
code: 'comparison-limit-exceeded',
|
|
16757
|
+
message: `Comparison accepts at most ${MAX_COMPARISON_BLOCKS} blocks per version and ${MAX_COMPARISON_TEXT} combined text characters.`
|
|
16758
|
+
} : null;
|
|
16759
|
+
}
|
|
16760
|
+
function blockSubstitutionCost(current, revised) {
|
|
16761
|
+
if (comparisonSemanticSignature(current) === comparisonSemanticSignature(revised)) return 0;
|
|
16762
|
+
if (!isSimpleTextBlock(current) || !isSimpleTextBlock(revised)) return 2.1;
|
|
16763
|
+
if (current.type !== revised.type) return 2.1;
|
|
16764
|
+
if (!current.textContent && !revised.textContent) return 0.8;
|
|
16765
|
+
return documentTextSimilarity(current.textContent, revised.textContent) >= 0.18 ? 0.8 : 2.1;
|
|
16766
|
+
}
|
|
16767
|
+
function documentTextSimilarity(left, right) {
|
|
16768
|
+
if (left === right) return 1;
|
|
16769
|
+
if (!left || !right) return 0;
|
|
16770
|
+
const maximum = Math.max(left.length, right.length);
|
|
16771
|
+
let prefix = 0;
|
|
16772
|
+
while(prefix < Math.min(left.length, right.length) && left[prefix] === right[prefix])prefix += 1;
|
|
16773
|
+
let suffix = 0;
|
|
16774
|
+
while(suffix < Math.min(left.length, right.length) - prefix && left[left.length - suffix - 1] === right[right.length - suffix - 1])suffix += 1;
|
|
16775
|
+
const boundaryScore = (prefix + suffix) / maximum;
|
|
16776
|
+
const leftWords = comparisonWords(left);
|
|
16777
|
+
const rightWords = comparisonWords(right);
|
|
16778
|
+
let shared = 0;
|
|
16779
|
+
for (const word of leftWords)if (rightWords.has(word)) shared += 1;
|
|
16780
|
+
const union = new Set([
|
|
16781
|
+
...leftWords,
|
|
16782
|
+
...rightWords
|
|
16783
|
+
]).size;
|
|
16784
|
+
return Math.max(boundaryScore, union ? shared / union : 0);
|
|
16785
|
+
}
|
|
16786
|
+
function comparisonWords(value) {
|
|
16787
|
+
return new Set((value.toLocaleLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).slice(0, 256));
|
|
16788
|
+
}
|
|
16789
|
+
function isSimpleTextBlock(node) {
|
|
16790
|
+
if ('paragraph' !== node.type.name && 'heading' !== node.type.name) return false;
|
|
16791
|
+
let simple = true;
|
|
16792
|
+
node.forEach((child)=>{
|
|
16793
|
+
if (!child.isText || child.marks.some((mark)=>REVIEW_ONLY_MARK_NAMES.has(mark.type.name))) simple = false;
|
|
16794
|
+
});
|
|
16795
|
+
return simple;
|
|
16796
|
+
}
|
|
16797
|
+
function simpleDocumentShape(document1) {
|
|
16798
|
+
const sections = documentSections(document1);
|
|
16799
|
+
return Boolean(sections?.every((section)=>childNodes(section).every((block)=>isSimpleTextBlock(block))));
|
|
16800
|
+
}
|
|
16801
|
+
function hasStructuralBlockRevisions(document1) {
|
|
16802
|
+
let found = false;
|
|
16803
|
+
document1.descendants((node)=>{
|
|
16804
|
+
if (('paragraph' === node.type.name || 'heading' === node.type.name) && ('insertion' === node.attrs.blockChangeKind || 'deletion' === node.attrs.blockChangeKind)) found = true;
|
|
16805
|
+
return !found;
|
|
16806
|
+
});
|
|
16807
|
+
return found;
|
|
16808
|
+
}
|
|
16809
|
+
function inlineUnits(node) {
|
|
16810
|
+
if (!isSimpleTextBlock(node)) return null;
|
|
16811
|
+
const units = [];
|
|
16812
|
+
node.forEach((child)=>{
|
|
16813
|
+
const text = child.text ?? '';
|
|
16814
|
+
for (const part of text.match(/\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) ?? [])units.push({
|
|
16815
|
+
text: part,
|
|
16816
|
+
marks: child.marks
|
|
16817
|
+
});
|
|
16818
|
+
});
|
|
16819
|
+
return units;
|
|
16820
|
+
}
|
|
16821
|
+
function marksEqual(left, right) {
|
|
16822
|
+
const normalizedLeft = withoutReviewMarks(left);
|
|
16823
|
+
const normalizedRight = withoutReviewMarks(right);
|
|
16824
|
+
return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((mark, index)=>mark.eq(normalizedRight[index]));
|
|
16825
|
+
}
|
|
16826
|
+
function nonCharacterMarksEqual(left, right) {
|
|
16827
|
+
const normalizedLeft = withoutReviewMarks(left).filter((mark)=>!isDocumentCharacterFormatMark(mark.type.name));
|
|
16828
|
+
const normalizedRight = withoutReviewMarks(right).filter((mark)=>!isDocumentCharacterFormatMark(mark.type.name));
|
|
16829
|
+
return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((mark, index)=>mark.eq(normalizedRight[index]));
|
|
16830
|
+
}
|
|
16831
|
+
function withoutReviewMarks(marks) {
|
|
16832
|
+
return marks.filter((mark)=>!REVIEW_ONLY_MARK_NAMES.has(mark.type.name));
|
|
16833
|
+
}
|
|
16834
|
+
function blockStructuralSignature(node) {
|
|
16835
|
+
const ignored = new Set([
|
|
16836
|
+
...DOCUMENT_PARAGRAPH_FORMAT_ATTRIBUTES,
|
|
16837
|
+
...DOCUMENT_PARAGRAPH_CHANGE_ATTRIBUTES,
|
|
16838
|
+
...BLOCK_CHANGE_ATTRIBUTE_NAMES,
|
|
16839
|
+
...PARAGRAPH_IDENTITY_ATTRIBUTE_NAMES
|
|
16840
|
+
]);
|
|
16841
|
+
return stableJson({
|
|
16842
|
+
attrs: filteredAttributes(node.attrs, ignored),
|
|
16843
|
+
type: node.type.name
|
|
16844
|
+
});
|
|
16845
|
+
}
|
|
16846
|
+
function paragraphFormattingAttributes(attributes) {
|
|
16847
|
+
return Object.fromEntries(DOCUMENT_PARAGRAPH_FORMAT_ATTRIBUTES.map((name)=>[
|
|
16848
|
+
name,
|
|
16849
|
+
attributes[name]
|
|
16850
|
+
]));
|
|
16851
|
+
}
|
|
16852
|
+
function sectionLayoutSignature(section) {
|
|
16853
|
+
return stableJson(filteredAttributes(section.attrs, new Set([
|
|
16854
|
+
'id'
|
|
16855
|
+
])));
|
|
16856
|
+
}
|
|
16857
|
+
function comparisonSemanticSignature(node) {
|
|
16858
|
+
return stableJson(comparisonSemanticValue(node));
|
|
16859
|
+
}
|
|
16860
|
+
function comparisonSemanticValue(node) {
|
|
16861
|
+
const ignored = new Set([
|
|
16862
|
+
...DOCUMENT_PARAGRAPH_CHANGE_ATTRIBUTES,
|
|
16863
|
+
...BLOCK_CHANGE_ATTRIBUTE_NAMES,
|
|
16864
|
+
...PARAGRAPH_IDENTITY_ATTRIBUTE_NAMES,
|
|
16865
|
+
...'documentSection' === node.type.name ? [
|
|
16866
|
+
'id'
|
|
16867
|
+
] : []
|
|
16868
|
+
]);
|
|
16869
|
+
return {
|
|
16870
|
+
type: node.type.name,
|
|
16871
|
+
...node.isText ? {
|
|
16872
|
+
text: node.text
|
|
16873
|
+
} : {},
|
|
16874
|
+
...Object.keys(node.attrs).length ? {
|
|
16875
|
+
attrs: filteredAttributes(node.attrs, ignored)
|
|
16876
|
+
} : {},
|
|
16877
|
+
...node.marks.length ? {
|
|
16878
|
+
marks: withoutReviewMarks(node.marks).map((mark)=>mark.toJSON())
|
|
16879
|
+
} : {},
|
|
16880
|
+
...node.childCount ? {
|
|
16881
|
+
content: childNodes(node).map(comparisonSemanticValue)
|
|
16882
|
+
} : {}
|
|
16883
|
+
};
|
|
16884
|
+
}
|
|
16885
|
+
function transferCombineIdentities(current, reviewed) {
|
|
16886
|
+
if (current.type !== reviewed.type) return null;
|
|
16887
|
+
if ('paragraph' === reviewed.type.name || 'heading' === reviewed.type.name) return reviewed.type.create({
|
|
16888
|
+
...reviewed.attrs,
|
|
16889
|
+
paragraphId: current.attrs.paragraphId,
|
|
16890
|
+
textId: current.attrs.textId
|
|
16891
|
+
}, reviewed.content, reviewed.marks);
|
|
16892
|
+
if (current.childCount !== reviewed.childCount) return null;
|
|
16893
|
+
const content = [];
|
|
16894
|
+
for(let index = 0; index < current.childCount; index += 1){
|
|
16895
|
+
const currentChild = current.child(index);
|
|
16896
|
+
const reviewedChild = reviewed.child(index);
|
|
16897
|
+
const transferred = transferCombineIdentities(currentChild, reviewedChild);
|
|
16898
|
+
if (!transferred) return null;
|
|
16899
|
+
content.push(transferred);
|
|
16900
|
+
}
|
|
16901
|
+
const attributes = {
|
|
16902
|
+
...reviewed.attrs
|
|
16903
|
+
};
|
|
16904
|
+
if ('documentSection' === reviewed.type.name) attributes.id = current.attrs.id;
|
|
16905
|
+
return reviewed.isText ? reviewed : reviewed.type.create(attributes, model_Fragment.fromArray(content), reviewed.marks);
|
|
16906
|
+
}
|
|
16907
|
+
function comparisonIdentityFactory(current, revised, options) {
|
|
16908
|
+
const seed = stableHash(`${options.mode}\u0000${comparisonSemanticSignature(current)}\u0000${comparisonSemanticSignature(revised)}`);
|
|
16909
|
+
const author = boundedMetadata(options.author, 256) || 'A3S Work user';
|
|
16910
|
+
const date = normalizedDate(options.date);
|
|
16911
|
+
const summary = emptyComparisonSummary();
|
|
16912
|
+
let sequence = 0;
|
|
16913
|
+
return {
|
|
16914
|
+
summary,
|
|
16915
|
+
create (kind) {
|
|
16916
|
+
sequence += 1;
|
|
16917
|
+
if ('insertion' === kind) summary.insertions += 1;
|
|
16918
|
+
else if ('deletion' === kind) summary.deletions += 1;
|
|
16919
|
+
else if ('formatting' === kind) summary.formatting += 1;
|
|
16920
|
+
else summary.paragraphFormatting += 1;
|
|
16921
|
+
return {
|
|
16922
|
+
id: `compare-${seed}-${kind}-${sequence.toString(36)}`,
|
|
16923
|
+
author,
|
|
16924
|
+
date
|
|
16925
|
+
};
|
|
16926
|
+
},
|
|
16927
|
+
paragraphIdentity (channel) {
|
|
16928
|
+
return {
|
|
16929
|
+
paragraphId: wordParagraphId(`${seed}:${channel}:paragraph`),
|
|
16930
|
+
textId: wordParagraphId(`${seed}:${channel}:text`)
|
|
16931
|
+
};
|
|
16932
|
+
}
|
|
16933
|
+
};
|
|
16934
|
+
}
|
|
16935
|
+
function summarizeChanges(changes) {
|
|
16936
|
+
const summary = emptyComparisonSummary();
|
|
16937
|
+
for (const change of changes)if ('insertion' === change.kind) summary.insertions += 1;
|
|
16938
|
+
else if ('deletion' === change.kind) summary.deletions += 1;
|
|
16939
|
+
else if ('formatting' === change.kind) summary.formatting += 1;
|
|
16940
|
+
else summary.paragraphFormatting += 1;
|
|
16941
|
+
return summary;
|
|
16942
|
+
}
|
|
16943
|
+
function structuralBlockDiagnostic(block, section, blockIndex) {
|
|
16944
|
+
return 'paragraph' === block.type.name || 'heading' === block.type.name ? {
|
|
16945
|
+
code: 'empty-structural-change',
|
|
16946
|
+
message: 'An empty inserted or deleted paragraph cannot carry a reviewable text revision.',
|
|
16947
|
+
section,
|
|
16948
|
+
block: blockIndex
|
|
16949
|
+
} : {
|
|
16950
|
+
code: 'changed-complex-structure',
|
|
16951
|
+
message: `Changed ${block.type.name} content is not approximated as plain text.`,
|
|
16952
|
+
section,
|
|
16953
|
+
block: blockIndex
|
|
16954
|
+
};
|
|
16955
|
+
}
|
|
16956
|
+
function unsupportedResult(diagnostic) {
|
|
16957
|
+
return {
|
|
16958
|
+
status: 'unsupported',
|
|
16959
|
+
summary: emptyComparisonSummary(),
|
|
16960
|
+
diagnostics: [
|
|
16961
|
+
diagnostic
|
|
16962
|
+
]
|
|
16963
|
+
};
|
|
16964
|
+
}
|
|
16965
|
+
function emptyComparisonSummary() {
|
|
16966
|
+
return {
|
|
16967
|
+
deletions: 0,
|
|
16968
|
+
formatting: 0,
|
|
16969
|
+
insertions: 0,
|
|
16970
|
+
paragraphFormatting: 0
|
|
16971
|
+
};
|
|
16972
|
+
}
|
|
16973
|
+
function childNodes(node) {
|
|
16974
|
+
const children = [];
|
|
16975
|
+
node.forEach((child)=>{
|
|
16976
|
+
children.push(child);
|
|
16977
|
+
});
|
|
16978
|
+
return children;
|
|
16979
|
+
}
|
|
16980
|
+
function filteredAttributes(attributes, ignored) {
|
|
16981
|
+
return Object.fromEntries(Object.entries(attributes).filter(([name])=>!ignored.has(name)));
|
|
16982
|
+
}
|
|
16983
|
+
function DocumentCompareDialog({ initialMode, restoreFocusTarget, onApplied, onClose, onSubmit }) {
|
|
16984
|
+
const [mode, setMode] = useState(initialMode);
|
|
16985
|
+
const [file, setFile] = useState(null);
|
|
16986
|
+
const [author, setAuthor] = useState('审阅者');
|
|
16987
|
+
const [authorTouched, setAuthorTouched] = useState(false);
|
|
16988
|
+
const [busy, setBusy] = useState(false);
|
|
16989
|
+
const [result, setResult] = useState(null);
|
|
16990
|
+
const fileInputRef = useRef(null);
|
|
16991
|
+
const formId = useId();
|
|
16992
|
+
const authorId = useId();
|
|
16993
|
+
const submit = async (event)=>{
|
|
16994
|
+
event.preventDefault();
|
|
16995
|
+
if (!file || busy || 'compare' === mode && !author.trim()) return;
|
|
16996
|
+
setBusy(true);
|
|
16997
|
+
setResult(null);
|
|
16998
|
+
try {
|
|
16999
|
+
const next = await onSubmit({
|
|
17000
|
+
author: author.trim(),
|
|
17001
|
+
file,
|
|
17002
|
+
mode
|
|
17003
|
+
});
|
|
17004
|
+
setResult(next);
|
|
17005
|
+
if ('applied' === next.status) onApplied(next);
|
|
17006
|
+
} catch {
|
|
17007
|
+
setResult({
|
|
17008
|
+
status: 'unsupported',
|
|
17009
|
+
summary: {
|
|
17010
|
+
deletions: 0,
|
|
17011
|
+
formatting: 0,
|
|
17012
|
+
insertions: 0,
|
|
17013
|
+
paragraphFormatting: 0
|
|
17014
|
+
},
|
|
17015
|
+
diagnostics: [
|
|
17016
|
+
{
|
|
17017
|
+
code: 'invalid-revised-content',
|
|
17018
|
+
message: 'The selected file could not be imported.'
|
|
17019
|
+
}
|
|
17020
|
+
]
|
|
17021
|
+
});
|
|
17022
|
+
} finally{
|
|
17023
|
+
setBusy(false);
|
|
17024
|
+
}
|
|
17025
|
+
};
|
|
17026
|
+
const selectFile = (next)=>{
|
|
17027
|
+
setFile(next);
|
|
17028
|
+
setResult(null);
|
|
17029
|
+
if (!authorTouched) setAuthor(fileStem(next.name) || '审阅者');
|
|
17030
|
+
};
|
|
17031
|
+
return /*#__PURE__*/ jsx(Dialog, {
|
|
17032
|
+
title: "比较与合并文档",
|
|
17033
|
+
description: "把另一版本转换为可逐项接受或拒绝的 Writer 修订。",
|
|
17034
|
+
className: "work-document-compare-dialog",
|
|
17035
|
+
closeDisabled: busy,
|
|
17036
|
+
restoreFocusTarget: restoreFocusTarget,
|
|
17037
|
+
onClose: onClose,
|
|
17038
|
+
footer: /*#__PURE__*/ jsxs(Fragment, {
|
|
17039
|
+
children: [
|
|
17040
|
+
/*#__PURE__*/ jsx(button_Button, {
|
|
17041
|
+
tone: "quiet",
|
|
17042
|
+
disabled: busy,
|
|
17043
|
+
onClick: onClose,
|
|
17044
|
+
children: "取消"
|
|
17045
|
+
}),
|
|
17046
|
+
/*#__PURE__*/ jsx(button_Button, {
|
|
17047
|
+
tone: "primary",
|
|
17048
|
+
type: "submit",
|
|
17049
|
+
form: formId,
|
|
17050
|
+
disabled: !file || busy || 'compare' === mode && !author.trim(),
|
|
17051
|
+
children: busy ? '正在处理…' : 'compare' === mode ? '生成比较结果' : '合并修订'
|
|
17052
|
+
})
|
|
17053
|
+
]
|
|
17054
|
+
}),
|
|
17055
|
+
children: /*#__PURE__*/ jsxs("form", {
|
|
17056
|
+
id: formId,
|
|
17057
|
+
onSubmit: submit,
|
|
17058
|
+
children: [
|
|
17059
|
+
/*#__PURE__*/ jsxs("fieldset", {
|
|
17060
|
+
className: "work-document-compare-mode",
|
|
17061
|
+
children: [
|
|
17062
|
+
/*#__PURE__*/ jsx("legend", {
|
|
17063
|
+
children: "处理方式"
|
|
17064
|
+
}),
|
|
17065
|
+
/*#__PURE__*/ jsxs("label", {
|
|
17066
|
+
children: [
|
|
17067
|
+
/*#__PURE__*/ jsx("input", {
|
|
17068
|
+
type: "radio",
|
|
17069
|
+
name: "document-comparison-mode",
|
|
17070
|
+
value: "compare",
|
|
17071
|
+
checked: 'compare' === mode,
|
|
17072
|
+
onChange: ()=>{
|
|
17073
|
+
setMode('compare');
|
|
17074
|
+
setResult(null);
|
|
17075
|
+
}
|
|
17076
|
+
}),
|
|
17077
|
+
/*#__PURE__*/ jsxs("span", {
|
|
17078
|
+
children: [
|
|
17079
|
+
/*#__PURE__*/ jsx(GitCompareArrows, {
|
|
17080
|
+
size: 18,
|
|
17081
|
+
"aria-hidden": "true"
|
|
17082
|
+
}),
|
|
17083
|
+
/*#__PURE__*/ jsx("strong", {
|
|
17084
|
+
children: "比较"
|
|
17085
|
+
}),
|
|
17086
|
+
/*#__PURE__*/ jsx("small", {
|
|
17087
|
+
children: "当前文档作为原稿,导入文件作为修订稿。"
|
|
17088
|
+
})
|
|
17089
|
+
]
|
|
17090
|
+
})
|
|
17091
|
+
]
|
|
17092
|
+
}),
|
|
17093
|
+
/*#__PURE__*/ jsxs("label", {
|
|
17094
|
+
children: [
|
|
17095
|
+
/*#__PURE__*/ jsx("input", {
|
|
17096
|
+
type: "radio",
|
|
17097
|
+
name: "document-comparison-mode",
|
|
17098
|
+
value: "combine",
|
|
17099
|
+
checked: 'combine' === mode,
|
|
17100
|
+
onChange: ()=>{
|
|
17101
|
+
setMode('combine');
|
|
17102
|
+
setResult(null);
|
|
17103
|
+
}
|
|
17104
|
+
}),
|
|
17105
|
+
/*#__PURE__*/ jsxs("span", {
|
|
17106
|
+
children: [
|
|
17107
|
+
/*#__PURE__*/ jsx(FileCheck2, {
|
|
17108
|
+
size: 18,
|
|
17109
|
+
"aria-hidden": "true"
|
|
17110
|
+
}),
|
|
17111
|
+
/*#__PURE__*/ jsx("strong", {
|
|
17112
|
+
children: "合并"
|
|
17113
|
+
}),
|
|
17114
|
+
/*#__PURE__*/ jsx("small", {
|
|
17115
|
+
children: "导入带修订的审阅副本,并核验其原始基线。"
|
|
17116
|
+
})
|
|
17117
|
+
]
|
|
17118
|
+
})
|
|
17119
|
+
]
|
|
17120
|
+
})
|
|
17121
|
+
]
|
|
17122
|
+
}),
|
|
17123
|
+
/*#__PURE__*/ jsx(office_text_field_OfficeFileInput, {
|
|
17124
|
+
ref: fileInputRef,
|
|
17125
|
+
accept: ".docx,.html,.htm,.txt,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/html,text/plain",
|
|
17126
|
+
"aria-label": 'compare' === mode ? '选择修订版本文件' : '选择审阅副本文件',
|
|
17127
|
+
disabled: busy,
|
|
17128
|
+
onFileSelect: selectFile
|
|
17129
|
+
}),
|
|
17130
|
+
/*#__PURE__*/ jsxs("button", {
|
|
17131
|
+
type: "button",
|
|
17132
|
+
className: "work-document-compare-file",
|
|
17133
|
+
"data-autofocus": "true",
|
|
17134
|
+
disabled: busy,
|
|
17135
|
+
onClick: ()=>fileInputRef.current?.click(),
|
|
17136
|
+
children: [
|
|
17137
|
+
/*#__PURE__*/ jsx(FileUp, {
|
|
17138
|
+
size: 20,
|
|
17139
|
+
"aria-hidden": "true"
|
|
17140
|
+
}),
|
|
17141
|
+
/*#__PURE__*/ jsxs("span", {
|
|
17142
|
+
children: [
|
|
17143
|
+
/*#__PURE__*/ jsx("strong", {
|
|
17144
|
+
children: file?.name ?? ('compare' === mode ? '选择修订版本' : '选择带修订的审阅副本')
|
|
17145
|
+
}),
|
|
17146
|
+
/*#__PURE__*/ jsx("small", {
|
|
17147
|
+
children: file ? `${fileTypeLabel(file.name)} · ${formatFileSize(file.size)}` : '支持 DOCX、HTML 和 TXT;当前文档不会在预检失败时改变。'
|
|
17148
|
+
})
|
|
17149
|
+
]
|
|
17150
|
+
})
|
|
17151
|
+
]
|
|
17152
|
+
}),
|
|
17153
|
+
'compare' === mode && /*#__PURE__*/ jsxs("label", {
|
|
17154
|
+
className: "work-document-compare-author",
|
|
17155
|
+
htmlFor: authorId,
|
|
17156
|
+
children: [
|
|
17157
|
+
/*#__PURE__*/ jsx("span", {
|
|
17158
|
+
children: "修订者名称"
|
|
17159
|
+
}),
|
|
17160
|
+
/*#__PURE__*/ jsx(office_text_field_OfficeTextField, {
|
|
17161
|
+
id: authorId,
|
|
17162
|
+
"aria-label": "比较结果修订者名称",
|
|
17163
|
+
value: author,
|
|
17164
|
+
disabled: busy,
|
|
17165
|
+
maxLength: 256,
|
|
17166
|
+
onChange: (event)=>{
|
|
17167
|
+
setAuthorTouched(true);
|
|
17168
|
+
setAuthor(event.currentTarget.value);
|
|
17169
|
+
}
|
|
17170
|
+
}),
|
|
17171
|
+
/*#__PURE__*/ jsx("small", {
|
|
17172
|
+
children: "该名称会显示在生成的插入、删除与格式修订中。"
|
|
17173
|
+
})
|
|
17174
|
+
]
|
|
17175
|
+
}),
|
|
17176
|
+
/*#__PURE__*/ jsx(ComparisonBoundary, {
|
|
17177
|
+
mode: mode
|
|
17178
|
+
}),
|
|
17179
|
+
result?.status === 'unchanged' && /*#__PURE__*/ jsx("p", {
|
|
17180
|
+
className: "work-document-compare-status",
|
|
17181
|
+
role: "status",
|
|
17182
|
+
children: "两份文档在支持的比较范围内完全一致,没有生成修订。"
|
|
17183
|
+
}),
|
|
17184
|
+
result?.status === 'unsupported' && /*#__PURE__*/ jsxs("div", {
|
|
17185
|
+
className: "work-document-compare-errors",
|
|
17186
|
+
role: "alert",
|
|
17187
|
+
children: [
|
|
17188
|
+
/*#__PURE__*/ jsx("strong", {
|
|
17189
|
+
children: "无法安全处理这份文档"
|
|
17190
|
+
}),
|
|
17191
|
+
/*#__PURE__*/ jsx("ul", {
|
|
17192
|
+
children: result.diagnostics.map((diagnostic, index)=>/*#__PURE__*/ jsx("li", {
|
|
17193
|
+
children: comparisonDiagnosticText(diagnostic)
|
|
17194
|
+
}, `${diagnostic.code}-${index}`))
|
|
17195
|
+
})
|
|
17196
|
+
]
|
|
17197
|
+
})
|
|
17198
|
+
]
|
|
17199
|
+
})
|
|
17200
|
+
});
|
|
17201
|
+
}
|
|
17202
|
+
function ComparisonBoundary({ mode }) {
|
|
17203
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
17204
|
+
className: "work-document-compare-boundary",
|
|
17205
|
+
children: [
|
|
17206
|
+
/*#__PURE__*/ jsx("strong", {
|
|
17207
|
+
children: 'compare' === mode ? '确定性比较边界' : '安全合并边界'
|
|
17208
|
+
}),
|
|
17209
|
+
/*#__PURE__*/ jsx("p", {
|
|
17210
|
+
children: 'compare' === mode ? '支持同一分节布局中的段落、标题、文字和格式差异;复杂对象或节布局变化会明确停止。' : '审阅副本必须包含修订,且拒绝全部修订后与当前文档一致;现有修订须先处理。'
|
|
17211
|
+
})
|
|
17212
|
+
]
|
|
17213
|
+
});
|
|
17214
|
+
}
|
|
17215
|
+
function comparisonDiagnosticText(diagnostic) {
|
|
17216
|
+
const location = void 0 === diagnostic.section ? '' : `(第 ${diagnostic.section + 1} 节${void 0 === diagnostic.block ? '' : `,差异块 ${diagnostic.block + 1}`})`;
|
|
17217
|
+
const messages = {
|
|
17218
|
+
'changed-complex-structure': '检测到表格、图片、列表或其他复杂结构变化,未降级为纯文字。',
|
|
17219
|
+
'combine-baseline-mismatch': '审阅副本的原始基线与当前文档不一致。',
|
|
17220
|
+
'combine-resolution-invalid': '审阅副本包含损坏或无法拒绝的格式修订。',
|
|
17221
|
+
'combine-structural-revisions': '审阅副本改变了段落树;当前合并路径只接受行内和格式修订。',
|
|
17222
|
+
'combine-without-revisions': '审阅副本中没有可合并的修订。',
|
|
17223
|
+
'comparison-limit-exceeded': '文档超过本地有界比较限制。',
|
|
17224
|
+
'current-revisions-present': '当前文档仍有未处理修订,请先接受或拒绝。',
|
|
17225
|
+
'empty-structural-change': '空段落的结构变化无法承载可审阅文字修订。',
|
|
17226
|
+
'invalid-revised-content': '文件无法转换为当前 Writer 文档模型。',
|
|
17227
|
+
'revised-revisions-present': '修订稿已经包含修订,请改用“合并”。',
|
|
17228
|
+
'section-layout-mismatch': '分节数量或页面布局不一致。',
|
|
17229
|
+
'unsupported-inline-review-state': '导入内容包含无法安全迁移的批注或审阅状态。'
|
|
17230
|
+
};
|
|
17231
|
+
return `${messages[diagnostic.code]}${location}`;
|
|
17232
|
+
}
|
|
17233
|
+
function fileStem(name) {
|
|
17234
|
+
return name.replace(/\.[^.]+$/, '').trim();
|
|
17235
|
+
}
|
|
17236
|
+
function fileTypeLabel(name) {
|
|
17237
|
+
return name.split('.').at(-1)?.toLocaleUpperCase() || '文件';
|
|
17238
|
+
}
|
|
17239
|
+
function formatFileSize(bytes) {
|
|
17240
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
17241
|
+
if (bytes < 1048576) return `${Math.max(0.1, bytes / 1024).toFixed(1)} KB`;
|
|
17242
|
+
return `${Math.max(0.1, bytes / 1048576).toFixed(1)} MB`;
|
|
17243
|
+
}
|
|
17244
|
+
function useDocumentComparison({ editor, onApplied }) {
|
|
17245
|
+
const [dialog, setDialog] = useState(null);
|
|
17246
|
+
const editorRef = useRef(editor);
|
|
17247
|
+
editorRef.current = editor;
|
|
17248
|
+
const open = (mode)=>{
|
|
17249
|
+
const current = editorRef.current;
|
|
17250
|
+
if (!current || current.isDestroyed) return;
|
|
17251
|
+
const active = document.activeElement;
|
|
17252
|
+
setDialog({
|
|
17253
|
+
mode,
|
|
17254
|
+
restoreFocusTarget: active instanceof HTMLElement && active.isConnected ? active : current.view.dom
|
|
17255
|
+
});
|
|
17256
|
+
};
|
|
17257
|
+
const close = ()=>setDialog(null);
|
|
17258
|
+
const submit = async (request)=>{
|
|
17259
|
+
const current = editorRef.current;
|
|
17260
|
+
if (!current || current.isDestroyed) return invalidComparisonResult();
|
|
17261
|
+
const extension = comparisonFileExtension(request.file.name);
|
|
17262
|
+
if (!extension) return invalidComparisonResult();
|
|
17263
|
+
const artifact = await importWorkDocumentFile(request.file, extension);
|
|
17264
|
+
try {
|
|
17265
|
+
if ('document' !== artifact.content.type) return invalidComparisonResult();
|
|
17266
|
+
const content = artifact.content;
|
|
17267
|
+
const model = documentModelForContent(content);
|
|
17268
|
+
const fallbackHtml = model ? content.html : normalizeDocumentHtml(content);
|
|
17269
|
+
const input = resolveWorkDocumentEditorInput(content, fallbackHtml, model);
|
|
17270
|
+
return applyDocumentComparison(current, input.source, {
|
|
17271
|
+
author: request.author,
|
|
17272
|
+
date: new Date().toISOString(),
|
|
17273
|
+
mode: request.mode,
|
|
17274
|
+
sourceName: request.file.name
|
|
17275
|
+
});
|
|
17276
|
+
} finally{
|
|
17277
|
+
forgetWorkSourceBlob(artifact.id);
|
|
17278
|
+
}
|
|
17279
|
+
};
|
|
17280
|
+
const applied = (result)=>{
|
|
17281
|
+
const count = result.summary.insertions + result.summary.deletions + result.summary.formatting + result.summary.paragraphFormatting;
|
|
17282
|
+
close();
|
|
17283
|
+
onApplied();
|
|
17284
|
+
showToast(`已生成 ${count} 项可审阅修订`, 'success');
|
|
17285
|
+
};
|
|
17286
|
+
return {
|
|
17287
|
+
dialog: dialog ? /*#__PURE__*/ jsx(DocumentCompareDialog, {
|
|
17288
|
+
initialMode: dialog.mode,
|
|
17289
|
+
restoreFocusTarget: ()=>dialog.restoreFocusTarget?.isConnected ? dialog.restoreFocusTarget : editorRef.current?.view.dom ?? null,
|
|
17290
|
+
onApplied: applied,
|
|
17291
|
+
onClose: close,
|
|
17292
|
+
onSubmit: submit
|
|
17293
|
+
}) : null,
|
|
17294
|
+
open
|
|
17295
|
+
};
|
|
17296
|
+
}
|
|
17297
|
+
function comparisonFileExtension(name) {
|
|
17298
|
+
const extension = name.split('.').at(-1)?.toLocaleLowerCase() ?? '';
|
|
17299
|
+
return 'docx' === extension || 'html' === extension || 'htm' === extension || 'txt' === extension ? extension : null;
|
|
17300
|
+
}
|
|
17301
|
+
function invalidComparisonResult() {
|
|
17302
|
+
return {
|
|
17303
|
+
status: 'unsupported',
|
|
17304
|
+
summary: {
|
|
17305
|
+
deletions: 0,
|
|
17306
|
+
formatting: 0,
|
|
17307
|
+
insertions: 0,
|
|
17308
|
+
paragraphFormatting: 0
|
|
17309
|
+
},
|
|
17310
|
+
diagnostics: [
|
|
17311
|
+
{
|
|
17312
|
+
code: 'invalid-revised-content',
|
|
17313
|
+
message: 'The selected file cannot be imported as a Writer document.'
|
|
17314
|
+
}
|
|
17315
|
+
]
|
|
17316
|
+
};
|
|
17317
|
+
}
|
|
16246
17318
|
function DocumentTableOfContentsDialog({ editing, options, restoreFocusTarget, onCancel, onOptionsChange, onSubmit }) {
|
|
16247
17319
|
const updateOptions = (update)=>onOptionsChange({
|
|
16248
17320
|
...options,
|
|
@@ -18886,6 +19958,13 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
18886
19958
|
requestEditorViewChange,
|
|
18887
19959
|
taskPane
|
|
18888
19960
|
]);
|
|
19961
|
+
const documentComparison = useDocumentComparison({
|
|
19962
|
+
editor,
|
|
19963
|
+
onApplied: ()=>{
|
|
19964
|
+
if (editor && !editor.isDestroyed) taskPaneInvokerRef.current = editor.view.dom;
|
|
19965
|
+
setTaskPane('changes');
|
|
19966
|
+
}
|
|
19967
|
+
});
|
|
18889
19968
|
const startCommentDraft = useCallback(async ()=>{
|
|
18890
19969
|
rememberTaskPaneInvoker();
|
|
18891
19970
|
if (!await requestEditorViewChange(null, false)) return;
|
|
@@ -19308,6 +20387,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
19308
20387
|
restoreDocumentBodyFocus();
|
|
19309
20388
|
},
|
|
19310
20389
|
onToggleChanges: ()=>void toggleTaskPane('changes'),
|
|
20390
|
+
onOpenComparison: documentComparison.open,
|
|
19311
20391
|
onDecideChange: (change, decision)=>decideDocumentChanges([
|
|
19312
20392
|
change
|
|
19313
20393
|
], decision),
|
|
@@ -19635,6 +20715,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
|
|
|
19635
20715
|
onClose: ()=>setSelectionMenu(null)
|
|
19636
20716
|
}),
|
|
19637
20717
|
canEditDocument && documentInsert.dialog,
|
|
20718
|
+
canEditDocument && documentComparison.dialog,
|
|
19638
20719
|
!preview && taskPaneDialog.dialog,
|
|
19639
20720
|
!preview && statisticsOpen && /*#__PURE__*/ jsx(DocumentStatisticsDialog, {
|
|
19640
20721
|
pageCount: pageCount,
|