@jvs-milkdown/crepe 1.2.12 → 1.2.14
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/lib/cjs/builder.js +41 -2
- package/lib/cjs/builder.js.map +1 -1
- package/lib/cjs/feature/block-edit/index.js +10 -2
- package/lib/cjs/feature/block-edit/index.js.map +1 -1
- package/lib/cjs/feature/code-mirror/index.js +9 -2
- package/lib/cjs/feature/code-mirror/index.js.map +1 -1
- package/lib/cjs/feature/cursor/index.js +9 -2
- package/lib/cjs/feature/cursor/index.js.map +1 -1
- package/lib/cjs/feature/image-block/index.js +10 -3
- package/lib/cjs/feature/image-block/index.js.map +1 -1
- package/lib/cjs/feature/inline-diff/index.js +1298 -0
- package/lib/cjs/feature/inline-diff/index.js.map +1 -0
- package/lib/cjs/feature/latex/index.js +9 -2
- package/lib/cjs/feature/latex/index.js.map +1 -1
- package/lib/cjs/feature/link-tooltip/index.js +10 -2
- package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
- package/lib/cjs/feature/list-item/index.js +9 -2
- package/lib/cjs/feature/list-item/index.js.map +1 -1
- package/lib/cjs/feature/placeholder/index.js +9 -2
- package/lib/cjs/feature/placeholder/index.js.map +1 -1
- package/lib/cjs/feature/table/index.js +10 -2
- package/lib/cjs/feature/table/index.js.map +1 -1
- package/lib/cjs/feature/toolbar/index.js +134 -12
- package/lib/cjs/feature/toolbar/index.js.map +1 -1
- package/lib/cjs/index.js +1410 -241
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/builder.js +41 -2
- package/lib/esm/builder.js.map +1 -1
- package/lib/esm/feature/block-edit/index.js +10 -2
- package/lib/esm/feature/block-edit/index.js.map +1 -1
- package/lib/esm/feature/code-mirror/index.js +9 -2
- package/lib/esm/feature/code-mirror/index.js.map +1 -1
- package/lib/esm/feature/cursor/index.js +9 -2
- package/lib/esm/feature/cursor/index.js.map +1 -1
- package/lib/esm/feature/image-block/index.js +10 -3
- package/lib/esm/feature/image-block/index.js.map +1 -1
- package/lib/esm/feature/inline-diff/index.js +1274 -0
- package/lib/esm/feature/inline-diff/index.js.map +1 -0
- package/lib/esm/feature/latex/index.js +9 -2
- package/lib/esm/feature/latex/index.js.map +1 -1
- package/lib/esm/feature/link-tooltip/index.js +10 -2
- package/lib/esm/feature/link-tooltip/index.js.map +1 -1
- package/lib/esm/feature/list-item/index.js +9 -2
- package/lib/esm/feature/list-item/index.js.map +1 -1
- package/lib/esm/feature/placeholder/index.js +9 -2
- package/lib/esm/feature/placeholder/index.js.map +1 -1
- package/lib/esm/feature/table/index.js +10 -2
- package/lib/esm/feature/table/index.js.map +1 -1
- package/lib/esm/feature/toolbar/index.js +134 -12
- package/lib/esm/feature/toolbar/index.js.map +1 -1
- package/lib/esm/index.js +1392 -242
- package/lib/esm/index.js.map +1 -1
- package/lib/theme/common/diff-block.css +41 -0
- package/lib/theme/common/inline-diff.css +142 -0
- package/lib/theme/common/style.css +2 -0
- package/lib/theme/common/table.css +4 -4
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/core/builder.d.ts +2 -0
- package/lib/types/core/builder.d.ts.map +1 -1
- package/lib/types/core/locale.d.ts +4 -0
- package/lib/types/core/locale.d.ts.map +1 -1
- package/lib/types/feature/diff-block/index.d.ts +10 -0
- package/lib/types/feature/diff-block/index.d.ts.map +1 -0
- package/lib/types/feature/fixed-toolbar/document-header.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/index.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/menu-bar.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/outline-panel.d.ts.map +1 -1
- package/lib/types/feature/index.d.ts +7 -1
- package/lib/types/feature/index.d.ts.map +1 -1
- package/lib/types/feature/inline-diff/change-panel.d.ts +4 -0
- package/lib/types/feature/inline-diff/change-panel.d.ts.map +1 -0
- package/lib/types/feature/inline-diff/config.d.ts +12 -0
- package/lib/types/feature/inline-diff/config.d.ts.map +1 -0
- package/lib/types/feature/inline-diff/diff-engine.d.ts +20 -0
- package/lib/types/feature/inline-diff/diff-engine.d.ts.map +1 -0
- package/lib/types/feature/inline-diff/diff-view.d.ts +2 -0
- package/lib/types/feature/inline-diff/diff-view.d.ts.map +1 -0
- package/lib/types/feature/inline-diff/doc-builder.d.ts +21 -0
- package/lib/types/feature/inline-diff/doc-builder.d.ts.map +1 -0
- package/lib/types/feature/inline-diff/index.d.ts +9 -0
- package/lib/types/feature/inline-diff/index.d.ts.map +1 -0
- package/lib/types/feature/loader.d.ts.map +1 -1
- package/lib/types/feature/toolbar/component.d.ts.map +1 -1
- package/lib/types/feature/toolbar/index.d.ts.map +1 -1
- package/lib/types/icons/remove.d.ts +1 -1
- package/lib/types/icons/remove.d.ts.map +1 -1
- package/lib/types/utils/fixed-toolbar-popup-state.d.ts +7 -0
- package/lib/types/utils/fixed-toolbar-popup-state.d.ts.map +1 -0
- package/package.json +15 -4
- package/src/core/builder.ts +19 -0
- package/src/core/locale.ts +7 -0
- package/src/feature/diff-block/index.ts +48 -0
- package/src/feature/fixed-toolbar/index.ts +97 -25
- package/src/feature/fixed-toolbar/menu-bar.tsx +13 -2
- package/src/feature/fixed-toolbar/outline-panel.tsx +3 -2
- package/src/feature/fixed-toolbar/shortcut-help-modal.tsx +1 -1
- package/src/feature/fixed-toolbar/view-menu-state.ts +1 -1
- package/src/feature/image-block/index.ts +1 -1
- package/src/feature/index.ts +12 -0
- package/src/feature/inline-diff/change-panel.ts +280 -0
- package/src/feature/inline-diff/config.ts +28 -0
- package/src/feature/inline-diff/diff-engine.ts +181 -0
- package/src/feature/inline-diff/diff-view.ts +2 -0
- package/src/feature/inline-diff/doc-builder.ts +139 -0
- package/src/feature/inline-diff/index.ts +514 -0
- package/src/feature/loader.ts +8 -0
- package/src/feature/toolbar/component.tsx +97 -9
- package/src/feature/toolbar/index.ts +33 -0
- package/src/icons/remove.ts +1 -0
- package/src/theme/common/diff-block.css +43 -0
- package/src/theme/common/inline-diff.css +148 -0
- package/src/theme/common/style.css +2 -0
- package/src/theme/common/table.css +4 -4
- package/src/utils/fixed-toolbar-popup-state.ts +27 -0
|
@@ -2,12 +2,16 @@ import type { Ctx } from '@jvs-milkdown/kit/ctx'
|
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@jvs-milkdown/kit/component'
|
|
4
4
|
// @ts-ignore
|
|
5
|
-
import { defineComponent, ref, onUnmounted, computed, onMounted, h } from 'vue'
|
|
5
|
+
import { defineComponent, ref, onUnmounted, computed, onMounted, h, watch } from 'vue'
|
|
6
6
|
|
|
7
7
|
import type { FixedToolbarConfig } from './index'
|
|
8
8
|
|
|
9
9
|
import { i18n } from '../../core/locale'
|
|
10
10
|
import { menuIcon } from '../../icons'
|
|
11
|
+
import {
|
|
12
|
+
incrementPopupCount,
|
|
13
|
+
decrementPopupCount,
|
|
14
|
+
} from '../../utils/fixed-toolbar-popup-state'
|
|
11
15
|
import { randomCover } from './cover-defaults'
|
|
12
16
|
import { viewMenuStateCtx, type EditorWidth } from './view-menu-state'
|
|
13
17
|
|
|
@@ -116,12 +120,19 @@ export const MenuBar = defineComponent({
|
|
|
116
120
|
onUnmounted(() => {
|
|
117
121
|
document.removeEventListener('click', closeMenu)
|
|
118
122
|
if (rafId) cancelAnimationFrame(rafId)
|
|
123
|
+
if (showMenu.value) decrementPopupCount()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Notify floating toolbar when the menu popup opens/closes
|
|
127
|
+
watch(showMenu, (val) => {
|
|
128
|
+
if (val) incrementPopupCount()
|
|
129
|
+
else decrementPopupCount()
|
|
119
130
|
})
|
|
120
131
|
|
|
121
132
|
const viewState = computed(() => props.ctx.get(viewMenuStateCtx.key))
|
|
122
133
|
|
|
123
134
|
const bgColors = [
|
|
124
|
-
{ name: '
|
|
135
|
+
{ name: '白色', value: '#FFFFFF' },
|
|
125
136
|
{ name: '浅灰', value: '#F7F7F5' },
|
|
126
137
|
{ name: '浅棕', value: '#F4EEDB' },
|
|
127
138
|
{ name: '浅橙', value: '#FBECDD' },
|
|
@@ -283,11 +283,12 @@ export const OutlinePanel = defineComponent({
|
|
|
283
283
|
{/* Header */}
|
|
284
284
|
<div
|
|
285
285
|
style={{
|
|
286
|
-
padding: '
|
|
286
|
+
padding: '12px 16px',
|
|
287
287
|
fontWeight: 'bold',
|
|
288
288
|
fontSize: '14px',
|
|
289
289
|
color: 'var(--crepe-color-on-surface)',
|
|
290
|
-
borderBottom:
|
|
290
|
+
borderBottom:
|
|
291
|
+
'1px solid var(--crepe-color-outline-variant, color-mix(in srgb, var(--crepe-color-outline, #ddd), transparent 80%))',
|
|
291
292
|
display: 'flex',
|
|
292
293
|
justifyContent: 'space-between',
|
|
293
294
|
alignItems: 'center',
|
|
@@ -65,7 +65,7 @@ const rightGroups: ShortcutGroup[] = [
|
|
|
65
65
|
titleKey: 'shortcuts.history',
|
|
66
66
|
items: [
|
|
67
67
|
{ labelKey: 'shortcuts.undo', keys: ['Mod+z'] },
|
|
68
|
-
{ labelKey: 'shortcuts.redo', keys: ['Mod+y', 'Shift+Mod+
|
|
68
|
+
{ labelKey: 'shortcuts.redo', keys: ['Mod+y', 'Shift+Mod+Z'] },
|
|
69
69
|
],
|
|
70
70
|
},
|
|
71
71
|
]
|
package/src/feature/index.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type { AttachmentFeatureConfig } from './attachment/config'
|
|
|
2
2
|
import type { BlockEditFeatureConfig } from './block-edit'
|
|
3
3
|
import type { CodeMirrorFeatureConfig } from './code-mirror'
|
|
4
4
|
import type { CursorFeatureConfig } from './cursor'
|
|
5
|
+
import type { DiffBlockFeatureConfig } from './diff-block'
|
|
5
6
|
import type { FixedToolbarFeatureConfig } from './fixed-toolbar'
|
|
6
7
|
import type { ImageBlockFeatureConfig } from './image-block'
|
|
8
|
+
import type { InlineDiffFeatureConfig } from './inline-diff'
|
|
7
9
|
import type { LatexFeatureConfig } from './latex'
|
|
8
10
|
import type { LinkTooltipFeatureConfig } from './link-tooltip'
|
|
9
11
|
import type { ListItemFeatureConfig } from './list-item'
|
|
@@ -50,6 +52,12 @@ export enum CrepeFeature {
|
|
|
50
52
|
|
|
51
53
|
/// File attachment uploads
|
|
52
54
|
Attachment = 'attachment',
|
|
55
|
+
|
|
56
|
+
/// Inline diff comparison between two document versions, similar to Word track changes.
|
|
57
|
+
InlineDiff = 'inline-diff',
|
|
58
|
+
|
|
59
|
+
/// Diff comparison block for showing code differences
|
|
60
|
+
DiffBlock = 'diff-block',
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export interface CrepeFeatureConfig {
|
|
@@ -65,6 +73,8 @@ export interface CrepeFeatureConfig {
|
|
|
65
73
|
[CrepeFeature.Table]?: TableFeatureConfig
|
|
66
74
|
[CrepeFeature.Latex]?: LatexFeatureConfig
|
|
67
75
|
[CrepeFeature.Attachment]?: AttachmentFeatureConfig
|
|
76
|
+
[CrepeFeature.InlineDiff]?: InlineDiffFeatureConfig
|
|
77
|
+
[CrepeFeature.DiffBlock]?: DiffBlockFeatureConfig
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
export const defaultFeatures: Record<CrepeFeature, boolean> = {
|
|
@@ -80,4 +90,6 @@ export const defaultFeatures: Record<CrepeFeature, boolean> = {
|
|
|
80
90
|
[CrepeFeature.Table]: true,
|
|
81
91
|
[CrepeFeature.Latex]: true,
|
|
82
92
|
[CrepeFeature.Attachment]: true,
|
|
93
|
+
[CrepeFeature.InlineDiff]: false,
|
|
94
|
+
[CrepeFeature.DiffBlock]: true,
|
|
83
95
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { createApp, h, ref, computed, type App as VueApp } from 'vue'
|
|
2
|
+
|
|
3
|
+
import type { ChangeInfo, ChangeType } from './doc-builder'
|
|
4
|
+
|
|
5
|
+
type FilterTab = 'all' | ChangeType
|
|
6
|
+
|
|
7
|
+
const typeLabels: Record<ChangeType, string> = {
|
|
8
|
+
added: '新增',
|
|
9
|
+
removed: '删除',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const typeColors: Record<ChangeType, string> = {
|
|
13
|
+
added: '#4caf50',
|
|
14
|
+
removed: '#c0392b',
|
|
15
|
+
}
|
|
16
|
+
export function mountChangePanel(
|
|
17
|
+
container: HTMLElement,
|
|
18
|
+
changes: ChangeInfo[],
|
|
19
|
+
onNavigate: (from: number) => void,
|
|
20
|
+
onClose?: () => void
|
|
21
|
+
): VueApp {
|
|
22
|
+
const app = createApp({
|
|
23
|
+
setup() {
|
|
24
|
+
const activeTab = ref<FilterTab>('all')
|
|
25
|
+
|
|
26
|
+
const filteredChanges = computed(() => {
|
|
27
|
+
if (activeTab.value === 'all') return changes
|
|
28
|
+
return changes.filter((c) => c.type === activeTab.value)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const counts = computed(() => ({
|
|
32
|
+
all: changes.length,
|
|
33
|
+
removed: changes.filter((c) => c.type === 'removed').length,
|
|
34
|
+
added: changes.filter((c) => c.type === 'added').length,
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
function renderTab(tab: FilterTab, label: string) {
|
|
38
|
+
const isActive = activeTab.value === tab
|
|
39
|
+
return h(
|
|
40
|
+
'button',
|
|
41
|
+
{
|
|
42
|
+
style: {
|
|
43
|
+
padding: '6px 10px',
|
|
44
|
+
border: 'none',
|
|
45
|
+
background: 'none',
|
|
46
|
+
cursor: 'pointer',
|
|
47
|
+
fontSize: '12px',
|
|
48
|
+
color: isActive ? '#1890ff' : '#666',
|
|
49
|
+
borderBottom: isActive
|
|
50
|
+
? '2px solid #1890ff'
|
|
51
|
+
: '2px solid transparent',
|
|
52
|
+
},
|
|
53
|
+
onClick: () => {
|
|
54
|
+
activeTab.value = tab
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
label
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderItem(change: ChangeInfo, index: number) {
|
|
62
|
+
const color = typeColors[change.type]
|
|
63
|
+
const children: ReturnType<typeof h>[] = [
|
|
64
|
+
h('div', { style: { marginBottom: '4px' } }, [
|
|
65
|
+
h(
|
|
66
|
+
'span',
|
|
67
|
+
{
|
|
68
|
+
style: {
|
|
69
|
+
display: 'inline-block',
|
|
70
|
+
padding: '1px 6px',
|
|
71
|
+
borderRadius: '3px',
|
|
72
|
+
color: '#fff',
|
|
73
|
+
fontSize: '11px',
|
|
74
|
+
fontWeight: '500',
|
|
75
|
+
backgroundColor: color,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
`${index} ${typeLabels[change.type]}`
|
|
79
|
+
),
|
|
80
|
+
]),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
if (change.oldText) {
|
|
84
|
+
children.push(
|
|
85
|
+
h(
|
|
86
|
+
'div',
|
|
87
|
+
{
|
|
88
|
+
style: {
|
|
89
|
+
marginTop: '3px',
|
|
90
|
+
fontSize: '12px',
|
|
91
|
+
lineHeight: '1.5',
|
|
92
|
+
color: '#555',
|
|
93
|
+
wordBreak: 'break-all',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
[
|
|
97
|
+
h(
|
|
98
|
+
'span',
|
|
99
|
+
{ style: { color: '#999', fontSize: '11px' } },
|
|
100
|
+
'原文: '
|
|
101
|
+
),
|
|
102
|
+
h('span', null, change.oldText),
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
if (change.newText) {
|
|
108
|
+
children.push(
|
|
109
|
+
h(
|
|
110
|
+
'div',
|
|
111
|
+
{
|
|
112
|
+
style: {
|
|
113
|
+
marginTop: '3px',
|
|
114
|
+
fontSize: '12px',
|
|
115
|
+
lineHeight: '1.5',
|
|
116
|
+
wordBreak: 'break-all',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
[
|
|
120
|
+
h(
|
|
121
|
+
'span',
|
|
122
|
+
{ style: { color: '#999', fontSize: '11px' } },
|
|
123
|
+
'差异: '
|
|
124
|
+
),
|
|
125
|
+
h('span', { style: { color } }, change.newText),
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return h(
|
|
132
|
+
'div',
|
|
133
|
+
{
|
|
134
|
+
style: {
|
|
135
|
+
padding: '8px 10px',
|
|
136
|
+
borderRadius: '4px',
|
|
137
|
+
marginBottom: '6px',
|
|
138
|
+
background: 'rgba(0,0,0,0.02)',
|
|
139
|
+
border: '1px solid #eee',
|
|
140
|
+
cursor: change.from > 0 ? 'pointer' : 'default',
|
|
141
|
+
},
|
|
142
|
+
onClick: () => {
|
|
143
|
+
if (change.from > 0) onNavigate(change.from)
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
children
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return () =>
|
|
151
|
+
h(
|
|
152
|
+
'div',
|
|
153
|
+
{
|
|
154
|
+
style: {
|
|
155
|
+
width: '100%',
|
|
156
|
+
height: '100%',
|
|
157
|
+
display: 'flex',
|
|
158
|
+
flexDirection: 'column',
|
|
159
|
+
backgroundColor: 'var(--crepe-color-surface, #fff)',
|
|
160
|
+
boxSizing: 'border-box',
|
|
161
|
+
fontFamily:
|
|
162
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
163
|
+
fontSize: '13px',
|
|
164
|
+
color: 'var(--crepe-color-on-surface, #333)',
|
|
165
|
+
overflow: 'hidden',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
[
|
|
169
|
+
// Header
|
|
170
|
+
h(
|
|
171
|
+
'div',
|
|
172
|
+
{
|
|
173
|
+
style: {
|
|
174
|
+
padding: '12px 16px',
|
|
175
|
+
fontSize: '14px',
|
|
176
|
+
fontWeight: 'bold',
|
|
177
|
+
borderBottom:
|
|
178
|
+
'1px solid var(--crepe-color-outline-variant, color-mix(in srgb, var(--crepe-color-outline, #ddd), transparent 80%))',
|
|
179
|
+
color: 'var(--crepe-color-on-surface, #333)',
|
|
180
|
+
display: 'flex',
|
|
181
|
+
justifyContent: 'space-between',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
flexShrink: 0,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
[
|
|
187
|
+
h('span', null, '文档差异变更'),
|
|
188
|
+
onClose
|
|
189
|
+
? h(
|
|
190
|
+
'div',
|
|
191
|
+
{
|
|
192
|
+
style: {
|
|
193
|
+
cursor: 'pointer',
|
|
194
|
+
display: 'flex',
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
width: '24px',
|
|
198
|
+
height: '24px',
|
|
199
|
+
borderRadius: '4px',
|
|
200
|
+
color: 'var(--crepe-color-outline, #999)',
|
|
201
|
+
transition: 'background-color 0.2s, color 0.2s',
|
|
202
|
+
},
|
|
203
|
+
onClick: onClose,
|
|
204
|
+
onMouseenter: (e: MouseEvent) => {
|
|
205
|
+
const target = e.currentTarget as HTMLElement
|
|
206
|
+
target.style.backgroundColor =
|
|
207
|
+
'var(--crepe-color-hover, rgba(0,0,0,0.05))'
|
|
208
|
+
target.style.color =
|
|
209
|
+
'var(--crepe-color-on-surface, #333)'
|
|
210
|
+
},
|
|
211
|
+
onMouseleave: (e: MouseEvent) => {
|
|
212
|
+
const target = e.currentTarget as HTMLElement
|
|
213
|
+
target.style.backgroundColor = 'transparent'
|
|
214
|
+
target.style.color =
|
|
215
|
+
'var(--crepe-color-outline, #999)'
|
|
216
|
+
},
|
|
217
|
+
title: '关闭差异',
|
|
218
|
+
},
|
|
219
|
+
[
|
|
220
|
+
h(
|
|
221
|
+
'svg',
|
|
222
|
+
{
|
|
223
|
+
width: '16',
|
|
224
|
+
height: '16',
|
|
225
|
+
viewBox: '0 0 24 24',
|
|
226
|
+
fill: 'none',
|
|
227
|
+
stroke: 'currentColor',
|
|
228
|
+
strokeWidth: '2',
|
|
229
|
+
strokeLinecap: 'round',
|
|
230
|
+
strokeLinejoin: 'round',
|
|
231
|
+
},
|
|
232
|
+
[
|
|
233
|
+
h('line', { x1: '18', y1: '6', x2: '6', y2: '18' }),
|
|
234
|
+
h('line', { x1: '6', y1: '6', x2: '18', y2: '18' }),
|
|
235
|
+
]
|
|
236
|
+
),
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
: null,
|
|
240
|
+
]
|
|
241
|
+
),
|
|
242
|
+
// Tabs
|
|
243
|
+
h(
|
|
244
|
+
'div',
|
|
245
|
+
{
|
|
246
|
+
style: {
|
|
247
|
+
display: 'flex',
|
|
248
|
+
borderBottom: '1px solid #eee',
|
|
249
|
+
padding: '0 8px',
|
|
250
|
+
flexShrink: 0,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
[
|
|
254
|
+
renderTab('all', `全部(${counts.value.all})`),
|
|
255
|
+
renderTab('removed', `删除(${counts.value.removed})`),
|
|
256
|
+
renderTab('added', `新增(${counts.value.added})`),
|
|
257
|
+
]
|
|
258
|
+
),
|
|
259
|
+
// List
|
|
260
|
+
h(
|
|
261
|
+
'div',
|
|
262
|
+
{
|
|
263
|
+
style: {
|
|
264
|
+
flex: 1,
|
|
265
|
+
overflowY: 'auto',
|
|
266
|
+
padding: '8px',
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
filteredChanges.value.map((change, i) =>
|
|
270
|
+
renderItem(change, i + 1)
|
|
271
|
+
)
|
|
272
|
+
),
|
|
273
|
+
]
|
|
274
|
+
)
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
app.mount(container)
|
|
279
|
+
return app
|
|
280
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { $ctx } from '@jvs-milkdown/kit/utils'
|
|
2
|
+
|
|
3
|
+
export interface InlineDiffConfig {
|
|
4
|
+
addedClass: string
|
|
5
|
+
deletedClass: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface InlineDiffApi {
|
|
9
|
+
showDiff: (oldMarkdown: string, newMarkdown?: string) => void
|
|
10
|
+
hideDiff: () => void
|
|
11
|
+
isShowing: () => boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const inlineDiffConfig = $ctx(
|
|
15
|
+
{
|
|
16
|
+
addedClass: 'crepe-diff-added',
|
|
17
|
+
deletedClass: 'crepe-diff-deleted',
|
|
18
|
+
} as InlineDiffConfig,
|
|
19
|
+
'inlineDiffConfigCtx'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const noopApi: InlineDiffApi = {
|
|
23
|
+
showDiff: () => {},
|
|
24
|
+
hideDiff: () => {},
|
|
25
|
+
isShowing: () => false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const inlineDiffApiCtx = $ctx(noopApi, 'inlineDiffApiCtx')
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Node as PMNode } from '@jvs-milkdown/kit/prose/model'
|
|
2
|
+
|
|
3
|
+
import * as Diff from 'diff'
|
|
4
|
+
|
|
5
|
+
export type DiffType = 'unchanged' | 'added' | 'removed'
|
|
6
|
+
|
|
7
|
+
export interface CharPart {
|
|
8
|
+
type: 'equal' | 'insert' | 'delete'
|
|
9
|
+
value: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BlockDiff {
|
|
13
|
+
type: DiffType
|
|
14
|
+
oldNode?: PMNode
|
|
15
|
+
newNode?: PMNode
|
|
16
|
+
parts?: CharPart[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ChunkStatus = 'pending' | 'accepted' | 'rejected'
|
|
20
|
+
|
|
21
|
+
export interface DiffChunk {
|
|
22
|
+
id: string
|
|
23
|
+
diffs: BlockDiff[]
|
|
24
|
+
status: ChunkStatus
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lcs<T>(
|
|
28
|
+
a: T[],
|
|
29
|
+
b: T[],
|
|
30
|
+
eq: (x: T, y: T) => boolean
|
|
31
|
+
): [number, number][] {
|
|
32
|
+
const m = a.length
|
|
33
|
+
const n = b.length
|
|
34
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
|
35
|
+
Array.from({ length: n + 1 }, () => 0)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
39
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
40
|
+
const diag = dp[i + 1]?.[j + 1] ?? 0
|
|
41
|
+
const down = dp[i + 1]?.[j] ?? 0
|
|
42
|
+
const right = dp[i]?.[j + 1] ?? 0
|
|
43
|
+
dp[i]![j] = eq(a[i]!, b[j]!) ? 1 + diag : Math.max(down, right)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pairs: [number, number][] = []
|
|
48
|
+
let i = 0
|
|
49
|
+
let j = 0
|
|
50
|
+
while (i < m && j < n) {
|
|
51
|
+
const down = dp[i + 1]?.[j] ?? 0
|
|
52
|
+
const right = dp[i]?.[j + 1] ?? 0
|
|
53
|
+
if (eq(a[i]!, b[j]!)) {
|
|
54
|
+
pairs.push([i, j])
|
|
55
|
+
i++
|
|
56
|
+
j++
|
|
57
|
+
} else if (down >= right) {
|
|
58
|
+
i++
|
|
59
|
+
} else {
|
|
60
|
+
j++
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return pairs
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function computeDiff(
|
|
67
|
+
oldBlocks: PMNode[],
|
|
68
|
+
newBlocks: PMNode[]
|
|
69
|
+
): DiffChunk[] {
|
|
70
|
+
const result: DiffChunk[] = []
|
|
71
|
+
let chunkCounter = 0
|
|
72
|
+
|
|
73
|
+
const pushChunk = (diffs: BlockDiff[]) => {
|
|
74
|
+
if (diffs.length > 0) {
|
|
75
|
+
result.push({
|
|
76
|
+
id: `chunk-${chunkCounter++}`,
|
|
77
|
+
diffs,
|
|
78
|
+
status: 'pending',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pairs = lcs(
|
|
84
|
+
oldBlocks,
|
|
85
|
+
newBlocks,
|
|
86
|
+
(a, b) => a.textContent === b.textContent
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
let oi = 0
|
|
90
|
+
let ni = 0
|
|
91
|
+
|
|
92
|
+
for (const [pi, pj] of pairs) {
|
|
93
|
+
// Pair up unmatched blocks in the gap before this LCS match
|
|
94
|
+
const gapOld = pi - oi
|
|
95
|
+
const gapNew = pj - ni
|
|
96
|
+
const gapMax = Math.max(gapOld, gapNew)
|
|
97
|
+
|
|
98
|
+
for (let k = 0; k < gapMax; k++) {
|
|
99
|
+
const hasOld = oi + k < pi
|
|
100
|
+
const hasNew = ni + k < pj
|
|
101
|
+
const currentDiffs: BlockDiff[] = []
|
|
102
|
+
|
|
103
|
+
if (hasOld && hasNew) {
|
|
104
|
+
const oldNode = oldBlocks[oi + k]!
|
|
105
|
+
const newNode = newBlocks[ni + k]!
|
|
106
|
+
const changes = Diff.diffWordsWithSpace(
|
|
107
|
+
oldNode.textContent,
|
|
108
|
+
newNode.textContent
|
|
109
|
+
)
|
|
110
|
+
const parts: CharPart[] = changes.map((c) => ({
|
|
111
|
+
type: c.added ? 'insert' : c.removed ? 'delete' : 'equal',
|
|
112
|
+
value: c.value,
|
|
113
|
+
}))
|
|
114
|
+
currentDiffs.push({ type: 'removed', oldNode, parts })
|
|
115
|
+
currentDiffs.push({ type: 'added', newNode, parts })
|
|
116
|
+
} else if (hasOld) {
|
|
117
|
+
currentDiffs.push({ type: 'removed', oldNode: oldBlocks[oi + k]! })
|
|
118
|
+
} else {
|
|
119
|
+
currentDiffs.push({ type: 'added', newNode: newBlocks[ni + k]! })
|
|
120
|
+
}
|
|
121
|
+
pushChunk(currentDiffs)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// LCS match itself
|
|
125
|
+
const oldNode = oldBlocks[pi]!
|
|
126
|
+
const newNode = newBlocks[pj]!
|
|
127
|
+
|
|
128
|
+
if (oldNode.textContent === newNode.textContent && oldNode.eq(newNode)) {
|
|
129
|
+
pushChunk([{ type: 'unchanged', oldNode, newNode }])
|
|
130
|
+
} else {
|
|
131
|
+
const changes = Diff.diffWordsWithSpace(
|
|
132
|
+
oldNode.textContent,
|
|
133
|
+
newNode.textContent
|
|
134
|
+
)
|
|
135
|
+
const parts: CharPart[] = changes.map((c) => ({
|
|
136
|
+
type: c.added ? 'insert' : c.removed ? 'delete' : 'equal',
|
|
137
|
+
value: c.value,
|
|
138
|
+
}))
|
|
139
|
+
pushChunk([
|
|
140
|
+
{ type: 'removed', oldNode, parts },
|
|
141
|
+
{ type: 'added', newNode, parts },
|
|
142
|
+
])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
oi = pi + 1
|
|
146
|
+
ni = pj + 1
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Trailing gap
|
|
150
|
+
const trailOld = oldBlocks.length - oi
|
|
151
|
+
const trailNew = newBlocks.length - ni
|
|
152
|
+
const trailMax = Math.max(trailOld, trailNew)
|
|
153
|
+
|
|
154
|
+
for (let k = 0; k < trailMax; k++) {
|
|
155
|
+
const hasOld = oi + k < oldBlocks.length
|
|
156
|
+
const hasNew = ni + k < newBlocks.length
|
|
157
|
+
const currentDiffs: BlockDiff[] = []
|
|
158
|
+
|
|
159
|
+
if (hasOld && hasNew) {
|
|
160
|
+
const oldNode = oldBlocks[oi + k]!
|
|
161
|
+
const newNode = newBlocks[ni + k]!
|
|
162
|
+
const changes = Diff.diffWordsWithSpace(
|
|
163
|
+
oldNode.textContent,
|
|
164
|
+
newNode.textContent
|
|
165
|
+
)
|
|
166
|
+
const parts: CharPart[] = changes.map((c) => ({
|
|
167
|
+
type: c.added ? 'insert' : c.removed ? 'delete' : 'equal',
|
|
168
|
+
value: c.value,
|
|
169
|
+
}))
|
|
170
|
+
currentDiffs.push({ type: 'removed', oldNode, parts })
|
|
171
|
+
currentDiffs.push({ type: 'added', newNode, parts })
|
|
172
|
+
} else if (hasOld) {
|
|
173
|
+
currentDiffs.push({ type: 'removed', oldNode: oldBlocks[oi + k]! })
|
|
174
|
+
} else {
|
|
175
|
+
currentDiffs.push({ type: 'added', newNode: newBlocks[ni + k]! })
|
|
176
|
+
}
|
|
177
|
+
pushChunk(currentDiffs)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
}
|