@ctzhian/tiptap 3.0.2 → 3.0.3
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 +4 -0
- package/dist/EditorDiff/index.d.ts +3 -1
- package/dist/EditorDiff/index.js +3 -1
- package/dist/extension/component/UploadProgress/index.d.ts +1 -1
- package/dist/extension/extension/StructuredDiff.d.ts +4 -2
- package/dist/extension/extension/StructuredDiff.js +19 -6
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +3 -2
- package/dist/type/index.d.ts +3 -0
- package/dist/util/decorations.d.ts +3 -2
- package/dist/util/decorations.js +108 -44
- package/dist/util/structuredDiff.d.ts +8 -2
- package/dist/util/structuredDiff.js +165 -36
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -117,6 +117,10 @@ const { editor } = useTiptap({
|
|
|
117
117
|
以只读方式展示两段 HTML 的结构化差异。
|
|
118
118
|
|
|
119
119
|
- Props:`{ oldHtml: string; newHtml: string }`
|
|
120
|
+
- 可选增强:支持 `diffOptions`,例如通过 `ignoreAttrs` 忽略节点或 mark 上的指定属性差异,或通过 `engine: 'enhanced'` 为特殊 inline block 和重排场景启用更细粒度的 diff;未传时默认行为保持不变。
|
|
121
|
+
- 兼容性:现有接入方式无需调整,`v3.x` 内默认仍保持零配置升级。
|
|
122
|
+
- 当前定位:只读结构化 diff 展示,不等同于修订模式或 track changes。
|
|
123
|
+
- 本轮优化:自动清理“无差异时残留高亮”,并增强图片、附件、公式、音视频等删除节点的预览文案。
|
|
120
124
|
|
|
121
125
|
```tsx
|
|
122
126
|
<EditorDiff oldHtml={'<p>old</p>'} newHtml={'<p><strong>new</strong></p>'} />
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { StructuredDiffOptions } from "../util/structuredDiff";
|
|
2
3
|
import 'core-js/actual/array/find-last';
|
|
3
4
|
interface EditorDiffProps {
|
|
4
5
|
oldHtml: string;
|
|
5
6
|
newHtml: string;
|
|
6
7
|
baseUrl?: string;
|
|
8
|
+
diffOptions?: Partial<StructuredDiffOptions>;
|
|
7
9
|
}
|
|
8
|
-
declare const EditorDiff: ({ oldHtml, newHtml, baseUrl }: EditorDiffProps) => React.JSX.Element;
|
|
10
|
+
declare const EditorDiff: ({ oldHtml, newHtml, baseUrl, diffOptions }: EditorDiffProps) => React.JSX.Element;
|
|
9
11
|
export default EditorDiff;
|
package/dist/EditorDiff/index.js
CHANGED
|
@@ -9,11 +9,13 @@ import useTiptap from "../hook";
|
|
|
9
9
|
var EditorDiff = function EditorDiff(_ref) {
|
|
10
10
|
var oldHtml = _ref.oldHtml,
|
|
11
11
|
newHtml = _ref.newHtml,
|
|
12
|
-
baseUrl = _ref.baseUrl
|
|
12
|
+
baseUrl = _ref.baseUrl,
|
|
13
|
+
diffOptions = _ref.diffOptions;
|
|
13
14
|
var editorRef = useTiptap({
|
|
14
15
|
editable: false,
|
|
15
16
|
content: newHtml,
|
|
16
17
|
baseUrl: baseUrl,
|
|
18
|
+
structuredDiffOptions: diffOptions,
|
|
17
19
|
exclude: ['youtube', 'mention']
|
|
18
20
|
});
|
|
19
21
|
useEffect(function () {
|
|
@@ -7,6 +7,6 @@ export interface UploadProgressAttributes {
|
|
|
7
7
|
tempId: string;
|
|
8
8
|
}
|
|
9
9
|
export declare const getFileIcon: (fileType: string) => React.JSX.Element;
|
|
10
|
-
export declare const getFileTypeText: (fileType: string) => "
|
|
10
|
+
export declare const getFileTypeText: (fileType: string) => "图片" | "视频" | "音频" | "文件";
|
|
11
11
|
declare const UploadProgressView: React.FC<NodeViewProps>;
|
|
12
12
|
export default UploadProgressView;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import type { Editor } from '@tiptap/core';
|
|
2
3
|
import { PluginKey } from '@tiptap/pm/state';
|
|
4
|
+
import type { StructuredDiffOptions } from '../../util/structuredDiff';
|
|
3
5
|
declare const diffPluginKey: PluginKey<any>;
|
|
4
6
|
declare module '@tiptap/core' {
|
|
5
7
|
interface Commands<ReturnType> {
|
|
@@ -9,7 +11,7 @@ declare module '@tiptap/core' {
|
|
|
9
11
|
};
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
|
-
export declare const StructuredDiffExtension: Extension<
|
|
14
|
+
export declare const StructuredDiffExtension: Extension<StructuredDiffOptions, any>;
|
|
13
15
|
export { diffPluginKey };
|
|
14
16
|
export declare function getDiffState(editor: Editor): {
|
|
15
17
|
isActive: any;
|
|
@@ -10,6 +10,11 @@ import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
|
10
10
|
import { createDecorationsFromDiffs, createEmptyDecorationSet } from "../../util/decorations";
|
|
11
11
|
import { compareDocuments } from "../../util/structuredDiff";
|
|
12
12
|
var diffPluginKey = new PluginKey('structuredDiff');
|
|
13
|
+
function createHideDiffTransaction(tr) {
|
|
14
|
+
return tr.setMeta(diffPluginKey, {
|
|
15
|
+
type: 'hideDiff'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
13
18
|
var DiffPluginState = /*#__PURE__*/function () {
|
|
14
19
|
function DiffPluginState(decorations) {
|
|
15
20
|
var diffs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
@@ -52,6 +57,13 @@ var DiffPluginState = /*#__PURE__*/function () {
|
|
|
52
57
|
}();
|
|
53
58
|
export var StructuredDiffExtension = Extension.create({
|
|
54
59
|
name: 'structuredDiff',
|
|
60
|
+
addOptions: function addOptions() {
|
|
61
|
+
return {
|
|
62
|
+
engine: 'legacy',
|
|
63
|
+
ignoreAttrs: [],
|
|
64
|
+
nodePreviewSerializer: undefined
|
|
65
|
+
};
|
|
66
|
+
},
|
|
55
67
|
addProseMirrorPlugins: function addProseMirrorPlugins() {
|
|
56
68
|
return [new Plugin({
|
|
57
69
|
key: diffPluginKey,
|
|
@@ -68,6 +80,7 @@ export var StructuredDiffExtension = Extension.create({
|
|
|
68
80
|
})];
|
|
69
81
|
},
|
|
70
82
|
addCommands: function addCommands() {
|
|
83
|
+
var diffOptions = this.options;
|
|
71
84
|
return {
|
|
72
85
|
showStructuredDiff: function showStructuredDiff(oldHtml, newHtml) {
|
|
73
86
|
return function (_ref) {
|
|
@@ -77,11 +90,14 @@ export var StructuredDiffExtension = Extension.create({
|
|
|
77
90
|
editor = _ref.editor;
|
|
78
91
|
try {
|
|
79
92
|
var extensions = editor.extensionManager.extensions;
|
|
80
|
-
var comparison = compareDocuments(oldHtml, newHtml, extensions);
|
|
93
|
+
var comparison = compareDocuments(oldHtml, newHtml, extensions, diffOptions);
|
|
81
94
|
if (!comparison.hasChanges) {
|
|
95
|
+
if (dispatch) {
|
|
96
|
+
dispatch(createHideDiffTransaction(tr));
|
|
97
|
+
}
|
|
82
98
|
return false;
|
|
83
99
|
}
|
|
84
|
-
var decorations = createDecorationsFromDiffs(comparison.diffs, state.doc);
|
|
100
|
+
var decorations = createDecorationsFromDiffs(comparison.diffs, state.doc, diffOptions);
|
|
85
101
|
var newTr = tr.setMeta(diffPluginKey, {
|
|
86
102
|
type: 'showDiff',
|
|
87
103
|
decorations: decorations,
|
|
@@ -100,12 +116,9 @@ export var StructuredDiffExtension = Extension.create({
|
|
|
100
116
|
hideStructuredDiff: function hideStructuredDiff() {
|
|
101
117
|
return function (_ref2) {
|
|
102
118
|
var tr = _ref2.tr,
|
|
103
|
-
state = _ref2.state,
|
|
104
119
|
dispatch = _ref2.dispatch;
|
|
105
120
|
try {
|
|
106
|
-
var newTr = tr
|
|
107
|
-
type: 'hideDiff'
|
|
108
|
-
});
|
|
121
|
+
var newTr = createHideDiffTransaction(tr);
|
|
109
122
|
if (dispatch) dispatch(newTr);
|
|
110
123
|
return true;
|
|
111
124
|
} catch (error) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { GetExtensionsProps } from '../type';
|
|
2
|
-
export declare const getExtensions: ({ limit, exclude, extensions: extensionsProps, editable, mentionItems, baseUrl, onMentionFilter, onUpload, onUploadImgUrl, onError, onTocUpdate, onAiWritingGetSuggestion, onValidateUrl, placeholder, mermaidOptions, youtubeOptions, tableOfContentsOptions, }: GetExtensionsProps) => any;
|
|
2
|
+
export declare const getExtensions: ({ limit, exclude, extensions: extensionsProps, editable, mentionItems, baseUrl, onMentionFilter, onUpload, onUploadImgUrl, onError, onTocUpdate, onAiWritingGetSuggestion, onValidateUrl, placeholder, mermaidOptions, youtubeOptions, tableOfContentsOptions, structuredDiffOptions, }: GetExtensionsProps) => any;
|
package/dist/extension/index.js
CHANGED
|
@@ -34,7 +34,8 @@ export var getExtensions = function getExtensions(_ref) {
|
|
|
34
34
|
_placeholder = _ref.placeholder,
|
|
35
35
|
mermaidOptions = _ref.mermaidOptions,
|
|
36
36
|
youtubeOptions = _ref.youtubeOptions,
|
|
37
|
-
tableOfContentsOptions = _ref.tableOfContentsOptions
|
|
37
|
+
tableOfContentsOptions = _ref.tableOfContentsOptions,
|
|
38
|
+
structuredDiffOptions = _ref.structuredDiffOptions;
|
|
38
39
|
var defaultExtensions = [ImeComposition, StarterKit.configure({
|
|
39
40
|
link: false,
|
|
40
41
|
code: false,
|
|
@@ -170,7 +171,7 @@ export var getExtensions = function getExtensions(_ref) {
|
|
|
170
171
|
} else {
|
|
171
172
|
// 只读模式
|
|
172
173
|
if (!(exclude !== null && exclude !== void 0 && exclude.includes('structuredDiff'))) {
|
|
173
|
-
defaultExtensions.push(StructuredDiffExtension);
|
|
174
|
+
defaultExtensions.push(StructuredDiffExtension.configure(structuredDiffOptions || {}));
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
if (!(exclude !== null && exclude !== void 0 && exclude.includes('mention')) && (mentionItems && mentionItems.length > 0 || onMentionFilter)) {
|
package/dist/type/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { TableOfContentsOptions } from '@tiptap/extension-table-of-contents';
|
|
|
5
5
|
import { YoutubeOptions } from '@tiptap/extension-youtube';
|
|
6
6
|
import { UseEditorOptions } from '@tiptap/react';
|
|
7
7
|
import { MermaidConfig } from 'mermaid';
|
|
8
|
+
import type { StructuredDiffOptions } from '../util/structuredDiff';
|
|
8
9
|
export type { Editor } from '@tiptap/react';
|
|
10
|
+
export type { StructuredDiffEngine, StructuredDiffOptions } from '../util/structuredDiff';
|
|
9
11
|
export interface MenuItem {
|
|
10
12
|
label?: React.ReactNode;
|
|
11
13
|
customLabel?: React.ReactNode;
|
|
@@ -118,6 +120,7 @@ export type NodeOrMetaOrSuggestionOrExtensionOptions = {
|
|
|
118
120
|
mermaidOptions?: Partial<MermaidConfig>;
|
|
119
121
|
youtubeOptions?: Partial<YoutubeOptions>;
|
|
120
122
|
tableOfContentsOptions?: Partial<TableOfContentsOptions>;
|
|
123
|
+
structuredDiffOptions?: Partial<StructuredDiffOptions>;
|
|
121
124
|
};
|
|
122
125
|
export type BaseExtensionOptions = {
|
|
123
126
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Node as PMNode } from '@tiptap/pm/model';
|
|
2
2
|
import { DecorationSet } from '@tiptap/pm/view';
|
|
3
|
-
import { DiffItem, ProseMirrorNode } from './structuredDiff';
|
|
3
|
+
import type { DiffItem, ProseMirrorNode, StructuredDiffOptions } from './structuredDiff';
|
|
4
|
+
export declare function getDeletedNodePreviewText(node: ProseMirrorNode, options?: StructuredDiffOptions): string;
|
|
4
5
|
/**
|
|
5
6
|
* 根据差异数组创建装饰集合
|
|
6
7
|
* @param {Array} diffs - 差异数组
|
|
7
8
|
* @param {Object} doc - ProseMirror文档
|
|
8
9
|
* @returns {DecorationSet} 装饰集合
|
|
9
10
|
*/
|
|
10
|
-
export declare function createDecorationsFromDiffs(diffs: DiffItem[], doc: PMNode): DecorationSet;
|
|
11
|
+
export declare function createDecorationsFromDiffs(diffs: DiffItem[], doc: PMNode, options?: StructuredDiffOptions): DecorationSet;
|
|
11
12
|
/**
|
|
12
13
|
* 创建空的装饰集合
|
|
13
14
|
* @param {Object} doc - ProseMirror文档
|
package/dist/util/decorations.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
|
|
2
2
|
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
3
3
|
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
5
|
+
|
|
4
6
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
5
7
|
import { pathToPos } from "./structuredDiff";
|
|
6
|
-
|
|
7
8
|
/**
|
|
8
9
|
* 创建插入内容的装饰
|
|
9
10
|
* @param {number} from - 开始位置
|
|
@@ -36,13 +37,13 @@ function createModifyDecoration(from, to) {
|
|
|
36
37
|
* @param {Object} deletedNode - 被删除的节点
|
|
37
38
|
* @returns {Decoration} widget装饰对象
|
|
38
39
|
*/
|
|
39
|
-
function createDeleteWidget(pos, deletedNode) {
|
|
40
|
+
function createDeleteWidget(pos, deletedNode, options) {
|
|
40
41
|
var widget = document.createElement('span');
|
|
41
42
|
widget.className = 'diff-delete-widget';
|
|
42
43
|
widget.style.cssText = "\n background-color: rgba(220, 53, 69, 0.1);\n color: #dc3545;\n text-decoration: line-through;\n padding: 2px 4px;\n border-radius: 3px;\n margin: 0 1px;\n border: 1px dashed #dc3545;\n font-style: italic;\n ";
|
|
43
44
|
|
|
44
45
|
// 提取删除内容的文本
|
|
45
|
-
var deletedText =
|
|
46
|
+
var deletedText = getDeletedNodePreviewText(deletedNode, options);
|
|
46
47
|
widget.textContent = deletedText || '[已删除]';
|
|
47
48
|
|
|
48
49
|
// 添加工具提示
|
|
@@ -51,6 +52,43 @@ function createDeleteWidget(pos, deletedNode) {
|
|
|
51
52
|
side: 1
|
|
52
53
|
});
|
|
53
54
|
}
|
|
55
|
+
function normalizePreviewText(value) {
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === 'number') {
|
|
60
|
+
return String(value);
|
|
61
|
+
}
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
function truncatePreviewText(text) {
|
|
65
|
+
var maxLength = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 60;
|
|
66
|
+
if (text.length <= maxLength) {
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
return "".concat(text.slice(0, maxLength - 1), "...");
|
|
70
|
+
}
|
|
71
|
+
function getFileNameFromUrl(url) {
|
|
72
|
+
var normalizedUrl = normalizePreviewText(url);
|
|
73
|
+
if (!normalizedUrl) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
var parsedUrl = new URL(normalizedUrl, 'https://placeholder.local');
|
|
78
|
+
var segments = parsedUrl.pathname.split('/').filter(Boolean);
|
|
79
|
+
return decodeURIComponent(segments[segments.length - 1] || '');
|
|
80
|
+
} catch (_unused) {
|
|
81
|
+
var _segments = normalizedUrl.split('/').filter(Boolean);
|
|
82
|
+
return _segments[_segments.length - 1] || normalizedUrl;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function formatDeletedNodeLabel(typeLabel, detail) {
|
|
86
|
+
var normalizedDetail = normalizePreviewText(detail);
|
|
87
|
+
if (!normalizedDetail) {
|
|
88
|
+
return "[\u5DF2\u5220\u9664".concat(typeLabel, "]");
|
|
89
|
+
}
|
|
90
|
+
return "[\u5DF2\u5220\u9664".concat(typeLabel, ": ").concat(truncatePreviewText(normalizedDetail), "]");
|
|
91
|
+
}
|
|
54
92
|
|
|
55
93
|
/**
|
|
56
94
|
* 从节点中提取文本内容
|
|
@@ -69,6 +107,59 @@ function extractTextFromNode(node) {
|
|
|
69
107
|
}
|
|
70
108
|
return '';
|
|
71
109
|
}
|
|
110
|
+
export function getDeletedNodePreviewText(node, options) {
|
|
111
|
+
var _options$nodePreviewS;
|
|
112
|
+
if (!node) {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
var customPreview = normalizePreviewText(options === null || options === void 0 || (_options$nodePreviewS = options.nodePreviewSerializer) === null || _options$nodePreviewS === void 0 ? void 0 : _options$nodePreviewS.call(options, node));
|
|
116
|
+
if (customPreview) {
|
|
117
|
+
return truncatePreviewText(customPreview);
|
|
118
|
+
}
|
|
119
|
+
var textContent = truncatePreviewText(normalizePreviewText(extractTextFromNode(node)));
|
|
120
|
+
if (textContent) {
|
|
121
|
+
return textContent;
|
|
122
|
+
}
|
|
123
|
+
var attrs = node.attrs || {};
|
|
124
|
+
var nodeType = node.type || 'content';
|
|
125
|
+
var namedDetail = function namedDetail(value, fallback) {
|
|
126
|
+
var detail = normalizePreviewText(value) || normalizePreviewText(fallback);
|
|
127
|
+
return detail || '';
|
|
128
|
+
};
|
|
129
|
+
switch (nodeType) {
|
|
130
|
+
case 'image':
|
|
131
|
+
return formatDeletedNodeLabel('图片', namedDetail(attrs.alt, attrs.src && getFileNameFromUrl(attrs.src)));
|
|
132
|
+
case 'inlineAttachment':
|
|
133
|
+
case 'blockAttachment':
|
|
134
|
+
return formatDeletedNodeLabel('附件', namedDetail(attrs.title, attrs.url && getFileNameFromUrl(attrs.url)));
|
|
135
|
+
case 'video':
|
|
136
|
+
return formatDeletedNodeLabel('视频', namedDetail(attrs.title, attrs.src && getFileNameFromUrl(attrs.src)));
|
|
137
|
+
case 'audio':
|
|
138
|
+
return formatDeletedNodeLabel('音频', namedDetail(attrs.title, attrs.src && getFileNameFromUrl(attrs.src)));
|
|
139
|
+
case 'iframe':
|
|
140
|
+
return formatDeletedNodeLabel('嵌入内容', namedDetail(attrs.title, attrs.src));
|
|
141
|
+
case 'inlineMath':
|
|
142
|
+
return formatDeletedNodeLabel('行内公式', attrs.latex);
|
|
143
|
+
case 'blockMath':
|
|
144
|
+
return formatDeletedNodeLabel('公式', attrs.latex);
|
|
145
|
+
case 'inlineLink':
|
|
146
|
+
case 'blockLink':
|
|
147
|
+
return formatDeletedNodeLabel('链接', namedDetail(attrs.title, attrs.href));
|
|
148
|
+
case 'details':
|
|
149
|
+
return formatDeletedNodeLabel('折叠块');
|
|
150
|
+
case 'detailsSummary':
|
|
151
|
+
return formatDeletedNodeLabel('折叠标题');
|
|
152
|
+
case 'detailsContent':
|
|
153
|
+
return formatDeletedNodeLabel('折叠内容');
|
|
154
|
+
case 'table':
|
|
155
|
+
return formatDeletedNodeLabel('表格');
|
|
156
|
+
case 'codeBlock':
|
|
157
|
+
case 'code_block':
|
|
158
|
+
return formatDeletedNodeLabel('代码块', attrs.language || attrs.lang);
|
|
159
|
+
default:
|
|
160
|
+
return formatDeletedNodeLabel('内容');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
72
163
|
|
|
73
164
|
/**
|
|
74
165
|
* 根据差异数组创建装饰集合
|
|
@@ -76,7 +167,7 @@ function extractTextFromNode(node) {
|
|
|
76
167
|
* @param {Object} doc - ProseMirror文档
|
|
77
168
|
* @returns {DecorationSet} 装饰集合
|
|
78
169
|
*/
|
|
79
|
-
export function createDecorationsFromDiffs(diffs, doc) {
|
|
170
|
+
export function createDecorationsFromDiffs(diffs, doc, options) {
|
|
80
171
|
var decorations = [];
|
|
81
172
|
diffs.forEach(function (diff) {
|
|
82
173
|
try {
|
|
@@ -126,10 +217,10 @@ export function createDecorationsFromDiffs(diffs, doc) {
|
|
|
126
217
|
decorations.push(createDeleteWidget(widgetPos, {
|
|
127
218
|
type: 'text',
|
|
128
219
|
text: diff.textDiff.text
|
|
129
|
-
}));
|
|
220
|
+
}, options));
|
|
130
221
|
} else if (diff.node) {
|
|
131
222
|
// 节点级别的删除 - 使用widget显示
|
|
132
|
-
decorations.push(createDeleteWidget(pos, diff.node));
|
|
223
|
+
decorations.push(createDeleteWidget(pos, diff.node, options));
|
|
133
224
|
}
|
|
134
225
|
break;
|
|
135
226
|
case 'modify':
|
|
@@ -172,50 +263,23 @@ export function createDecorationsFromDiffs(diffs, doc) {
|
|
|
172
263
|
});
|
|
173
264
|
return DecorationSet.create(doc, decorations);
|
|
174
265
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* 获取指定路径节点的大小
|
|
178
|
-
* @param {Array} path - 节点路径
|
|
179
|
-
* @param {Object} doc - ProseMirror文档
|
|
180
|
-
* @returns {number} 节点大小
|
|
181
|
-
*/
|
|
182
|
-
function getNodeSizeAtPath(path, doc) {
|
|
183
|
-
var currentNode = doc;
|
|
266
|
+
function getPMNodeAtPath(path, doc) {
|
|
267
|
+
var current = doc;
|
|
184
268
|
var _iterator = _createForOfIteratorHelper(path),
|
|
185
269
|
_step;
|
|
186
270
|
try {
|
|
187
271
|
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
188
|
-
var index = _step.value;
|
|
189
|
-
if (currentNode.content && currentNode.content[index]) {
|
|
190
|
-
currentNode = currentNode.content[index];
|
|
191
|
-
} else {
|
|
192
|
-
return 1; // 默认大小
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
_iterator.e(err);
|
|
197
|
-
} finally {
|
|
198
|
-
_iterator.f();
|
|
199
|
-
}
|
|
200
|
-
return getNodeSize(currentNode);
|
|
201
|
-
}
|
|
202
|
-
function getPMNodeAtPath(path, doc) {
|
|
203
|
-
var current = doc;
|
|
204
|
-
var _iterator2 = _createForOfIteratorHelper(path),
|
|
205
|
-
_step2;
|
|
206
|
-
try {
|
|
207
|
-
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
208
272
|
var _childCount;
|
|
209
|
-
var index =
|
|
273
|
+
var index = _step.value;
|
|
210
274
|
if (!current) return null;
|
|
211
275
|
var childCount = (_childCount = current.childCount) !== null && _childCount !== void 0 ? _childCount : 0;
|
|
212
276
|
if (index >= childCount) return null;
|
|
213
277
|
current = current.child(index);
|
|
214
278
|
}
|
|
215
279
|
} catch (err) {
|
|
216
|
-
|
|
280
|
+
_iterator.e(err);
|
|
217
281
|
} finally {
|
|
218
|
-
|
|
282
|
+
_iterator.f();
|
|
219
283
|
}
|
|
220
284
|
return current;
|
|
221
285
|
}
|
|
@@ -285,17 +349,17 @@ export function getNodeSize(node) {
|
|
|
285
349
|
}
|
|
286
350
|
var size = 2; // 开始和结束标记
|
|
287
351
|
if (node.content && Array.isArray(node.content)) {
|
|
288
|
-
var
|
|
289
|
-
|
|
352
|
+
var _iterator2 = _createForOfIteratorHelper(node.content),
|
|
353
|
+
_step2;
|
|
290
354
|
try {
|
|
291
|
-
for (
|
|
292
|
-
var child =
|
|
355
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
356
|
+
var child = _step2.value;
|
|
293
357
|
size += getNodeSize(child);
|
|
294
358
|
}
|
|
295
359
|
} catch (err) {
|
|
296
|
-
|
|
360
|
+
_iterator2.e(err);
|
|
297
361
|
} finally {
|
|
298
|
-
|
|
362
|
+
_iterator2.f();
|
|
299
363
|
}
|
|
300
364
|
}
|
|
301
365
|
return size;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Extensions } from '@tiptap/core';
|
|
1
|
+
import type { Extensions } from '@tiptap/core';
|
|
2
2
|
export interface TextDiff {
|
|
3
3
|
offset: number;
|
|
4
4
|
length: number;
|
|
@@ -35,6 +35,12 @@ export interface ProseMirrorNode {
|
|
|
35
35
|
attrs?: Record<string, any>;
|
|
36
36
|
}>;
|
|
37
37
|
}
|
|
38
|
+
export type StructuredDiffEngine = 'legacy' | 'enhanced';
|
|
39
|
+
export interface StructuredDiffOptions {
|
|
40
|
+
engine?: StructuredDiffEngine;
|
|
41
|
+
ignoreAttrs?: string[];
|
|
42
|
+
nodePreviewSerializer?: (node: ProseMirrorNode) => string | null | undefined;
|
|
43
|
+
}
|
|
38
44
|
/**
|
|
39
45
|
* 将HTML转换为ProseMirror文档结构
|
|
40
46
|
* @param {string} html - HTML字符串
|
|
@@ -49,7 +55,7 @@ export declare function parseHtmlToDoc(html: string, extensions: Extensions): an
|
|
|
49
55
|
* @param {Array} extensions - Tiptap扩展数组
|
|
50
56
|
* @returns {Object} 包含差异信息的对象
|
|
51
57
|
*/
|
|
52
|
-
export declare function compareDocuments(oldHtml: string, newHtml: string, extensions: Extensions): DocumentComparison;
|
|
58
|
+
export declare function compareDocuments(oldHtml: string, newHtml: string, extensions: Extensions, options?: StructuredDiffOptions): DocumentComparison;
|
|
53
59
|
/**
|
|
54
60
|
* 将路径转换为ProseMirror位置
|
|
55
61
|
* @param {Array} path - 节点路径
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
2
|
-
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
|
-
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
4
|
-
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
5
1
|
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
|
|
6
2
|
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
7
3
|
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
|
|
8
4
|
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
|
|
5
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
6
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
7
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
8
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
9
9
|
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
|
|
10
10
|
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
11
11
|
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
12
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
13
|
+
|
|
12
14
|
import { generateJSON } from '@tiptap/html';
|
|
13
15
|
import DiffMatchPatch from 'diff-match-patch';
|
|
14
16
|
|
|
@@ -17,6 +19,10 @@ var dmp = new DiffMatchPatch();
|
|
|
17
19
|
|
|
18
20
|
// 类型定义
|
|
19
21
|
|
|
22
|
+
var LEGACY_INLINE_CONTAINER_TYPES = new Set(['paragraph', 'heading']);
|
|
23
|
+
var NON_INLINE_LEAF_CONTAINER_TYPES = new Set(['doc', 'table', 'tableRow', 'table_row', 'tableCell', 'table_cell', 'tableHeader', 'table_header', 'orderedList', 'ordered_list', 'bulletList', 'bullet_list', 'taskList', 'task_list', 'taskItem', 'task_item', 'listItem', 'list_item', 'blockquote', 'details', 'detailsContent', 'details_content', 'codeBlock', 'code_block', 'alert', 'horizontalRule', 'horizontal_rule', 'flow', 'flipGrid', 'flip_grid', 'flipGridColumn', 'flip_grid_column', 'yamlFrontmatter', 'yaml_frontmatter']);
|
|
24
|
+
var BLOCKISH_CHILD_NODE_TYPES = new Set(['image', 'video', 'audio', 'iframe', 'flow', 'blockAttachment', 'blockLink', 'blockMath', 'table', 'horizontalRule', 'horizontal_rule', 'codeBlock', 'code_block', 'details', 'detailsContent', 'details_content', 'orderedList', 'ordered_list', 'bulletList', 'bullet_list', 'taskList', 'task_list', 'taskItem', 'task_item', 'listItem', 'list_item', 'blockquote', 'alert', 'flipGrid', 'flip_grid', 'flipGridColumn', 'flip_grid_column', 'yamlFrontmatter', 'yaml_frontmatter']);
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* 将HTML转换为ProseMirror文档结构
|
|
22
28
|
* @param {string} html - HTML字符串
|
|
@@ -26,20 +32,25 @@ var dmp = new DiffMatchPatch();
|
|
|
26
32
|
export function parseHtmlToDoc(html, extensions) {
|
|
27
33
|
return generateJSON(html, extensions);
|
|
28
34
|
}
|
|
29
|
-
function
|
|
35
|
+
function serializeMark(mark, options) {
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
type: mark.type,
|
|
38
|
+
attrs: getFilteredAttrs(mark.attrs, options)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function haveSameMarks(a, b, options) {
|
|
30
42
|
var arrA = a || [];
|
|
31
43
|
var arrB = b || [];
|
|
32
44
|
if (arrA.length !== arrB.length) return false;
|
|
33
|
-
var
|
|
34
|
-
return
|
|
35
|
-
};
|
|
36
|
-
var setA = new Set(arrA.map(serialize));
|
|
45
|
+
var setA = new Set(arrA.map(function (mark) {
|
|
46
|
+
return serializeMark(mark, options);
|
|
47
|
+
}));
|
|
37
48
|
var _iterator = _createForOfIteratorHelper(arrB),
|
|
38
49
|
_step;
|
|
39
50
|
try {
|
|
40
51
|
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
41
52
|
var m = _step.value;
|
|
42
|
-
if (!setA.has(
|
|
53
|
+
if (!setA.has(serializeMark(m, options))) return false;
|
|
43
54
|
}
|
|
44
55
|
} catch (err) {
|
|
45
56
|
_iterator.e(err);
|
|
@@ -56,8 +67,88 @@ function haveSameMarks(a, b) {
|
|
|
56
67
|
* @param {Array} path - 当前节点路径
|
|
57
68
|
* @returns {Array} 差异数组
|
|
58
69
|
*/
|
|
70
|
+
function isIgnoredAttr(key, options) {
|
|
71
|
+
var _options$ignoreAttrs;
|
|
72
|
+
return !!(options !== null && options !== void 0 && (_options$ignoreAttrs = options.ignoreAttrs) !== null && _options$ignoreAttrs !== void 0 && _options$ignoreAttrs.includes(key));
|
|
73
|
+
}
|
|
74
|
+
function getFilteredAttrs(attrs, options) {
|
|
75
|
+
var _options$ignoreAttrs2;
|
|
76
|
+
var source = attrs || {};
|
|
77
|
+
if (!(options !== null && options !== void 0 && (_options$ignoreAttrs2 = options.ignoreAttrs) !== null && _options$ignoreAttrs2 !== void 0 && _options$ignoreAttrs2.length)) {
|
|
78
|
+
return source;
|
|
79
|
+
}
|
|
80
|
+
return Object.fromEntries(Object.entries(source).filter(function (_ref) {
|
|
81
|
+
var _ref2 = _slicedToArray(_ref, 1),
|
|
82
|
+
key = _ref2[0];
|
|
83
|
+
return !isIgnoredAttr(key, options);
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
function areAttrsEqual(attrsA, attrsB, options) {
|
|
87
|
+
return JSON.stringify(getFilteredAttrs(attrsA, options)) === JSON.stringify(getFilteredAttrs(attrsB, options));
|
|
88
|
+
}
|
|
89
|
+
function getStructuredDiffEngine(options) {
|
|
90
|
+
var _options$engine;
|
|
91
|
+
return (_options$engine = options === null || options === void 0 ? void 0 : options.engine) !== null && _options$engine !== void 0 ? _options$engine : 'legacy';
|
|
92
|
+
}
|
|
93
|
+
function getNodeSignatureForAlign(node, options) {
|
|
94
|
+
if (!node) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
if (node.type === 'text') {
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
type: node.type,
|
|
100
|
+
text: node.text || '',
|
|
101
|
+
marks: (node.marks || []).map(function (mark) {
|
|
102
|
+
return serializeMark(mark, options);
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
type: node.type,
|
|
108
|
+
attrs: getFilteredAttrs(node.attrs, options),
|
|
109
|
+
content: (node.content || []).map(function (child) {
|
|
110
|
+
return getNodeSignatureForAlign(child, options);
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function createAlignPredicate(a, b, getNode, options) {
|
|
115
|
+
if (getStructuredDiffEngine(options) !== 'enhanced') {
|
|
116
|
+
return function (x, y) {
|
|
117
|
+
return nodesEqualForAlign(getNode(x), getNode(y), options);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var countSignatures = function countSignatures(items) {
|
|
121
|
+
var counts = new Map();
|
|
122
|
+
items.forEach(function (item) {
|
|
123
|
+
var signature = getNodeSignatureForAlign(getNode(item), options);
|
|
124
|
+
if (!signature) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
counts.set(signature, (counts.get(signature) || 0) + 1);
|
|
128
|
+
});
|
|
129
|
+
return counts;
|
|
130
|
+
};
|
|
131
|
+
var countsA = countSignatures(a);
|
|
132
|
+
var countsB = countSignatures(b);
|
|
133
|
+
var uniqueSharedSignatures = new Set(Array.from(countsA.keys()).filter(function (signature) {
|
|
134
|
+
return countsA.get(signature) === 1 && countsB.get(signature) === 1;
|
|
135
|
+
}));
|
|
136
|
+
return function (x, y) {
|
|
137
|
+
var nodeX = getNode(x);
|
|
138
|
+
var nodeY = getNode(y);
|
|
139
|
+
var signatureX = getNodeSignatureForAlign(nodeX, options);
|
|
140
|
+
var signatureY = getNodeSignatureForAlign(nodeY, options);
|
|
141
|
+
var isXUnique = uniqueSharedSignatures.has(signatureX);
|
|
142
|
+
var isYUnique = uniqueSharedSignatures.has(signatureY);
|
|
143
|
+
if (isXUnique || isYUnique) {
|
|
144
|
+
return isXUnique && isYUnique && signatureX === signatureY;
|
|
145
|
+
}
|
|
146
|
+
return nodesEqualForAlign(nodeX, nodeY, options);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
59
149
|
function compareNodes(nodeA, nodeB) {
|
|
60
150
|
var path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
|
|
151
|
+
var options = arguments.length > 3 ? arguments[3] : undefined;
|
|
61
152
|
var diffs = [];
|
|
62
153
|
|
|
63
154
|
// 如果节点类型不同,标记整个节点为变更
|
|
@@ -93,10 +184,10 @@ function compareNodes(nodeA, nodeB) {
|
|
|
93
184
|
|
|
94
185
|
var textOffset = 0; // offset in the NEW text node
|
|
95
186
|
|
|
96
|
-
textDiffs.forEach(function (
|
|
97
|
-
var
|
|
98
|
-
operation =
|
|
99
|
-
text =
|
|
187
|
+
textDiffs.forEach(function (_ref3) {
|
|
188
|
+
var _ref4 = _slicedToArray(_ref3, 2),
|
|
189
|
+
operation = _ref4[0],
|
|
190
|
+
text = _ref4[1];
|
|
100
191
|
if (operation === -1) {
|
|
101
192
|
// Delete
|
|
102
193
|
// The widget should be placed at the current position in the new text.
|
|
@@ -133,7 +224,7 @@ function compareNodes(nodeA, nodeB) {
|
|
|
133
224
|
});
|
|
134
225
|
}
|
|
135
226
|
// 文本相等或不相等时都可检查 marks 差异(粗粒度:节点级)
|
|
136
|
-
if (!haveSameMarks(nodeA.marks, nodeB.marks)) {
|
|
227
|
+
if (!haveSameMarks(nodeA.marks, nodeB.marks, options)) {
|
|
137
228
|
diffs.push({
|
|
138
229
|
type: 'modify',
|
|
139
230
|
path: _toConsumableArray(path),
|
|
@@ -148,11 +239,11 @@ function compareNodes(nodeA, nodeB) {
|
|
|
148
239
|
}
|
|
149
240
|
|
|
150
241
|
// 优先:段落/标题等内联容器执行字符级 diff 和 marks 区间对比
|
|
151
|
-
if (nodeA && nodeB && isInlineContainer(nodeA) && isInlineContainer(nodeB)) {
|
|
242
|
+
if (nodeA && nodeB && isInlineContainer(nodeA, options) && isInlineContainer(nodeB, options)) {
|
|
152
243
|
// 文本与 marks 的字符级 diff
|
|
153
|
-
diffs.push.apply(diffs, _toConsumableArray(compareInlineContainer(nodeA, nodeB, path)));
|
|
244
|
+
diffs.push.apply(diffs, _toConsumableArray(compareInlineContainer(nodeA, nodeB, path, options)));
|
|
154
245
|
// 非文本内联节点(如行内数学、行内公式等)的结构化对比
|
|
155
|
-
diffs.push.apply(diffs, _toConsumableArray(compareInlineContainerChildren(nodeA, nodeB, path)));
|
|
246
|
+
diffs.push.apply(diffs, _toConsumableArray(compareInlineContainerChildren(nodeA, nodeB, path, options)));
|
|
156
247
|
return diffs;
|
|
157
248
|
}
|
|
158
249
|
|
|
@@ -162,6 +253,9 @@ function compareNodes(nodeA, nodeB) {
|
|
|
162
253
|
var allAttrKeys = new Set([].concat(_toConsumableArray(Object.keys(attrsA)), _toConsumableArray(Object.keys(attrsB))));
|
|
163
254
|
for (var _i = 0, _Array$from = Array.from(allAttrKeys); _i < _Array$from.length; _i++) {
|
|
164
255
|
var key = _Array$from[_i];
|
|
256
|
+
if (isIgnoredAttr(key, options)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
165
259
|
if (attrsA[key] !== attrsB[key]) {
|
|
166
260
|
diffs.push({
|
|
167
261
|
type: 'modify',
|
|
@@ -178,7 +272,9 @@ function compareNodes(nodeA, nodeB) {
|
|
|
178
272
|
// 使用 LCS 对齐子节点,减少错配
|
|
179
273
|
var contentA = (nodeA === null || nodeA === void 0 ? void 0 : nodeA.content) || [];
|
|
180
274
|
var contentB = (nodeB === null || nodeB === void 0 ? void 0 : nodeB.content) || [];
|
|
181
|
-
var pairs = lcsAlign(contentA, contentB,
|
|
275
|
+
var pairs = lcsAlign(contentA, contentB, createAlignPredicate(contentA, contentB, function (node) {
|
|
276
|
+
return node;
|
|
277
|
+
}, options));
|
|
182
278
|
var ai = 0;
|
|
183
279
|
var bi = 0;
|
|
184
280
|
for (var _i2 = 0, _pairs = pairs; _i2 < _pairs.length; _i2++) {
|
|
@@ -208,7 +304,7 @@ function compareNodes(nodeA, nodeB) {
|
|
|
208
304
|
// 匹配上的成对递归,路径以新文档索引为准
|
|
209
305
|
var childA = contentA[i];
|
|
210
306
|
var childB = contentB[j];
|
|
211
|
-
diffs.push.apply(diffs, _toConsumableArray(compareNodes(childA, childB, [].concat(_toConsumableArray(path), [j]))));
|
|
307
|
+
diffs.push.apply(diffs, _toConsumableArray(compareNodes(childA, childB, [].concat(_toConsumableArray(path), [j]), options)));
|
|
212
308
|
ai = i + 1;
|
|
213
309
|
bi = j + 1;
|
|
214
310
|
}
|
|
@@ -233,8 +329,31 @@ function compareNodes(nodeA, nodeB) {
|
|
|
233
329
|
}
|
|
234
330
|
return diffs;
|
|
235
331
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
332
|
+
function isInlineLikeChildNode(node) {
|
|
333
|
+
var _node$content;
|
|
334
|
+
if (node.type === 'text') {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
if (BLOCKISH_CHILD_NODE_TYPES.has(node.type)) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
return !((_node$content = node.content) !== null && _node$content !== void 0 && _node$content.length);
|
|
341
|
+
}
|
|
342
|
+
function isInlineLeafContainer(node) {
|
|
343
|
+
if (NON_INLINE_LEAF_CONTAINER_TYPES.has(node.type)) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
var content = node.content || [];
|
|
347
|
+
if (content.length === 0) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return content.every(isInlineLikeChildNode);
|
|
351
|
+
}
|
|
352
|
+
function isInlineContainer(node, options) {
|
|
353
|
+
if (getStructuredDiffEngine(options) === 'legacy') {
|
|
354
|
+
return LEGACY_INLINE_CONTAINER_TYPES.has(node.type);
|
|
355
|
+
}
|
|
356
|
+
return isInlineLeafContainer(node);
|
|
238
357
|
}
|
|
239
358
|
function extractInlineTextAndMarks(node) {
|
|
240
359
|
var resultChars = [];
|
|
@@ -263,7 +382,7 @@ function extractInlineTextAndMarks(node) {
|
|
|
263
382
|
marksMap: marksMap
|
|
264
383
|
};
|
|
265
384
|
}
|
|
266
|
-
function compareInlineContainer(nodeA, nodeB, path) {
|
|
385
|
+
function compareInlineContainer(nodeA, nodeB, path, options) {
|
|
267
386
|
var diffs = [];
|
|
268
387
|
var _extractInlineTextAnd = extractInlineTextAndMarks(nodeA),
|
|
269
388
|
oldText = _extractInlineTextAnd.text,
|
|
@@ -312,7 +431,7 @@ function compareInlineContainer(nodeA, nodeB, path) {
|
|
|
312
431
|
for (var i = 0; i < len; i++) {
|
|
313
432
|
var oldIdx = oldOffset + i;
|
|
314
433
|
var newIdx = newOffset + i;
|
|
315
|
-
var same = haveSameMarks(oldMarks[oldIdx] || [], newMarks[newIdx] || []);
|
|
434
|
+
var same = haveSameMarks(oldMarks[oldIdx] || [], newMarks[newIdx] || [], options);
|
|
316
435
|
if (!same) {
|
|
317
436
|
if (runStart === null) runStart = i;
|
|
318
437
|
} else if (runStart !== null) {
|
|
@@ -350,32 +469,40 @@ function compareInlineContainer(nodeA, nodeB, path) {
|
|
|
350
469
|
}
|
|
351
470
|
return diffs;
|
|
352
471
|
}
|
|
353
|
-
function nodesEqualForAlign(a, b) {
|
|
472
|
+
function nodesEqualForAlign(a, b, options) {
|
|
354
473
|
if (!a || !b) return false;
|
|
355
474
|
if (a.type !== b.type) return false;
|
|
356
475
|
// 对部分结构节点使用关键属性辅助匹配
|
|
357
476
|
if (a.type === 'heading') {
|
|
358
477
|
var _a$attrs$level, _a$attrs, _b$attrs$level, _b$attrs;
|
|
478
|
+
if (isIgnoredAttr('level', options)) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
359
481
|
return ((_a$attrs$level = (_a$attrs = a.attrs) === null || _a$attrs === void 0 ? void 0 : _a$attrs.level) !== null && _a$attrs$level !== void 0 ? _a$attrs$level : null) === ((_b$attrs$level = (_b$attrs = b.attrs) === null || _b$attrs === void 0 ? void 0 : _b$attrs.level) !== null && _b$attrs$level !== void 0 ? _b$attrs$level : null);
|
|
360
482
|
}
|
|
361
483
|
if (a.type === 'codeBlock' || a.type === 'code_block') {
|
|
362
|
-
var
|
|
363
|
-
|
|
364
|
-
|
|
484
|
+
var _ref5, _a$attrs$language, _a$attrs2, _a$attrs3, _ref6, _b$attrs$language, _b$attrs2, _b$attrs3;
|
|
485
|
+
if (isIgnoredAttr('language', options) || isIgnoredAttr('lang', options)) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
var langA = (_ref5 = (_a$attrs$language = (_a$attrs2 = a.attrs) === null || _a$attrs2 === void 0 ? void 0 : _a$attrs2.language) !== null && _a$attrs$language !== void 0 ? _a$attrs$language : (_a$attrs3 = a.attrs) === null || _a$attrs3 === void 0 ? void 0 : _a$attrs3.lang) !== null && _ref5 !== void 0 ? _ref5 : null;
|
|
489
|
+
var langB = (_ref6 = (_b$attrs$language = (_b$attrs2 = b.attrs) === null || _b$attrs2 === void 0 ? void 0 : _b$attrs2.language) !== null && _b$attrs$language !== void 0 ? _b$attrs$language : (_b$attrs3 = b.attrs) === null || _b$attrs3 === void 0 ? void 0 : _b$attrs3.lang) !== null && _ref6 !== void 0 ? _ref6 : null;
|
|
365
490
|
return langA === langB;
|
|
366
491
|
}
|
|
367
492
|
if (a.type === 'table_cell' || a.type === 'tableHeader' || a.type === 'table_header') {
|
|
368
493
|
var _a$attrs$colspan, _a$attrs4, _b$attrs$colspan, _b$attrs4, _a$attrs$rowspan, _a$attrs5, _b$attrs$rowspan, _b$attrs5;
|
|
494
|
+
var compareColspan = !isIgnoredAttr('colspan', options);
|
|
495
|
+
var compareRowspan = !isIgnoredAttr('rowspan', options);
|
|
369
496
|
var colspanA = (_a$attrs$colspan = (_a$attrs4 = a.attrs) === null || _a$attrs4 === void 0 ? void 0 : _a$attrs4.colspan) !== null && _a$attrs$colspan !== void 0 ? _a$attrs$colspan : 1;
|
|
370
497
|
var colspanB = (_b$attrs$colspan = (_b$attrs4 = b.attrs) === null || _b$attrs4 === void 0 ? void 0 : _b$attrs4.colspan) !== null && _b$attrs$colspan !== void 0 ? _b$attrs$colspan : 1;
|
|
371
498
|
var rowspanA = (_a$attrs$rowspan = (_a$attrs5 = a.attrs) === null || _a$attrs5 === void 0 ? void 0 : _a$attrs5.rowspan) !== null && _a$attrs$rowspan !== void 0 ? _a$attrs$rowspan : 1;
|
|
372
499
|
var rowspanB = (_b$attrs$rowspan = (_b$attrs5 = b.attrs) === null || _b$attrs5 === void 0 ? void 0 : _b$attrs5.rowspan) !== null && _b$attrs$rowspan !== void 0 ? _b$attrs$rowspan : 1;
|
|
373
|
-
return colspanA === colspanB && rowspanA === rowspanB;
|
|
500
|
+
return (!compareColspan || colspanA === colspanB) && (!compareRowspan || rowspanA === rowspanB);
|
|
374
501
|
}
|
|
375
502
|
// 默认仅按类型
|
|
376
503
|
return true;
|
|
377
504
|
}
|
|
378
|
-
function compareInlineContainerChildren(nodeA, nodeB, path) {
|
|
505
|
+
function compareInlineContainerChildren(nodeA, nodeB, path, options) {
|
|
379
506
|
var diffs = [];
|
|
380
507
|
var aList = (nodeA.content || []).map(function (n, idx) {
|
|
381
508
|
return {
|
|
@@ -398,7 +525,9 @@ function compareInlineContainerChildren(nodeA, nodeB, path) {
|
|
|
398
525
|
// 仅按类型对齐,属性差异作为 modify 处理
|
|
399
526
|
return x.n.type === y.n.type;
|
|
400
527
|
};
|
|
401
|
-
var pairs = lcsAlign(aList, bList,
|
|
528
|
+
var pairs = lcsAlign(aList, bList, getStructuredDiffEngine(options) === 'enhanced' ? createAlignPredicate(aList, bList, function (item) {
|
|
529
|
+
return item.n;
|
|
530
|
+
}, options) : equals);
|
|
402
531
|
var ai = 0,
|
|
403
532
|
bi = 0;
|
|
404
533
|
for (var _i5 = 0, _pairs2 = pairs; _i5 < _pairs2.length; _i5++) {
|
|
@@ -432,14 +561,14 @@ function compareInlineContainerChildren(nodeA, nodeB, path) {
|
|
|
432
561
|
if (aItem && bItem) {
|
|
433
562
|
var oldAttrs = aItem.n.attrs || {};
|
|
434
563
|
var newAttrs = bItem.n.attrs || {};
|
|
435
|
-
if (
|
|
564
|
+
if (!areAttrsEqual(oldAttrs, newAttrs, options)) {
|
|
436
565
|
diffs.push({
|
|
437
566
|
type: 'modify',
|
|
438
567
|
path: [].concat(_toConsumableArray(path), [bItem.idx]),
|
|
439
568
|
attrChange: {
|
|
440
569
|
key: 'attrs',
|
|
441
|
-
oldValue: oldAttrs,
|
|
442
|
-
newValue: newAttrs
|
|
570
|
+
oldValue: getFilteredAttrs(oldAttrs, options),
|
|
571
|
+
newValue: getFilteredAttrs(newAttrs, options)
|
|
443
572
|
}
|
|
444
573
|
});
|
|
445
574
|
}
|
|
@@ -506,10 +635,10 @@ function lcsAlign(a, b, equals) {
|
|
|
506
635
|
* @param {Array} extensions - Tiptap扩展数组
|
|
507
636
|
* @returns {Object} 包含差异信息的对象
|
|
508
637
|
*/
|
|
509
|
-
export function compareDocuments(oldHtml, newHtml, extensions) {
|
|
638
|
+
export function compareDocuments(oldHtml, newHtml, extensions, options) {
|
|
510
639
|
var docA = parseHtmlToDoc(oldHtml, extensions);
|
|
511
640
|
var docB = parseHtmlToDoc(newHtml, extensions);
|
|
512
|
-
var diffs = compareNodes(docA, docB);
|
|
641
|
+
var diffs = compareNodes(docA, docB, [], options);
|
|
513
642
|
return {
|
|
514
643
|
oldDoc: docA,
|
|
515
644
|
newDoc: docB,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctzhian/tiptap",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "基于 Tiptap 二次开发的编辑器组件",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"docs:build": "dumi build",
|
|
140
140
|
"docs:preview": "dumi preview",
|
|
141
141
|
"doctor": "father doctor",
|
|
142
|
+
"test:smoke": "pnpm build && node --loader ./test/esm-loader.mjs --test test/*.test.mjs",
|
|
142
143
|
"lint": "npm run lint:es && npm run lint:css",
|
|
143
144
|
"lint:css": "stylelint \"{src,test}/**/*.{css,less}\"",
|
|
144
145
|
"lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"",
|