@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.
Files changed (114) hide show
  1. package/lib/cjs/builder.js +41 -2
  2. package/lib/cjs/builder.js.map +1 -1
  3. package/lib/cjs/feature/block-edit/index.js +10 -2
  4. package/lib/cjs/feature/block-edit/index.js.map +1 -1
  5. package/lib/cjs/feature/code-mirror/index.js +9 -2
  6. package/lib/cjs/feature/code-mirror/index.js.map +1 -1
  7. package/lib/cjs/feature/cursor/index.js +9 -2
  8. package/lib/cjs/feature/cursor/index.js.map +1 -1
  9. package/lib/cjs/feature/image-block/index.js +10 -3
  10. package/lib/cjs/feature/image-block/index.js.map +1 -1
  11. package/lib/cjs/feature/inline-diff/index.js +1298 -0
  12. package/lib/cjs/feature/inline-diff/index.js.map +1 -0
  13. package/lib/cjs/feature/latex/index.js +9 -2
  14. package/lib/cjs/feature/latex/index.js.map +1 -1
  15. package/lib/cjs/feature/link-tooltip/index.js +10 -2
  16. package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
  17. package/lib/cjs/feature/list-item/index.js +9 -2
  18. package/lib/cjs/feature/list-item/index.js.map +1 -1
  19. package/lib/cjs/feature/placeholder/index.js +9 -2
  20. package/lib/cjs/feature/placeholder/index.js.map +1 -1
  21. package/lib/cjs/feature/table/index.js +10 -2
  22. package/lib/cjs/feature/table/index.js.map +1 -1
  23. package/lib/cjs/feature/toolbar/index.js +134 -12
  24. package/lib/cjs/feature/toolbar/index.js.map +1 -1
  25. package/lib/cjs/index.js +1410 -241
  26. package/lib/cjs/index.js.map +1 -1
  27. package/lib/esm/builder.js +41 -2
  28. package/lib/esm/builder.js.map +1 -1
  29. package/lib/esm/feature/block-edit/index.js +10 -2
  30. package/lib/esm/feature/block-edit/index.js.map +1 -1
  31. package/lib/esm/feature/code-mirror/index.js +9 -2
  32. package/lib/esm/feature/code-mirror/index.js.map +1 -1
  33. package/lib/esm/feature/cursor/index.js +9 -2
  34. package/lib/esm/feature/cursor/index.js.map +1 -1
  35. package/lib/esm/feature/image-block/index.js +10 -3
  36. package/lib/esm/feature/image-block/index.js.map +1 -1
  37. package/lib/esm/feature/inline-diff/index.js +1274 -0
  38. package/lib/esm/feature/inline-diff/index.js.map +1 -0
  39. package/lib/esm/feature/latex/index.js +9 -2
  40. package/lib/esm/feature/latex/index.js.map +1 -1
  41. package/lib/esm/feature/link-tooltip/index.js +10 -2
  42. package/lib/esm/feature/link-tooltip/index.js.map +1 -1
  43. package/lib/esm/feature/list-item/index.js +9 -2
  44. package/lib/esm/feature/list-item/index.js.map +1 -1
  45. package/lib/esm/feature/placeholder/index.js +9 -2
  46. package/lib/esm/feature/placeholder/index.js.map +1 -1
  47. package/lib/esm/feature/table/index.js +10 -2
  48. package/lib/esm/feature/table/index.js.map +1 -1
  49. package/lib/esm/feature/toolbar/index.js +134 -12
  50. package/lib/esm/feature/toolbar/index.js.map +1 -1
  51. package/lib/esm/index.js +1392 -242
  52. package/lib/esm/index.js.map +1 -1
  53. package/lib/theme/common/diff-block.css +41 -0
  54. package/lib/theme/common/inline-diff.css +142 -0
  55. package/lib/theme/common/style.css +2 -0
  56. package/lib/theme/common/table.css +4 -4
  57. package/lib/tsconfig.tsbuildinfo +1 -1
  58. package/lib/types/core/builder.d.ts +2 -0
  59. package/lib/types/core/builder.d.ts.map +1 -1
  60. package/lib/types/core/locale.d.ts +4 -0
  61. package/lib/types/core/locale.d.ts.map +1 -1
  62. package/lib/types/feature/diff-block/index.d.ts +10 -0
  63. package/lib/types/feature/diff-block/index.d.ts.map +1 -0
  64. package/lib/types/feature/fixed-toolbar/document-header.d.ts.map +1 -1
  65. package/lib/types/feature/fixed-toolbar/index.d.ts.map +1 -1
  66. package/lib/types/feature/fixed-toolbar/menu-bar.d.ts.map +1 -1
  67. package/lib/types/feature/fixed-toolbar/outline-panel.d.ts.map +1 -1
  68. package/lib/types/feature/index.d.ts +7 -1
  69. package/lib/types/feature/index.d.ts.map +1 -1
  70. package/lib/types/feature/inline-diff/change-panel.d.ts +4 -0
  71. package/lib/types/feature/inline-diff/change-panel.d.ts.map +1 -0
  72. package/lib/types/feature/inline-diff/config.d.ts +12 -0
  73. package/lib/types/feature/inline-diff/config.d.ts.map +1 -0
  74. package/lib/types/feature/inline-diff/diff-engine.d.ts +20 -0
  75. package/lib/types/feature/inline-diff/diff-engine.d.ts.map +1 -0
  76. package/lib/types/feature/inline-diff/diff-view.d.ts +2 -0
  77. package/lib/types/feature/inline-diff/diff-view.d.ts.map +1 -0
  78. package/lib/types/feature/inline-diff/doc-builder.d.ts +21 -0
  79. package/lib/types/feature/inline-diff/doc-builder.d.ts.map +1 -0
  80. package/lib/types/feature/inline-diff/index.d.ts +9 -0
  81. package/lib/types/feature/inline-diff/index.d.ts.map +1 -0
  82. package/lib/types/feature/loader.d.ts.map +1 -1
  83. package/lib/types/feature/toolbar/component.d.ts.map +1 -1
  84. package/lib/types/feature/toolbar/index.d.ts.map +1 -1
  85. package/lib/types/icons/remove.d.ts +1 -1
  86. package/lib/types/icons/remove.d.ts.map +1 -1
  87. package/lib/types/utils/fixed-toolbar-popup-state.d.ts +7 -0
  88. package/lib/types/utils/fixed-toolbar-popup-state.d.ts.map +1 -0
  89. package/package.json +15 -4
  90. package/src/core/builder.ts +19 -0
  91. package/src/core/locale.ts +7 -0
  92. package/src/feature/diff-block/index.ts +48 -0
  93. package/src/feature/fixed-toolbar/index.ts +97 -25
  94. package/src/feature/fixed-toolbar/menu-bar.tsx +13 -2
  95. package/src/feature/fixed-toolbar/outline-panel.tsx +3 -2
  96. package/src/feature/fixed-toolbar/shortcut-help-modal.tsx +1 -1
  97. package/src/feature/fixed-toolbar/view-menu-state.ts +1 -1
  98. package/src/feature/image-block/index.ts +1 -1
  99. package/src/feature/index.ts +12 -0
  100. package/src/feature/inline-diff/change-panel.ts +280 -0
  101. package/src/feature/inline-diff/config.ts +28 -0
  102. package/src/feature/inline-diff/diff-engine.ts +181 -0
  103. package/src/feature/inline-diff/diff-view.ts +2 -0
  104. package/src/feature/inline-diff/doc-builder.ts +139 -0
  105. package/src/feature/inline-diff/index.ts +514 -0
  106. package/src/feature/loader.ts +8 -0
  107. package/src/feature/toolbar/component.tsx +97 -9
  108. package/src/feature/toolbar/index.ts +33 -0
  109. package/src/icons/remove.ts +1 -0
  110. package/src/theme/common/diff-block.css +43 -0
  111. package/src/theme/common/inline-diff.css +148 -0
  112. package/src/theme/common/style.css +2 -0
  113. package/src/theme/common/table.css +4 -4
  114. 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: '默认', value: null },
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: '20px 16px',
286
+ padding: '12px 16px',
287
287
  fontWeight: 'bold',
288
288
  fontSize: '14px',
289
289
  color: 'var(--crepe-color-on-surface)',
290
- borderBottom: '1px solid var(--crepe-color-outline-variant)',
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+z'] },
68
+ { labelKey: 'shortcuts.redo', keys: ['Mod+y', 'Shift+Mod+Z'] },
69
69
  ],
70
70
  },
71
71
  ]
@@ -26,7 +26,7 @@ export const createViewMenuState = () =>
26
26
  outlineVisible: false,
27
27
  outlinePosition: 'left',
28
28
  outlineWidth: 250,
29
- documentBackground: null,
29
+ documentBackground: '#FFFFFF',
30
30
  showTitle: false,
31
31
  showCover: false,
32
32
  coverUrl: '',
@@ -178,7 +178,7 @@ const imageBlockConverter = $prose((ctx) => {
178
178
  tr.replaceWith(r.from, r.to, r.blocks)
179
179
  }
180
180
 
181
- return tr
181
+ return tr.setMeta('addToHistory', false)
182
182
  },
183
183
  })
184
184
  })
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ // Diff view logic is now in index.ts as InlineDiffView class
2
+ export {}