@samemichaeltadele/tiptap-compare 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/index.cjs.js +156 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/rollup.config.js +34 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# TiptapCompare
|
|
2
|
+
|
|
3
|
+
A Tiptap extension that provides real-time visual comparison between two versions of content in your editor. This plugin helps you track changes by highlighting additions, modifications, and deletions with different colors and styles.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Visual diff highlighting for tiptap editor changes changes
|
|
8
|
+
- Support for different types of blocks (text, paragraphs, images)
|
|
9
|
+
- Color-coded changes:
|
|
10
|
+
- Green background for added content
|
|
11
|
+
- Red background with strikethrough for removed content
|
|
12
|
+
- Yellow background for modified content
|
|
13
|
+
- Real-time comparison updates
|
|
14
|
+
- Support for nested content structures
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @samemichaeltadele/tiptap-compare
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { ComparePlugin } from 'tiptap-compare'
|
|
26
|
+
import { Editor } from '@tiptap/core'
|
|
27
|
+
|
|
28
|
+
const editor = new Editor({
|
|
29
|
+
extensions: [
|
|
30
|
+
// ... other extensions
|
|
31
|
+
ComparePlugin.configure({
|
|
32
|
+
comparisonContent: ""
|
|
33
|
+
})
|
|
34
|
+
]
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Update comparison content
|
|
38
|
+
editor.commands.setComparisonContent(newContent)
|
|
39
|
+
```
|
|
40
|
+
New content should be json format like you would get in by using the getJSON() comand on an editor
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
The plugin accepts the following options:
|
|
45
|
+
|
|
46
|
+
- `comparisonContent`: The content to compare against (default: empty string), should be the content with which it should be compared in json format.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Styling
|
|
50
|
+
|
|
51
|
+
The plugin uses the following CSS classes for styling:
|
|
52
|
+
|
|
53
|
+
- `diff-added`: Applied to added content (green background)
|
|
54
|
+
- `diff-removed`: Applied to removed content (red background with strikethrough)
|
|
55
|
+
- `diff-modified`: Applied to modified content (yellow background)
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
|
60
|
+
|
|
61
|
+
## Author
|
|
62
|
+
|
|
63
|
+
SameC137 <samemichael1415@gmail.com>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var core = require('@tiptap/core');
|
|
6
|
+
var prosemirrorState = require('prosemirror-state');
|
|
7
|
+
var prosemirrorView = require('prosemirror-view');
|
|
8
|
+
var diff = require('diff');
|
|
9
|
+
|
|
10
|
+
const pluginKey = new prosemirrorState.PluginKey("comparePlugin");
|
|
11
|
+
const ComparePlugin = core.Extension.create({
|
|
12
|
+
name: "compare",
|
|
13
|
+
addOptions() {
|
|
14
|
+
return {
|
|
15
|
+
comparisonContent: ""
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
addCommands() {
|
|
19
|
+
return {
|
|
20
|
+
setComparisonContent: (content) => ({ state, dispatch }) => {
|
|
21
|
+
const tr = state.tr.setMeta(pluginKey, {
|
|
22
|
+
comparisonContent: content,
|
|
23
|
+
});
|
|
24
|
+
if (dispatch)
|
|
25
|
+
dispatch(tr);
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
addProseMirrorPlugins() {
|
|
31
|
+
return [
|
|
32
|
+
new prosemirrorState.Plugin({
|
|
33
|
+
key: pluginKey,
|
|
34
|
+
state: {
|
|
35
|
+
init: (_, { doc }) => {
|
|
36
|
+
return {
|
|
37
|
+
comparisonContent: this.options.comparisonContent,
|
|
38
|
+
options: this.options,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
apply(tr, pluginState, _, newState) {
|
|
42
|
+
const meta = tr.getMeta(pluginKey);
|
|
43
|
+
if (meta && meta.comparisonContent !== undefined) {
|
|
44
|
+
return Object.assign(Object.assign({}, pluginState), { comparisonContent: meta.comparisonContent });
|
|
45
|
+
}
|
|
46
|
+
return pluginState;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
props: {
|
|
50
|
+
decorations(state) {
|
|
51
|
+
var _a, _b, _c;
|
|
52
|
+
const pluginState = pluginKey.getState(state);
|
|
53
|
+
if (!pluginState)
|
|
54
|
+
return null;
|
|
55
|
+
const { comparisonContent } = pluginState;
|
|
56
|
+
if (!comparisonContent)
|
|
57
|
+
return null;
|
|
58
|
+
if (!comparisonContent.content)
|
|
59
|
+
return null;
|
|
60
|
+
if (!comparisonContent.content[0].content)
|
|
61
|
+
return null;
|
|
62
|
+
const decos = [];
|
|
63
|
+
const oldContent = comparisonContent;
|
|
64
|
+
const newContent = state.doc.toJSON();
|
|
65
|
+
const oldNodes = oldContent.content;
|
|
66
|
+
const newNodes = newContent.content;
|
|
67
|
+
let pos = 0;
|
|
68
|
+
for (let i = 0; i < Math.max(oldNodes === null || oldNodes === void 0 ? void 0 : oldNodes.length, newNodes === null || newNodes === void 0 ? void 0 : newNodes.length); i++) {
|
|
69
|
+
const oldNode = oldNodes[i];
|
|
70
|
+
const newNode = newNodes[i];
|
|
71
|
+
if (!oldNode) {
|
|
72
|
+
// Node added
|
|
73
|
+
const nodeSize = ((_a = state.doc.nodeAt(pos)) === null || _a === void 0 ? void 0 : _a.nodeSize) || 0;
|
|
74
|
+
decos.push(prosemirrorView.Decoration.node(pos, pos + nodeSize, {
|
|
75
|
+
class: "diff-added bg-[#e6ffec] text-green-500",
|
|
76
|
+
}));
|
|
77
|
+
pos += nodeSize;
|
|
78
|
+
}
|
|
79
|
+
else if (!newNode) {
|
|
80
|
+
// Node removed
|
|
81
|
+
decos.push(prosemirrorView.Decoration.widget(pos, createRemovedNode(oldNode)));
|
|
82
|
+
}
|
|
83
|
+
else if (oldNode.type !== newNode.type) {
|
|
84
|
+
// Node type changed
|
|
85
|
+
const nodeSize = ((_b = state.doc.nodeAt(pos)) === null || _b === void 0 ? void 0 : _b.nodeSize) || 0;
|
|
86
|
+
decos.push(prosemirrorView.Decoration.node(pos, pos + nodeSize, {
|
|
87
|
+
class: "diff-modified bg-[#ffefc6] text-red-500",
|
|
88
|
+
}));
|
|
89
|
+
pos += nodeSize;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Compare node content
|
|
93
|
+
const oldText = nodeToText(oldNode);
|
|
94
|
+
const newText = nodeToText(newNode);
|
|
95
|
+
const diff$1 = diff.diffChars(oldText, newText);
|
|
96
|
+
let nodePos = pos + 1; // +1 to skip the node start tag
|
|
97
|
+
diff$1.forEach((part) => {
|
|
98
|
+
const length = part.value.length;
|
|
99
|
+
if (part.added) {
|
|
100
|
+
decos.push(prosemirrorView.Decoration.inline(nodePos, nodePos + length, {
|
|
101
|
+
class: "diff-added bg-[#e6ffec] text-green-500",
|
|
102
|
+
}));
|
|
103
|
+
nodePos += length;
|
|
104
|
+
}
|
|
105
|
+
else if (part.removed) {
|
|
106
|
+
decos.push(prosemirrorView.Decoration.widget(nodePos, createRemovedSpan(part.value)));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
nodePos += length;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
pos += ((_c = state.doc.nodeAt(pos)) === null || _c === void 0 ? void 0 : _c.nodeSize) || 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return prosemirrorView.DecorationSet.create(state.doc, decos);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
];
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
function nodeToText(node) {
|
|
123
|
+
var _a;
|
|
124
|
+
if (node.type === "text") {
|
|
125
|
+
return node.text || "";
|
|
126
|
+
}
|
|
127
|
+
else if (node.type === "paragraph") {
|
|
128
|
+
if (!node.content)
|
|
129
|
+
return " \n";
|
|
130
|
+
return ((_a = node.content) === null || _a === void 0 ? void 0 : _a.map(nodeToText).join("")) + "\n";
|
|
131
|
+
}
|
|
132
|
+
else if (node.type === "image") {
|
|
133
|
+
return `[Image: ${node.attrs.alt || "No alt text"} (${node.attrs.src})]\n`;
|
|
134
|
+
}
|
|
135
|
+
else if (node.content) {
|
|
136
|
+
return node.content.map(nodeToText).join("");
|
|
137
|
+
}
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
function createRemovedSpan(text) {
|
|
141
|
+
const span = document.createElement("span");
|
|
142
|
+
span.className = "diff-removed bg-[#ffebe9] line-through text-red-500";
|
|
143
|
+
span.textContent = text;
|
|
144
|
+
return span;
|
|
145
|
+
}
|
|
146
|
+
function createRemovedNode(node) {
|
|
147
|
+
const div = document.createElement("div");
|
|
148
|
+
div.className =
|
|
149
|
+
"diff-removed-node bg-[#ffebe9] line-through px-0 py-0.5 text-red-500";
|
|
150
|
+
div.textContent = nodeToText(node);
|
|
151
|
+
return div;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
exports.ComparePlugin = ComparePlugin;
|
|
155
|
+
exports.default = ComparePlugin;
|
|
156
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
interface ComparePluginOptions {
|
|
3
|
+
comparisonContent: any;
|
|
4
|
+
}
|
|
5
|
+
declare module "@tiptap/core" {
|
|
6
|
+
interface Commands<ReturnType> {
|
|
7
|
+
compare: {
|
|
8
|
+
setComparisonContent: (content: any) => ReturnType;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
declare const ComparePlugin: Extension<ComparePluginOptions, any>;
|
|
13
|
+
export { ComparePlugin };
|
|
14
|
+
export default ComparePlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { PluginKey, Plugin } from 'prosemirror-state';
|
|
3
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
4
|
+
import { diffChars } from 'diff';
|
|
5
|
+
|
|
6
|
+
const pluginKey = new PluginKey("comparePlugin");
|
|
7
|
+
const ComparePlugin = Extension.create({
|
|
8
|
+
name: "compare",
|
|
9
|
+
addOptions() {
|
|
10
|
+
return {
|
|
11
|
+
comparisonContent: ""
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
addCommands() {
|
|
15
|
+
return {
|
|
16
|
+
setComparisonContent: (content) => ({ state, dispatch }) => {
|
|
17
|
+
const tr = state.tr.setMeta(pluginKey, {
|
|
18
|
+
comparisonContent: content,
|
|
19
|
+
});
|
|
20
|
+
if (dispatch)
|
|
21
|
+
dispatch(tr);
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
addProseMirrorPlugins() {
|
|
27
|
+
return [
|
|
28
|
+
new Plugin({
|
|
29
|
+
key: pluginKey,
|
|
30
|
+
state: {
|
|
31
|
+
init: (_, { doc }) => {
|
|
32
|
+
return {
|
|
33
|
+
comparisonContent: this.options.comparisonContent,
|
|
34
|
+
options: this.options,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
apply(tr, pluginState, _, newState) {
|
|
38
|
+
const meta = tr.getMeta(pluginKey);
|
|
39
|
+
if (meta && meta.comparisonContent !== undefined) {
|
|
40
|
+
return Object.assign(Object.assign({}, pluginState), { comparisonContent: meta.comparisonContent });
|
|
41
|
+
}
|
|
42
|
+
return pluginState;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
props: {
|
|
46
|
+
decorations(state) {
|
|
47
|
+
var _a, _b, _c;
|
|
48
|
+
const pluginState = pluginKey.getState(state);
|
|
49
|
+
if (!pluginState)
|
|
50
|
+
return null;
|
|
51
|
+
const { comparisonContent } = pluginState;
|
|
52
|
+
if (!comparisonContent)
|
|
53
|
+
return null;
|
|
54
|
+
if (!comparisonContent.content)
|
|
55
|
+
return null;
|
|
56
|
+
if (!comparisonContent.content[0].content)
|
|
57
|
+
return null;
|
|
58
|
+
const decos = [];
|
|
59
|
+
const oldContent = comparisonContent;
|
|
60
|
+
const newContent = state.doc.toJSON();
|
|
61
|
+
const oldNodes = oldContent.content;
|
|
62
|
+
const newNodes = newContent.content;
|
|
63
|
+
let pos = 0;
|
|
64
|
+
for (let i = 0; i < Math.max(oldNodes === null || oldNodes === void 0 ? void 0 : oldNodes.length, newNodes === null || newNodes === void 0 ? void 0 : newNodes.length); i++) {
|
|
65
|
+
const oldNode = oldNodes[i];
|
|
66
|
+
const newNode = newNodes[i];
|
|
67
|
+
if (!oldNode) {
|
|
68
|
+
// Node added
|
|
69
|
+
const nodeSize = ((_a = state.doc.nodeAt(pos)) === null || _a === void 0 ? void 0 : _a.nodeSize) || 0;
|
|
70
|
+
decos.push(Decoration.node(pos, pos + nodeSize, {
|
|
71
|
+
class: "diff-added bg-[#e6ffec] text-green-500",
|
|
72
|
+
}));
|
|
73
|
+
pos += nodeSize;
|
|
74
|
+
}
|
|
75
|
+
else if (!newNode) {
|
|
76
|
+
// Node removed
|
|
77
|
+
decos.push(Decoration.widget(pos, createRemovedNode(oldNode)));
|
|
78
|
+
}
|
|
79
|
+
else if (oldNode.type !== newNode.type) {
|
|
80
|
+
// Node type changed
|
|
81
|
+
const nodeSize = ((_b = state.doc.nodeAt(pos)) === null || _b === void 0 ? void 0 : _b.nodeSize) || 0;
|
|
82
|
+
decos.push(Decoration.node(pos, pos + nodeSize, {
|
|
83
|
+
class: "diff-modified bg-[#ffefc6] text-red-500",
|
|
84
|
+
}));
|
|
85
|
+
pos += nodeSize;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Compare node content
|
|
89
|
+
const oldText = nodeToText(oldNode);
|
|
90
|
+
const newText = nodeToText(newNode);
|
|
91
|
+
const diff = diffChars(oldText, newText);
|
|
92
|
+
let nodePos = pos + 1; // +1 to skip the node start tag
|
|
93
|
+
diff.forEach((part) => {
|
|
94
|
+
const length = part.value.length;
|
|
95
|
+
if (part.added) {
|
|
96
|
+
decos.push(Decoration.inline(nodePos, nodePos + length, {
|
|
97
|
+
class: "diff-added bg-[#e6ffec] text-green-500",
|
|
98
|
+
}));
|
|
99
|
+
nodePos += length;
|
|
100
|
+
}
|
|
101
|
+
else if (part.removed) {
|
|
102
|
+
decos.push(Decoration.widget(nodePos, createRemovedSpan(part.value)));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
nodePos += length;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
pos += ((_c = state.doc.nodeAt(pos)) === null || _c === void 0 ? void 0 : _c.nodeSize) || 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return DecorationSet.create(state.doc, decos);
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
];
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
function nodeToText(node) {
|
|
119
|
+
var _a;
|
|
120
|
+
if (node.type === "text") {
|
|
121
|
+
return node.text || "";
|
|
122
|
+
}
|
|
123
|
+
else if (node.type === "paragraph") {
|
|
124
|
+
if (!node.content)
|
|
125
|
+
return " \n";
|
|
126
|
+
return ((_a = node.content) === null || _a === void 0 ? void 0 : _a.map(nodeToText).join("")) + "\n";
|
|
127
|
+
}
|
|
128
|
+
else if (node.type === "image") {
|
|
129
|
+
return `[Image: ${node.attrs.alt || "No alt text"} (${node.attrs.src})]\n`;
|
|
130
|
+
}
|
|
131
|
+
else if (node.content) {
|
|
132
|
+
return node.content.map(nodeToText).join("");
|
|
133
|
+
}
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
function createRemovedSpan(text) {
|
|
137
|
+
const span = document.createElement("span");
|
|
138
|
+
span.className = "diff-removed bg-[#ffebe9] line-through text-red-500";
|
|
139
|
+
span.textContent = text;
|
|
140
|
+
return span;
|
|
141
|
+
}
|
|
142
|
+
function createRemovedNode(node) {
|
|
143
|
+
const div = document.createElement("div");
|
|
144
|
+
div.className =
|
|
145
|
+
"diff-removed-node bg-[#ffebe9] line-through px-0 py-0.5 text-red-500";
|
|
146
|
+
div.textContent = nodeToText(node);
|
|
147
|
+
return div;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { ComparePlugin, ComparePlugin as default };
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@samemichaeltadele/tiptap-compare",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Tip Tap diff compare extension ",
|
|
5
|
+
"keywords": ["tiptap", "compare", "diff", "highlight", "highlighting", "diffing", "diff-highlighting", "diff-highlighting-tiptap", "tiptap-compare", "tiptap-diff", "tiptap-highlight", "tiptap-highlighting", "tiptap-diff-highlighting", "tiptap-diff-highlighting-tiptap"],
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/SameC137/tiptap-compare.git"
|
|
9
|
+
},
|
|
10
|
+
"author": "SameC137 <samemichael1415@gmail.com>",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"main": "dist/index.cjs.js",
|
|
13
|
+
"module": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "rm -r -Force dist",
|
|
17
|
+
"build": "rollup -c",
|
|
18
|
+
"dev": " rollup -c -w"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@rollup/plugin-babel": "^6.0.3",
|
|
22
|
+
"@rollup/plugin-commonjs": "^24.0.1",
|
|
23
|
+
"@tiptap/core": "^2.0.0-beta.220",
|
|
24
|
+
"@tiptap/pm": "^2.0.0-beta.220",
|
|
25
|
+
"rollup": "^3.17.3",
|
|
26
|
+
"rollup-plugin-auto-external": "^2.0.0",
|
|
27
|
+
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
28
|
+
"rollup-plugin-typescript2": "^0.34.1",
|
|
29
|
+
"typescript": "^4.9.5"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@tiptap/core": "^2.0.0-beta.220",
|
|
33
|
+
"@tiptap/pm": "^2.0.0-beta.220"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"diff": "^7.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// rollup.config.js
|
|
2
|
+
|
|
3
|
+
const autoExternal = require("rollup-plugin-auto-external");
|
|
4
|
+
const sourcemaps = require("rollup-plugin-sourcemaps");
|
|
5
|
+
const commonjs = require("@rollup/plugin-commonjs");
|
|
6
|
+
const babel = require("@rollup/plugin-babel");
|
|
7
|
+
const typescript = require("rollup-plugin-typescript2");
|
|
8
|
+
|
|
9
|
+
const config = {
|
|
10
|
+
input: "src/index.ts",
|
|
11
|
+
output: [
|
|
12
|
+
{
|
|
13
|
+
file: "dist/index.cjs.js",
|
|
14
|
+
format: "cjs",
|
|
15
|
+
exports: "named",
|
|
16
|
+
sourcemap: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
file: "dist/index.js",
|
|
20
|
+
format: "esm",
|
|
21
|
+
exports: "named",
|
|
22
|
+
sourcemap: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
plugins: [
|
|
26
|
+
autoExternal({ packagePath: "./package.json" }),
|
|
27
|
+
sourcemaps(),
|
|
28
|
+
babel(),
|
|
29
|
+
commonjs(),
|
|
30
|
+
typescript(),
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = config;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
4
|
+
import { diffChars } from "diff";
|
|
5
|
+
|
|
6
|
+
interface ComparePluginOptions {
|
|
7
|
+
comparisonContent: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module "@tiptap/core" {
|
|
11
|
+
interface Commands<ReturnType> {
|
|
12
|
+
compare: {
|
|
13
|
+
setComparisonContent: (content: any) => ReturnType;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pluginKey = new PluginKey("comparePlugin");
|
|
19
|
+
|
|
20
|
+
const ComparePlugin = Extension.create<ComparePluginOptions>({
|
|
21
|
+
name: "compare",
|
|
22
|
+
|
|
23
|
+
addOptions() {
|
|
24
|
+
return {
|
|
25
|
+
comparisonContent: ""
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
addCommands() {
|
|
30
|
+
return {
|
|
31
|
+
setComparisonContent:
|
|
32
|
+
(content: string) =>
|
|
33
|
+
({ state, dispatch }) => {
|
|
34
|
+
const tr = state.tr.setMeta(pluginKey, {
|
|
35
|
+
comparisonContent: content,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (dispatch) dispatch(tr);
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
addProseMirrorPlugins() {
|
|
45
|
+
return [
|
|
46
|
+
new Plugin({
|
|
47
|
+
key: pluginKey,
|
|
48
|
+
state: {
|
|
49
|
+
init: (_, { doc }) => {
|
|
50
|
+
return {
|
|
51
|
+
comparisonContent: this.options.comparisonContent,
|
|
52
|
+
options: this.options,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
apply(tr, pluginState, _, newState) {
|
|
56
|
+
const meta = tr.getMeta(pluginKey);
|
|
57
|
+
if (meta && meta.comparisonContent !== undefined) {
|
|
58
|
+
return {
|
|
59
|
+
...pluginState,
|
|
60
|
+
comparisonContent: meta.comparisonContent,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return pluginState;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
props: {
|
|
67
|
+
decorations(state) {
|
|
68
|
+
const pluginState = pluginKey.getState(state);
|
|
69
|
+
if (!pluginState) return null;
|
|
70
|
+
|
|
71
|
+
const { comparisonContent } = pluginState;
|
|
72
|
+
if (!comparisonContent) return null;
|
|
73
|
+
if (!comparisonContent.content) return null;
|
|
74
|
+
if (!comparisonContent.content[0].content) return null;
|
|
75
|
+
|
|
76
|
+
const decos: Decoration[] = [];
|
|
77
|
+
const oldContent = comparisonContent;
|
|
78
|
+
const newContent = state.doc.toJSON();
|
|
79
|
+
|
|
80
|
+
const oldNodes = oldContent.content;
|
|
81
|
+
const newNodes = newContent.content;
|
|
82
|
+
|
|
83
|
+
let pos = 0;
|
|
84
|
+
|
|
85
|
+
for (
|
|
86
|
+
let i = 0;
|
|
87
|
+
i < Math.max(oldNodes?.length, newNodes?.length);
|
|
88
|
+
i++
|
|
89
|
+
) {
|
|
90
|
+
const oldNode = oldNodes[i];
|
|
91
|
+
const newNode = newNodes[i];
|
|
92
|
+
|
|
93
|
+
if (!oldNode) {
|
|
94
|
+
// Node added
|
|
95
|
+
const nodeSize = state.doc.nodeAt(pos)?.nodeSize || 0;
|
|
96
|
+
decos.push(
|
|
97
|
+
Decoration.node(pos, pos + nodeSize, {
|
|
98
|
+
class: "diff-added bg-[#e6ffec] text-green-500",
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
pos += nodeSize;
|
|
102
|
+
} else if (!newNode) {
|
|
103
|
+
// Node removed
|
|
104
|
+
decos.push(Decoration.widget(pos, createRemovedNode(oldNode)));
|
|
105
|
+
} else if (oldNode.type !== newNode.type) {
|
|
106
|
+
// Node type changed
|
|
107
|
+
const nodeSize = state.doc.nodeAt(pos)?.nodeSize || 0;
|
|
108
|
+
decos.push(
|
|
109
|
+
Decoration.node(pos, pos + nodeSize, {
|
|
110
|
+
class: "diff-modified bg-[#ffefc6] text-red-500",
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
pos += nodeSize;
|
|
114
|
+
} else {
|
|
115
|
+
// Compare node content
|
|
116
|
+
const oldText = nodeToText(oldNode);
|
|
117
|
+
const newText = nodeToText(newNode);
|
|
118
|
+
|
|
119
|
+
const diff = diffChars(oldText, newText);
|
|
120
|
+
let nodePos = pos + 1; // +1 to skip the node start tag
|
|
121
|
+
|
|
122
|
+
diff.forEach((part) => {
|
|
123
|
+
const length = part.value.length;
|
|
124
|
+
if (part.added) {
|
|
125
|
+
decos.push(
|
|
126
|
+
Decoration.inline(nodePos, nodePos + length, {
|
|
127
|
+
class: "diff-added bg-[#e6ffec] text-green-500",
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
nodePos += length;
|
|
131
|
+
} else if (part.removed) {
|
|
132
|
+
decos.push(
|
|
133
|
+
Decoration.widget(nodePos, createRemovedSpan(part.value))
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
nodePos += length;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pos += state.doc.nodeAt(pos)?.nodeSize || 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return DecorationSet.create(state.doc, decos);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
];
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function nodeToText(node: any): string {
|
|
153
|
+
if (node.type === "text") {
|
|
154
|
+
return node.text || "";
|
|
155
|
+
} else if (node.type === "paragraph") {
|
|
156
|
+
if (!node.content) return " \n";
|
|
157
|
+
return node.content?.map(nodeToText).join("") + "\n";
|
|
158
|
+
} else if (node.type === "image") {
|
|
159
|
+
return `[Image: ${node.attrs.alt || "No alt text"} (${node.attrs.src})]\n`;
|
|
160
|
+
} else if (node.content) {
|
|
161
|
+
return node.content.map(nodeToText).join("");
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createRemovedSpan(text: string) {
|
|
167
|
+
const span = document.createElement("span");
|
|
168
|
+
span.className = "diff-removed bg-[#ffebe9] line-through text-red-500";
|
|
169
|
+
span.textContent = text;
|
|
170
|
+
return span;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createRemovedNode(node: any) {
|
|
174
|
+
const div = document.createElement("div");
|
|
175
|
+
div.className =
|
|
176
|
+
"diff-removed-node bg-[#ffebe9] line-through px-0 py-0.5 text-red-500";
|
|
177
|
+
div.textContent = nodeToText(node);
|
|
178
|
+
return div;
|
|
179
|
+
}
|
|
180
|
+
export { ComparePlugin };
|
|
181
|
+
|
|
182
|
+
export default ComparePlugin;
|