@ctzhian/tiptap 0.5.5 → 1.0.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/dist/Editor/demo.js +1 -1
- package/dist/EditorDiff/demo.d.ts +4 -0
- package/dist/EditorDiff/demo.js +12 -0
- package/dist/EditorDiff/index.d.ts +7 -0
- package/dist/EditorDiff/index.js +24 -0
- package/dist/extension/component/Attachment/Readonly.js +0 -1
- package/dist/extension/component/Image/Readonly.js +0 -1
- package/dist/extension/component/Link/Readonly.js +2 -2
- package/dist/extension/component/Mathematics/block/Readonly.js +0 -1
- package/dist/extension/component/Mathematics/inline/Readonly.js +0 -1
- package/dist/extension/component/Video/Readonly.js +0 -1
- package/dist/extension/extension/StructuredDiff.d.ts +18 -0
- package/dist/extension/extension/StructuredDiff.js +131 -0
- package/dist/extension/extension/index.d.ts +1 -0
- package/dist/extension/extension/index.js +2 -1
- package/dist/extension/index.js +38 -82
- package/dist/extension/node/Table.js +7 -7
- package/dist/index.css +3 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/themes/index.d.ts +1 -1
- package/dist/util/decorations.d.ts +22 -0
- package/dist/util/decorations.js +302 -0
- package/dist/util/structuredDiff.d.ts +59 -0
- package/dist/util/structuredDiff.js +550 -0
- package/package.json +3 -1
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
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
|
+
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
|
+
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
5
|
+
import { pathToPos } from "./structuredDiff";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 创建插入内容的装饰
|
|
9
|
+
* @param {number} from - 开始位置
|
|
10
|
+
* @param {number} to - 结束位置
|
|
11
|
+
* @returns {Decoration} 装饰对象
|
|
12
|
+
*/
|
|
13
|
+
function createInsertDecoration(from, to) {
|
|
14
|
+
return Decoration.inline(from, to, {
|
|
15
|
+
class: 'diff-insert-highlight',
|
|
16
|
+
style: 'background-color: #d4edda; color: #28a745; padding: 2px 4px; border-radius: 3px; margin: 0 1px; border: 1px dashed #28a745;'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 创建修改内容的装饰
|
|
22
|
+
* @param {number} from - 开始位置
|
|
23
|
+
* @param {number} to - 结束位置
|
|
24
|
+
* @returns {Decoration} 装饰对象
|
|
25
|
+
*/
|
|
26
|
+
function createModifyDecoration(from, to) {
|
|
27
|
+
return Decoration.inline(from, to, {
|
|
28
|
+
class: 'diff-modify-highlight',
|
|
29
|
+
style: 'background-color: #fff3cd; color: #856404; padding: 2px 4px; border-radius: 3px; margin: 0 1px; border: 1px dashed #856404;'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 创建删除内容的widget装饰
|
|
35
|
+
* @param {number} pos - 插入位置
|
|
36
|
+
* @param {Object} deletedNode - 被删除的节点
|
|
37
|
+
* @returns {Decoration} widget装饰对象
|
|
38
|
+
*/
|
|
39
|
+
function createDeleteWidget(pos, deletedNode) {
|
|
40
|
+
var widget = document.createElement('span');
|
|
41
|
+
widget.className = 'diff-delete-widget';
|
|
42
|
+
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
|
+
// 提取删除内容的文本
|
|
45
|
+
var deletedText = extractTextFromNode(deletedNode);
|
|
46
|
+
widget.textContent = deletedText || '[已删除]';
|
|
47
|
+
|
|
48
|
+
// 添加工具提示
|
|
49
|
+
widget.title = "\u5220\u9664\u7684\u5185\u5BB9: ".concat(deletedText);
|
|
50
|
+
return Decoration.widget(pos, widget, {
|
|
51
|
+
side: 1
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从节点中提取文本内容
|
|
57
|
+
* @param {Object} node - ProseMirror节点
|
|
58
|
+
* @returns {string} 提取的文本
|
|
59
|
+
*/
|
|
60
|
+
function extractTextFromNode(node) {
|
|
61
|
+
if (!node) return '';
|
|
62
|
+
if (node.type === 'text') {
|
|
63
|
+
return node.text || '';
|
|
64
|
+
}
|
|
65
|
+
if (node.content && Array.isArray(node.content)) {
|
|
66
|
+
return node.content.map(function (child) {
|
|
67
|
+
return extractTextFromNode(child);
|
|
68
|
+
}).join('');
|
|
69
|
+
}
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 根据差异数组创建装饰集合
|
|
75
|
+
* @param {Array} diffs - 差异数组
|
|
76
|
+
* @param {Object} doc - ProseMirror文档
|
|
77
|
+
* @returns {DecorationSet} 装饰集合
|
|
78
|
+
*/
|
|
79
|
+
export function createDecorationsFromDiffs(diffs, doc) {
|
|
80
|
+
var decorations = [];
|
|
81
|
+
diffs.forEach(function (diff) {
|
|
82
|
+
try {
|
|
83
|
+
var pos = pathToPos(diff.path, doc);
|
|
84
|
+
var pmNodeAtPath = getPMNodeAtPath(diff.path, doc);
|
|
85
|
+
switch (diff.type) {
|
|
86
|
+
case 'insert':
|
|
87
|
+
if (diff.textDiff) {
|
|
88
|
+
// 文本级别的插入
|
|
89
|
+
var base = adjustBaseForInline(pmNodeAtPath, pos);
|
|
90
|
+
var from = base + mapInlineOffsetToPM(pmNodeAtPath, diff.textDiff.offset);
|
|
91
|
+
var to = base + mapInlineOffsetToPM(pmNodeAtPath, diff.textDiff.offset + diff.textDiff.length);
|
|
92
|
+
decorations.push(createInsertDecoration(from, to));
|
|
93
|
+
} else if (diff.node) {
|
|
94
|
+
// 节点级别的插入
|
|
95
|
+
if (pmNodeAtPath) {
|
|
96
|
+
var _text;
|
|
97
|
+
if (pmNodeAtPath.isText && (((_text = pmNodeAtPath.text) === null || _text === void 0 ? void 0 : _text.length) || 0) > 0) {
|
|
98
|
+
var _text2;
|
|
99
|
+
var _from = pos;
|
|
100
|
+
var _to = pos + (((_text2 = pmNodeAtPath.text) === null || _text2 === void 0 ? void 0 : _text2.length) || 0);
|
|
101
|
+
decorations.push(createInsertDecoration(_from, _to));
|
|
102
|
+
} else {
|
|
103
|
+
var _from2 = pos;
|
|
104
|
+
var _to2 = pos + pmNodeAtPath.nodeSize;
|
|
105
|
+
decorations.push(Decoration.node(_from2, _to2, {
|
|
106
|
+
class: 'diff-insert-highlight',
|
|
107
|
+
style: 'background-color: rgba(40,167,69,0.12); border-radius: 3px; border: 1px dashed #28a745;'
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// 回退到基于 JSON 节点的估算
|
|
112
|
+
var nodeSize = getNodeSize(diff.node);
|
|
113
|
+
var _from3 = pos + 1;
|
|
114
|
+
var _to3 = pos + nodeSize - 1;
|
|
115
|
+
if (_to3 > _from3) {
|
|
116
|
+
decorations.push(createInsertDecoration(_from3, _to3));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case 'delete':
|
|
122
|
+
if (diff.textDiff) {
|
|
123
|
+
// 文本级别的删除 - 使用widget显示
|
|
124
|
+
var _base = adjustBaseForInline(pmNodeAtPath, pos);
|
|
125
|
+
var widgetPos = _base + mapInlineOffsetToPM(pmNodeAtPath, diff.textDiff.offset);
|
|
126
|
+
decorations.push(createDeleteWidget(widgetPos, {
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: diff.textDiff.text
|
|
129
|
+
}));
|
|
130
|
+
} else if (diff.node) {
|
|
131
|
+
// 节点级别的删除 - 使用widget显示
|
|
132
|
+
decorations.push(createDeleteWidget(pos, diff.node));
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'modify':
|
|
136
|
+
if (diff.attrChange) {
|
|
137
|
+
// 基于 marks 的区间高亮
|
|
138
|
+
if (diff.attrChange.key === 'marks' && typeof diff.attrChange.fromOffset === 'number' && typeof diff.attrChange.toOffset === 'number') {
|
|
139
|
+
var _base2 = adjustBaseForInline(pmNodeAtPath, pos);
|
|
140
|
+
var _from4 = _base2 + mapInlineOffsetToPM(pmNodeAtPath, diff.attrChange.fromOffset);
|
|
141
|
+
var _to4 = _base2 + mapInlineOffsetToPM(pmNodeAtPath, diff.attrChange.toOffset);
|
|
142
|
+
if (_to4 > _from4) {
|
|
143
|
+
decorations.push(createModifyDecoration(_from4, _to4));
|
|
144
|
+
}
|
|
145
|
+
} else if (diff.attrChange.key === 'marks' && pmNodeAtPath && pmNodeAtPath.isText) {
|
|
146
|
+
var textLength = pmNodeAtPath.text ? pmNodeAtPath.text.length : 0;
|
|
147
|
+
if (textLength > 0) {
|
|
148
|
+
var _base3 = adjustBaseForInline(pmNodeAtPath, pos);
|
|
149
|
+
decorations.push(createModifyDecoration(_base3, _base3 + textLength));
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
var _type;
|
|
153
|
+
// 其他属性修改 - 节点级高亮
|
|
154
|
+
var nodeName = (pmNodeAtPath === null || pmNodeAtPath === void 0 || (_type = pmNodeAtPath.type) === null || _type === void 0 ? void 0 : _type.name) || '';
|
|
155
|
+
var isDetailsFamily = nodeName === 'details' || nodeName === 'detailsSummary' || nodeName === 'detailsContent';
|
|
156
|
+
if (!isDetailsFamily) {
|
|
157
|
+
var _nodeSize = getPMNodeSizeAtPath(diff.path, doc);
|
|
158
|
+
var _from5 = pos;
|
|
159
|
+
var _to5 = pos + _nodeSize;
|
|
160
|
+
decorations.push(Decoration.node(_from5, _to5, {
|
|
161
|
+
class: 'diff-modify-highlight',
|
|
162
|
+
style: 'background-color: rgba(255,193,7,0.12); border-radius: 3px; border: 1px dashed #856404;'
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.warn('创建装饰时出错:', error, diff);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return DecorationSet.create(doc, decorations);
|
|
174
|
+
}
|
|
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;
|
|
184
|
+
var _iterator = _createForOfIteratorHelper(path),
|
|
185
|
+
_step;
|
|
186
|
+
try {
|
|
187
|
+
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
|
+
var _childCount;
|
|
209
|
+
var index = _step2.value;
|
|
210
|
+
if (!current) return null;
|
|
211
|
+
var childCount = (_childCount = current.childCount) !== null && _childCount !== void 0 ? _childCount : 0;
|
|
212
|
+
if (index >= childCount) return null;
|
|
213
|
+
current = current.child(index);
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
_iterator2.e(err);
|
|
217
|
+
} finally {
|
|
218
|
+
_iterator2.f();
|
|
219
|
+
}
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
function getPMNodeSizeAtPath(path, doc) {
|
|
223
|
+
var node = getPMNodeAtPath(path, doc);
|
|
224
|
+
return node ? node.nodeSize : 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 对内联文本容器(paragraph/heading)的字符偏移,基于容器内容起点(跳过开标签)
|
|
228
|
+
function adjustBaseForInline(pmNodeAtPath, absolutePos) {
|
|
229
|
+
if (!pmNodeAtPath) return absolutePos;
|
|
230
|
+
// 如果路径对应的是容器本身(非文本子节点),字符偏移应从内容起点计算
|
|
231
|
+
// 但此处无法直接判断路径是否止于容器。我们采用经验:
|
|
232
|
+
// - 若 pmNodeAtPath 不是 text,则偏移从 absolutePos + 1 开始(跳过开标签)
|
|
233
|
+
// - 若是 text,则 absolutePos 已指向文本起点
|
|
234
|
+
if (pmNodeAtPath.isText) return absolutePos;
|
|
235
|
+
return absolutePos + 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 将“内联字符偏移”映射到 PM 位置(计入文本节点间的边界)
|
|
239
|
+
function mapInlineOffsetToPM(pmNodeAtPath, inlineOffset) {
|
|
240
|
+
var _childCount2;
|
|
241
|
+
if (!pmNodeAtPath) return inlineOffset;
|
|
242
|
+
// 如果当前就是文本节点,offset 等同
|
|
243
|
+
if (pmNodeAtPath.isText) return inlineOffset;
|
|
244
|
+
// 若是段落/标题容器,则 inlineOffset 是按合并后的字符序列统计,需要遍历其子 text 节点求对应 PM 偏移
|
|
245
|
+
var remaining = inlineOffset;
|
|
246
|
+
var acc = 0;
|
|
247
|
+
var childCount = (_childCount2 = pmNodeAtPath.childCount) !== null && _childCount2 !== void 0 ? _childCount2 : 0;
|
|
248
|
+
for (var i = 0; i < childCount; i++) {
|
|
249
|
+
var _child$text;
|
|
250
|
+
var child = pmNodeAtPath.child(i);
|
|
251
|
+
var size = child.isText ? ((_child$text = child.text) === null || _child$text === void 0 ? void 0 : _child$text.length) || 0 : child.nodeSize; // 非文本节点视作整体
|
|
252
|
+
if (child.isText) {
|
|
253
|
+
if (remaining <= size) {
|
|
254
|
+
acc += remaining;
|
|
255
|
+
return acc;
|
|
256
|
+
}
|
|
257
|
+
acc += size;
|
|
258
|
+
remaining -= size;
|
|
259
|
+
} else {
|
|
260
|
+
// 跳过非文本节点的整体大小
|
|
261
|
+
acc += size;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// 超出则落在末尾
|
|
265
|
+
return acc;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 创建空的装饰集合
|
|
270
|
+
* @param {Object} doc - ProseMirror文档
|
|
271
|
+
* @returns {DecorationSet} 空装饰集合
|
|
272
|
+
*/
|
|
273
|
+
export function createEmptyDecorationSet(doc) {
|
|
274
|
+
return DecorationSet.create(doc, []);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 获取节点大小(包含子节点)
|
|
279
|
+
* @param {Object} node - ProseMirror节点
|
|
280
|
+
* @returns {number} 节点大小
|
|
281
|
+
*/
|
|
282
|
+
export function getNodeSize(node) {
|
|
283
|
+
if (node.type === 'text') {
|
|
284
|
+
return node.text ? node.text.length : 0;
|
|
285
|
+
}
|
|
286
|
+
var size = 2; // 开始和结束标记
|
|
287
|
+
if (node.content && Array.isArray(node.content)) {
|
|
288
|
+
var _iterator3 = _createForOfIteratorHelper(node.content),
|
|
289
|
+
_step3;
|
|
290
|
+
try {
|
|
291
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
292
|
+
var child = _step3.value;
|
|
293
|
+
size += getNodeSize(child);
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
_iterator3.e(err);
|
|
297
|
+
} finally {
|
|
298
|
+
_iterator3.f();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return size;
|
|
302
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Extensions } from '@tiptap/core';
|
|
2
|
+
export interface TextDiff {
|
|
3
|
+
offset: number;
|
|
4
|
+
length: number;
|
|
5
|
+
text: string;
|
|
6
|
+
operation: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AttrChange {
|
|
9
|
+
key: string;
|
|
10
|
+
oldValue: any;
|
|
11
|
+
newValue: any;
|
|
12
|
+
fromOffset?: number;
|
|
13
|
+
toOffset?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DiffItem {
|
|
16
|
+
type: 'insert' | 'delete' | 'modify';
|
|
17
|
+
path: number[];
|
|
18
|
+
node?: any;
|
|
19
|
+
textDiff?: TextDiff;
|
|
20
|
+
attrChange?: AttrChange;
|
|
21
|
+
}
|
|
22
|
+
export interface DocumentComparison {
|
|
23
|
+
oldDoc: any;
|
|
24
|
+
newDoc: any;
|
|
25
|
+
diffs: DiffItem[];
|
|
26
|
+
hasChanges: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface ProseMirrorNode {
|
|
29
|
+
type: string;
|
|
30
|
+
attrs?: Record<string, any>;
|
|
31
|
+
content?: ProseMirrorNode[];
|
|
32
|
+
text?: string;
|
|
33
|
+
marks?: Array<{
|
|
34
|
+
type: string;
|
|
35
|
+
attrs?: Record<string, any>;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 将HTML转换为ProseMirror文档结构
|
|
40
|
+
* @param {string} html - HTML字符串
|
|
41
|
+
* @param {Array} extensions - Tiptap扩展数组
|
|
42
|
+
* @returns {Object} ProseMirror文档对象
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseHtmlToDoc(html: string, extensions: Extensions): any;
|
|
45
|
+
/**
|
|
46
|
+
* 对比两个HTML文档并生成结构化差异
|
|
47
|
+
* @param {string} oldHtml - 旧HTML
|
|
48
|
+
* @param {string} newHtml - 新HTML
|
|
49
|
+
* @param {Array} extensions - Tiptap扩展数组
|
|
50
|
+
* @returns {Object} 包含差异信息的对象
|
|
51
|
+
*/
|
|
52
|
+
export declare function compareDocuments(oldHtml: string, newHtml: string, extensions: Extensions): DocumentComparison;
|
|
53
|
+
/**
|
|
54
|
+
* 将路径转换为ProseMirror位置
|
|
55
|
+
* @param {Array} path - 节点路径
|
|
56
|
+
* @param {Object} doc - ProseMirror文档
|
|
57
|
+
* @returns {number} 文档位置
|
|
58
|
+
*/
|
|
59
|
+
export declare function pathToPos(path: number[], doc: any): number;
|