milkdown-inline-diff 1.0.2
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 +207 -0
- package/dist/diff-config.d.ts +11 -0
- package/dist/diff-config.d.ts.map +1 -0
- package/dist/diff-tooltip.d.ts +37 -0
- package/dist/diff-tooltip.d.ts.map +1 -0
- package/dist/diffDecorationState.d.ts +7 -0
- package/dist/diffDecorationState.d.ts.map +1 -0
- package/dist/extended-table-schema.d.ts +2 -0
- package/dist/extended-table-schema.d.ts.map +1 -0
- package/dist/index.cjs +75 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26590 -0
- package/dist/markdown-diff.d.ts +52 -0
- package/dist/markdown-diff.d.ts.map +1 -0
- package/dist/myers-diff.d.ts +7 -0
- package/dist/myers-diff.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/diff-config.ts +28 -0
- package/src/diff-tooltip.ts +302 -0
- package/src/diffDecorationState.ts +48 -0
- package/src/extended-table-schema.ts +11 -0
- package/src/index.ts +43 -0
- package/src/markdown-diff.ts +971 -0
- package/src/myers-diff.ts +134 -0
- package/src/style.css +152 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Node, Schema } from '@milkdown/prose/model';
|
|
2
|
+
import { Decoration, DecorationSet } from '@milkdown/prose/view';
|
|
3
|
+
import { Ctx } from '@milkdown/ctx';
|
|
4
|
+
export type ChangeType = "insert" | "delete" | "unchanged";
|
|
5
|
+
export interface DiffState {
|
|
6
|
+
currentIndex: number;
|
|
7
|
+
count: number;
|
|
8
|
+
}
|
|
9
|
+
interface ExtractedBlock {
|
|
10
|
+
nodes: Node[];
|
|
11
|
+
}
|
|
12
|
+
export declare function nodesEqual(a: Node, b: Node): boolean;
|
|
13
|
+
export declare function blockEqual(a: ExtractedBlock, b: ExtractedBlock): boolean;
|
|
14
|
+
export declare function deepEquals(node: Node, other: Node): boolean;
|
|
15
|
+
export interface BlockChange {
|
|
16
|
+
type: ChangeType;
|
|
17
|
+
A: ExtractedBlock | null;
|
|
18
|
+
B: ExtractedBlock | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function blockDiff(docA: Node, docB: Node): BlockChange[];
|
|
21
|
+
export interface DiffEditState {
|
|
22
|
+
mergedDoc: Node;
|
|
23
|
+
decorations: DecorationSet;
|
|
24
|
+
mergeGroups: VNode[][];
|
|
25
|
+
}
|
|
26
|
+
export interface DiffDecoration {
|
|
27
|
+
groupIndex: string;
|
|
28
|
+
delete: Decoration[];
|
|
29
|
+
insert: Decoration[];
|
|
30
|
+
}
|
|
31
|
+
export interface VNode {
|
|
32
|
+
node: Node;
|
|
33
|
+
children: VNode[];
|
|
34
|
+
changeType: ChangeType;
|
|
35
|
+
newNode?: Node;
|
|
36
|
+
}
|
|
37
|
+
export declare function getDiffDecorations(ctx: Ctx): DiffDecoration[];
|
|
38
|
+
export declare function buildVNode(blockChanges: BlockChange[]): {
|
|
39
|
+
root: VNode;
|
|
40
|
+
mergeGroups: VNode[][];
|
|
41
|
+
};
|
|
42
|
+
export declare function createDiffEditState(originalDoc: Node, modifiedDoc: Node, schema: Schema): DiffEditState;
|
|
43
|
+
export declare function getDecorationClass(type: "delete" | "insert" | "modify"): string;
|
|
44
|
+
export declare function merge(ctx: Ctx, action: "accept" | "reject", index: number, mergeAll: boolean): boolean;
|
|
45
|
+
export declare function getDiffState(ctx: Ctx): {
|
|
46
|
+
currentIndex: number;
|
|
47
|
+
count: number;
|
|
48
|
+
};
|
|
49
|
+
export declare function jumpTo(ctx: Ctx, index: number): void;
|
|
50
|
+
export declare function diff(ctx: Ctx, newContent: string, originContent?: string): void;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=markdown-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-diff.d.ts","sourceRoot":"","sources":["../src/markdown-diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAY,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAIpC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE3D,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AACD,UAAU,cAAc;IACtB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAsCD,wBAAgB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,GAAG,OAAO,CA0CpD;AAuBD,wBAAgB,UAAU,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAWxE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAE3D;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACzB,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,WAAW,EAAE,CAgD/D;AA4GD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,aAAa,CAAC;IAC3B,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,EAAE,CAgC7D;AAoFD,wBAAgB,UAAU,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG;IACvD,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC;CACxB,CAmFA;AA+ED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,IAAI,EACjB,WAAW,EAAE,IAAI,EACjB,MAAM,EAAE,MAAM,GACb,aAAa,CA4Bf;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GACnC,MAAM,CASR;AA6JD,wBAAgB,KAAK,CACnB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAC3B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,OAAO,GAChB,OAAO,CA+ET;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAoBA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,QAyB7C;AAED,wBAAgB,IAAI,CAClB,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,EAClB,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI,CA6BN"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"myers-diff.d.ts","sourceRoot":"","sources":["../src/myers-diff.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEpD,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE,QAAQ,CAAA;IACd,KAAK,EAAE,CAAC,EAAE,CAAA;CACX;AAED,wBAAgB,SAAS,CAAC,CAAC,EACzB,CAAC,EAAE,CAAC,EAAE,EACN,CAAC,EAAE,CAAC,EAAE,EACN,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,CA+Cb"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "milkdown-inline-diff",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A Milkdown plugin for inline diff visualization and merge functionality",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"milkdown",
|
|
7
|
+
"markdown",
|
|
8
|
+
"diff",
|
|
9
|
+
"merge",
|
|
10
|
+
"editor"
|
|
11
|
+
],
|
|
12
|
+
"author": "张斌",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./style.css": "./src/style.css"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "vite build",
|
|
32
|
+
"dev": "vite build --watch",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@milkdown/core": "^7.18.0",
|
|
38
|
+
"@milkdown/ctx": "^7.18.0",
|
|
39
|
+
"@milkdown/preset-gfm": "^7.18.0",
|
|
40
|
+
"@milkdown/prose": "^7.18.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@milkdown/crepe": "^7.19.2",
|
|
44
|
+
"@milkdown/ctx": "^7.18.0",
|
|
45
|
+
"@milkdown/kit": "^7.19.2",
|
|
46
|
+
"@milkdown/prose": "^7.18.0",
|
|
47
|
+
"@milkdown/react": "^7.19.2",
|
|
48
|
+
"@prosemirror-adapter/react": "^0.5.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@milkdown/core": "^7.18.0",
|
|
52
|
+
"@milkdown/preset-gfm": "^7.18.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vite": "^5.0.0",
|
|
55
|
+
"vite-plugin-dts": "^4.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Ctx } from "@milkdown/ctx";
|
|
2
|
+
import { $ctx } from "@milkdown/kit/utils";
|
|
3
|
+
|
|
4
|
+
export interface DiffConfig {
|
|
5
|
+
acceptButtonTitle?: string;
|
|
6
|
+
rejectButtonTitle?: string;
|
|
7
|
+
originContent?: string;
|
|
8
|
+
modifiedContent?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const defaultDiffConfig: DiffConfig = {
|
|
12
|
+
acceptButtonTitle: "Accept",
|
|
13
|
+
rejectButtonTitle: "Reject",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const diffConfigCtx = $ctx<DiffConfig, "diffInlineConfig">(
|
|
17
|
+
defaultDiffConfig,
|
|
18
|
+
"diffInlineConfig",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export function diffConfig(config: DiffConfig = {}) {
|
|
22
|
+
return (ctx: Ctx) => {
|
|
23
|
+
ctx.update(diffConfigCtx.key, (prev) => ({
|
|
24
|
+
...prev,
|
|
25
|
+
...config,
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { Ctx, MilkdownPlugin } from "@milkdown/ctx";
|
|
2
|
+
import { tooltipFactory, TooltipProvider } from "@milkdown/kit/plugin/tooltip";
|
|
3
|
+
import { $ctx } from "@milkdown/kit/utils";
|
|
4
|
+
import type { EditorState, PluginView } from "@milkdown/prose/state";
|
|
5
|
+
import type { EditorView } from "@milkdown/prose/view";
|
|
6
|
+
import { Decoration } from "@milkdown/prose/view";
|
|
7
|
+
|
|
8
|
+
import { debounce } from "lodash";
|
|
9
|
+
|
|
10
|
+
import { defaultDiffConfig, diffConfigCtx } from "./diff-config";
|
|
11
|
+
import { getDiffDecorations, merge } from "./markdown-diff";
|
|
12
|
+
|
|
13
|
+
export const onMergeCtx = $ctx<
|
|
14
|
+
((action: "accept" | "reject", index: number) => void) | undefined,
|
|
15
|
+
"onMergeCtx"
|
|
16
|
+
>(undefined, "onMergeCtx");
|
|
17
|
+
|
|
18
|
+
export const diffToolTooltip = tooltipFactory("MERGE_TOOL");
|
|
19
|
+
|
|
20
|
+
export class DiffTooltipView implements PluginView {
|
|
21
|
+
private tooltipProvider: TooltipProvider;
|
|
22
|
+
private showRect?: DOMRect;
|
|
23
|
+
private shouldShow = false;
|
|
24
|
+
private currentGroupIndex?: number;
|
|
25
|
+
private currentPos?: number;
|
|
26
|
+
private currentDisplayPos?: number;
|
|
27
|
+
private scrollTarget: EventTarget | null = null;
|
|
28
|
+
|
|
29
|
+
tooltipElement: HTMLDivElement;
|
|
30
|
+
acceptButton: HTMLButtonElement;
|
|
31
|
+
rejectButton: HTMLButtonElement;
|
|
32
|
+
|
|
33
|
+
constructor(private ctx: Ctx, private editView: EditorView) {
|
|
34
|
+
this.tooltipElement = document.createElement("div");
|
|
35
|
+
this.tooltipElement.className = "milkdown-merge-tool-tooltip";
|
|
36
|
+
|
|
37
|
+
const buttonContainer = document.createElement("div");
|
|
38
|
+
buttonContainer.className = "milkdown-merge-tool-button-container";
|
|
39
|
+
|
|
40
|
+
const config = ctx.get(diffConfigCtx.key);
|
|
41
|
+
const acceptButtonTitle =
|
|
42
|
+
config.acceptButtonTitle ?? defaultDiffConfig.acceptButtonTitle ?? "Accept";
|
|
43
|
+
const rejectButtonTitle =
|
|
44
|
+
config.rejectButtonTitle ?? defaultDiffConfig.rejectButtonTitle ?? "Reject";
|
|
45
|
+
|
|
46
|
+
this.acceptButton = document.createElement("button");
|
|
47
|
+
this.acceptButton.textContent = acceptButtonTitle;
|
|
48
|
+
this.acceptButton.className = "milkdown-merge-tool-button accept";
|
|
49
|
+
this.acceptButton.addEventListener("click", () => this.handleAccept());
|
|
50
|
+
|
|
51
|
+
this.rejectButton = document.createElement("button");
|
|
52
|
+
this.rejectButton.textContent = rejectButtonTitle;
|
|
53
|
+
this.rejectButton.className = "milkdown-merge-tool-button reject";
|
|
54
|
+
this.rejectButton.addEventListener("click", () => this.handleReject());
|
|
55
|
+
|
|
56
|
+
buttonContainer.appendChild(this.rejectButton);
|
|
57
|
+
buttonContainer.appendChild(this.acceptButton);
|
|
58
|
+
this.tooltipElement.appendChild(buttonContainer);
|
|
59
|
+
|
|
60
|
+
this.tooltipProvider = new TooltipProvider({
|
|
61
|
+
content: this.tooltipElement,
|
|
62
|
+
offset: -2,
|
|
63
|
+
shouldShow: (_view: EditorView) => false,
|
|
64
|
+
floatingUIOptions: {
|
|
65
|
+
placement: "top-end",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const container = editView.dom.parentElement ?? document.body;
|
|
70
|
+
container.appendChild(this.tooltipElement);
|
|
71
|
+
|
|
72
|
+
const scrollContext = findScrollContainer(editView.dom);
|
|
73
|
+
this.scrollTarget = scrollContext.target;
|
|
74
|
+
|
|
75
|
+
if (this.scrollTarget) {
|
|
76
|
+
this.scrollTarget.addEventListener("scroll", this.handleScroll);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handleScroll = () => {
|
|
81
|
+
if (this.shouldShow && this.currentDisplayPos !== undefined) {
|
|
82
|
+
updateTooltipForPos(
|
|
83
|
+
this,
|
|
84
|
+
this.ctx,
|
|
85
|
+
this.editView,
|
|
86
|
+
this.currentDisplayPos,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
handleAccept(): void {
|
|
92
|
+
const callback = this.ctx.get(onMergeCtx.key);
|
|
93
|
+
callback?.("accept", this.currentGroupIndex!);
|
|
94
|
+
this.hideImmediate();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
handleReject(): void {
|
|
98
|
+
const callback = this.ctx.get(onMergeCtx.key);
|
|
99
|
+
callback?.("reject", this.currentGroupIndex!);
|
|
100
|
+
this.hideImmediate();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
toggleShow() {
|
|
104
|
+
if (this.shouldShow && this.showRect) {
|
|
105
|
+
this.tooltipProvider.show(
|
|
106
|
+
{ getBoundingClientRect: () => this.showRect! },
|
|
107
|
+
this.editView,
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
this.tooltipProvider.hide();
|
|
111
|
+
this.currentGroupIndex = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
show(groupIndex: number, rect: DOMRect, pos?: number) {
|
|
116
|
+
this.currentGroupIndex = groupIndex;
|
|
117
|
+
this.showRect = rect;
|
|
118
|
+
this.shouldShow = true;
|
|
119
|
+
if (pos !== undefined) {
|
|
120
|
+
this.currentDisplayPos = pos;
|
|
121
|
+
}
|
|
122
|
+
this.toggleShow();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
hide = () => {
|
|
126
|
+
this.shouldShow = false;
|
|
127
|
+
this.toggleShow();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
hideImmediate = () => {
|
|
131
|
+
this.shouldShow = false;
|
|
132
|
+
this.tooltipProvider.hide();
|
|
133
|
+
this.currentGroupIndex = undefined;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
isVisible(): boolean {
|
|
137
|
+
return this.shouldShow;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
update(view: EditorView, _prevState?: EditorState) {
|
|
141
|
+
const editState = view.state;
|
|
142
|
+
const selection = editState.selection;
|
|
143
|
+
|
|
144
|
+
const currentPos = selection.from;
|
|
145
|
+
if (this.currentPos !== currentPos) {
|
|
146
|
+
this.currentPos = currentPos;
|
|
147
|
+
updateTooltipForPos(this, this.ctx, view, currentPos);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
destroy = () => {
|
|
152
|
+
if (this.scrollTarget) {
|
|
153
|
+
this.scrollTarget.removeEventListener("scroll", this.handleScroll);
|
|
154
|
+
}
|
|
155
|
+
this.tooltipProvider.destroy();
|
|
156
|
+
this.tooltipElement.remove();
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const mergeToolPlugin: MilkdownPlugin = (ctx) => {
|
|
161
|
+
ctx.inject(onMergeCtx.key);
|
|
162
|
+
|
|
163
|
+
return async () => {
|
|
164
|
+
return () => {
|
|
165
|
+
ctx.remove(onMergeCtx.key);
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const diffTooltipPlugins: MilkdownPlugin[] = [
|
|
171
|
+
mergeToolPlugin,
|
|
172
|
+
...diffToolTooltip,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
export const tooltipViewConfig = (ctx: Ctx) => {
|
|
176
|
+
let tooltipView: DiffTooltipView | null = null;
|
|
177
|
+
|
|
178
|
+
const onMouseMove = debounce((view: EditorView, event: MouseEvent) => {
|
|
179
|
+
if (!tooltipView) return;
|
|
180
|
+
|
|
181
|
+
const pos = mouseToPos(view, event);
|
|
182
|
+
if (pos === undefined) {
|
|
183
|
+
tooltipView.hide();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
updateTooltipForPos(tooltipView, ctx, view, pos);
|
|
188
|
+
}, 0);
|
|
189
|
+
|
|
190
|
+
ctx.set(onMergeCtx.key, (action, index) => {
|
|
191
|
+
merge(ctx, action, index, false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
ctx.set(diffToolTooltip.key, {
|
|
195
|
+
props: {
|
|
196
|
+
handleDOMEvents: {
|
|
197
|
+
mousemove: onMouseMove,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
view: (view) => {
|
|
201
|
+
tooltipView = new DiffTooltipView(ctx, view);
|
|
202
|
+
return tooltipView;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
function mouseToPos(view: EditorView, event: MouseEvent): number | undefined {
|
|
208
|
+
const $pos = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
209
|
+
return $pos?.pos;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isScrollableElement(element: HTMLElement): boolean {
|
|
213
|
+
const style = window.getComputedStyle(element);
|
|
214
|
+
const overflowY = style.overflowY;
|
|
215
|
+
const overflowX = style.overflowX;
|
|
216
|
+
const overflow = style.overflow;
|
|
217
|
+
const canScrollY =
|
|
218
|
+
/(auto|scroll|overlay)/.test(overflowY) ||
|
|
219
|
+
/(auto|scroll|overlay)/.test(overflow);
|
|
220
|
+
const canScrollX =
|
|
221
|
+
/(auto|scroll|overlay)/.test(overflowX) ||
|
|
222
|
+
/(auto|scroll|overlay)/.test(overflow);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
(canScrollY && element.scrollHeight > element.clientHeight) ||
|
|
226
|
+
(canScrollX && element.scrollWidth > element.clientWidth)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findScrollContainer(start: HTMLElement): {
|
|
231
|
+
element: HTMLElement | null;
|
|
232
|
+
target: EventTarget | null;
|
|
233
|
+
} {
|
|
234
|
+
let current: HTMLElement | null = start.parentElement;
|
|
235
|
+
|
|
236
|
+
while (current) {
|
|
237
|
+
if (isScrollableElement(current)) {
|
|
238
|
+
return {
|
|
239
|
+
element: current,
|
|
240
|
+
target: current,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
current = current.parentElement;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const scrollingElement =
|
|
247
|
+
document.scrollingElement instanceof HTMLElement
|
|
248
|
+
? document.scrollingElement
|
|
249
|
+
: document.documentElement;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
element: scrollingElement,
|
|
253
|
+
target: window,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function shouldShowMergeToolForPos(
|
|
258
|
+
ctx: Ctx,
|
|
259
|
+
pos: number,
|
|
260
|
+
): { index: number; decoration: Decoration } | undefined {
|
|
261
|
+
const diffDecorations = getDiffDecorations(ctx);
|
|
262
|
+
|
|
263
|
+
const index = diffDecorations.findIndex((dec) => {
|
|
264
|
+
const all = [...dec.delete, ...dec.insert];
|
|
265
|
+
const start = all[0].from;
|
|
266
|
+
const end = all[all.length - 1].to;
|
|
267
|
+
return start <= pos && pos <= end;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (index >= 0) {
|
|
271
|
+
const group = diffDecorations[index];
|
|
272
|
+
const decoration = group.delete[0] || group.insert[0];
|
|
273
|
+
return { index, decoration };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function updateTooltipForPos(
|
|
280
|
+
tooltipView: DiffTooltipView,
|
|
281
|
+
ctx: Ctx,
|
|
282
|
+
view: EditorView,
|
|
283
|
+
pos: number,
|
|
284
|
+
): void {
|
|
285
|
+
const diffDecorations = getDiffDecorations(ctx);
|
|
286
|
+
if (diffDecorations.length === 0) {
|
|
287
|
+
tooltipView.hideImmediate();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = shouldShowMergeToolForPos(ctx, pos);
|
|
292
|
+
|
|
293
|
+
if (result) {
|
|
294
|
+
const { index, decoration } = result;
|
|
295
|
+
const domPos = (decoration.spec as any)?.offset
|
|
296
|
+
? decoration.from
|
|
297
|
+
: decoration.from + 1;
|
|
298
|
+
const dom = view.domAtPos(domPos).node as HTMLElement;
|
|
299
|
+
const domRect = dom.getBoundingClientRect();
|
|
300
|
+
tooltipView.show(index, domRect, pos);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from "@milkdown/prose/state";
|
|
2
|
+
import { DecorationSet } from "@milkdown/prose/view";
|
|
3
|
+
import { $ctx, $prose } from "@milkdown/kit/utils";
|
|
4
|
+
|
|
5
|
+
export interface DiffDecorationState {
|
|
6
|
+
decorations: DecorationSet | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const diffDecorationState = $ctx<
|
|
10
|
+
DiffDecorationState,
|
|
11
|
+
"diffDecorationState"
|
|
12
|
+
>(
|
|
13
|
+
{
|
|
14
|
+
decorations: null,
|
|
15
|
+
},
|
|
16
|
+
"diffDecorationState",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const diffDecorationPlugin = $prose((ctx) => {
|
|
20
|
+
const pluginKey = new PluginKey("MILKDOWN_DIFF_DECORATION");
|
|
21
|
+
ctx.inject(diffDecorationState.key);
|
|
22
|
+
return new Plugin({
|
|
23
|
+
key: pluginKey,
|
|
24
|
+
state: {
|
|
25
|
+
init() {
|
|
26
|
+
return DecorationSet.empty;
|
|
27
|
+
},
|
|
28
|
+
apply(tr, oldDeco) {
|
|
29
|
+
const ctxDeco = ctx.get(diffDecorationState.key).decorations;
|
|
30
|
+
|
|
31
|
+
if (ctxDeco && ctxDeco !== oldDeco) {
|
|
32
|
+
ctx.set(diffDecorationState.key, { decorations: ctxDeco });
|
|
33
|
+
return ctxDeco;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let rs = oldDeco.map(tr.mapping, tr.doc);
|
|
37
|
+
|
|
38
|
+
ctx.set(diffDecorationState.key, { decorations: rs });
|
|
39
|
+
return rs;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
props: {
|
|
43
|
+
decorations(state) {
|
|
44
|
+
return this.getState(state);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { tableSchema as officialTableSchema } from "@milkdown/preset-gfm";
|
|
2
|
+
|
|
3
|
+
export const extendedTableSchema = officialTableSchema.extendSchema((prev) => {
|
|
4
|
+
return (ctx) => {
|
|
5
|
+
const baseSchema = prev(ctx);
|
|
6
|
+
return {
|
|
7
|
+
...baseSchema,
|
|
8
|
+
content: "table_header_row+ table_row+",
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConfigReady, EditorViewReady } from "@milkdown/core";
|
|
2
|
+
import type { MilkdownPlugin } from "@milkdown/ctx";
|
|
3
|
+
|
|
4
|
+
import { diffConfigCtx, type DiffConfig, diffConfig } from "./diff-config";
|
|
5
|
+
import { diffDecorationPlugin } from "./diffDecorationState";
|
|
6
|
+
import { extendedTableSchema } from "./extended-table-schema";
|
|
7
|
+
import {
|
|
8
|
+
diffTooltipPlugins,
|
|
9
|
+
tooltipViewConfig,
|
|
10
|
+
} from "./diff-tooltip";
|
|
11
|
+
import { diff, getDiffState, jumpTo, merge } from "./markdown-diff";
|
|
12
|
+
|
|
13
|
+
const extendedTablePlugins =
|
|
14
|
+
extendedTableSchema as unknown as MilkdownPlugin[];
|
|
15
|
+
|
|
16
|
+
const diffAutoApplyPlugin: MilkdownPlugin = (ctx) => async () => {
|
|
17
|
+
await ctx.wait(ConfigReady);
|
|
18
|
+
await ctx.wait(EditorViewReady);
|
|
19
|
+
|
|
20
|
+
const { originContent, modifiedContent } = ctx.get(diffConfigCtx.key);
|
|
21
|
+
if (originContent !== undefined && modifiedContent !== undefined) {
|
|
22
|
+
diff(ctx, modifiedContent, originContent);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const diffTooltipConfigPlugin: MilkdownPlugin = (ctx) => {
|
|
27
|
+
tooltipViewConfig(ctx);
|
|
28
|
+
return async () => {};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const diffPlugins: MilkdownPlugin[] = [
|
|
32
|
+
diffConfigCtx,
|
|
33
|
+
...extendedTablePlugins,
|
|
34
|
+
diffDecorationPlugin,
|
|
35
|
+
...diffTooltipPlugins,
|
|
36
|
+
diffTooltipConfigPlugin,
|
|
37
|
+
diffAutoApplyPlugin,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const diffPlugIns = diffPlugins;
|
|
41
|
+
|
|
42
|
+
export type { DiffConfig };
|
|
43
|
+
export { diffConfig, diff, getDiffState, jumpTo, merge };
|