@jvs-milkdown/crepe 1.2.13 → 1.2.15

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 (100) 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 +9 -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 +9 -2
  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 +9 -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 +9 -2
  22. package/lib/cjs/feature/table/index.js.map +1 -1
  23. package/lib/cjs/feature/toolbar/index.js +14 -6
  24. package/lib/cjs/feature/toolbar/index.js.map +1 -1
  25. package/lib/cjs/index.js +1280 -260
  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 +9 -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 +9 -2
  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 +9 -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 +9 -2
  48. package/lib/esm/feature/table/index.js.map +1 -1
  49. package/lib/esm/feature/toolbar/index.js +14 -6
  50. package/lib/esm/feature/toolbar/index.js.map +1 -1
  51. package/lib/esm/index.js +1262 -261
  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/list-item.css +113 -0
  56. package/lib/theme/common/style.css +2 -0
  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/index.d.ts.map +1 -1
  65. package/lib/types/feature/fixed-toolbar/outline-panel.d.ts.map +1 -1
  66. package/lib/types/feature/index.d.ts +7 -1
  67. package/lib/types/feature/index.d.ts.map +1 -1
  68. package/lib/types/feature/inline-diff/change-panel.d.ts +4 -0
  69. package/lib/types/feature/inline-diff/change-panel.d.ts.map +1 -0
  70. package/lib/types/feature/inline-diff/config.d.ts +12 -0
  71. package/lib/types/feature/inline-diff/config.d.ts.map +1 -0
  72. package/lib/types/feature/inline-diff/diff-engine.d.ts +20 -0
  73. package/lib/types/feature/inline-diff/diff-engine.d.ts.map +1 -0
  74. package/lib/types/feature/inline-diff/diff-view.d.ts +2 -0
  75. package/lib/types/feature/inline-diff/diff-view.d.ts.map +1 -0
  76. package/lib/types/feature/inline-diff/doc-builder.d.ts +21 -0
  77. package/lib/types/feature/inline-diff/doc-builder.d.ts.map +1 -0
  78. package/lib/types/feature/inline-diff/index.d.ts +9 -0
  79. package/lib/types/feature/inline-diff/index.d.ts.map +1 -0
  80. package/lib/types/feature/loader.d.ts.map +1 -1
  81. package/lib/types/feature/toolbar/component.d.ts.map +1 -1
  82. package/package.json +15 -4
  83. package/src/core/builder.ts +19 -0
  84. package/src/core/locale.ts +7 -0
  85. package/src/feature/diff-block/index.ts +48 -0
  86. package/src/feature/fixed-toolbar/index.ts +62 -23
  87. package/src/feature/fixed-toolbar/outline-panel.tsx +5 -3
  88. package/src/feature/index.ts +12 -0
  89. package/src/feature/inline-diff/change-panel.ts +280 -0
  90. package/src/feature/inline-diff/config.ts +28 -0
  91. package/src/feature/inline-diff/diff-engine.ts +181 -0
  92. package/src/feature/inline-diff/diff-view.ts +2 -0
  93. package/src/feature/inline-diff/doc-builder.ts +139 -0
  94. package/src/feature/inline-diff/index.ts +514 -0
  95. package/src/feature/loader.ts +8 -0
  96. package/src/feature/toolbar/component.tsx +3 -2
  97. package/src/theme/common/diff-block.css +43 -0
  98. package/src/theme/common/inline-diff.css +148 -0
  99. package/src/theme/common/list-item.css +122 -0
  100. package/src/theme/common/style.css +2 -0
@@ -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 {}
@@ -0,0 +1,139 @@
1
+ import type { Schema, Node as PMNode } from '@jvs-milkdown/kit/prose/model'
2
+
3
+ import type { DiffChunk, CharPart } from './diff-engine'
4
+
5
+ export type ChangeType = 'added' | 'removed'
6
+
7
+ export interface ContentRange {
8
+ from: number
9
+ to: number
10
+ }
11
+
12
+ export interface ChangeInfo {
13
+ type: ChangeType
14
+ from: number
15
+ oldText: string
16
+ newText: string
17
+ blockRange: ContentRange
18
+ inlineRanges: ContentRange[]
19
+ chunkId: string
20
+ }
21
+
22
+ interface PendingChange {
23
+ type: ChangeType
24
+ childIndex: number
25
+ oldText: string
26
+ newText: string
27
+ parts?: CharPart[]
28
+ chunkId: string
29
+ }
30
+
31
+ export function buildNewDoc(
32
+ chunks: DiffChunk[],
33
+ schema: Schema
34
+ ): { doc: PMNode; changes: ChangeInfo[] } {
35
+ const children: PMNode[] = []
36
+ const pending: PendingChange[] = []
37
+
38
+ for (const chunk of chunks) {
39
+ if (chunk.status === 'accepted') {
40
+ for (const diff of chunk.diffs) {
41
+ if (diff.type === 'added') children.push(diff.newNode!)
42
+ if (diff.type === 'unchanged') children.push(diff.newNode!)
43
+ }
44
+ } else if (chunk.status === 'rejected') {
45
+ for (const diff of chunk.diffs) {
46
+ if (diff.type === 'removed') children.push(diff.oldNode!)
47
+ if (diff.type === 'unchanged') children.push(diff.oldNode!)
48
+ }
49
+ } else {
50
+ // pending
51
+ for (const diff of chunk.diffs) {
52
+ if (diff.type === 'unchanged') {
53
+ children.push(diff.newNode!)
54
+ } else if (diff.type === 'added') {
55
+ pending.push({
56
+ type: 'added',
57
+ childIndex: children.length,
58
+ oldText: '',
59
+ newText: diff.newNode?.textContent ?? '',
60
+ parts: diff.parts,
61
+ chunkId: chunk.id,
62
+ })
63
+ children.push(diff.newNode!)
64
+ } else if (diff.type === 'removed') {
65
+ pending.push({
66
+ type: 'removed',
67
+ childIndex: children.length,
68
+ oldText: diff.oldNode?.textContent ?? '',
69
+ newText: '',
70
+ parts: diff.parts,
71
+ chunkId: chunk.id,
72
+ })
73
+ children.push(diff.oldNode!)
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ if (children.length === 0) {
80
+ children.push(schema.node('paragraph', null, []))
81
+ }
82
+
83
+ const doc = schema.node('doc', null, children)
84
+
85
+ let pos = 0
86
+ const positions: ContentRange[] = []
87
+ for (const child of children) {
88
+ positions.push({ from: pos, to: pos + child.nodeSize })
89
+ pos += child.nodeSize
90
+ }
91
+
92
+ const changes: ChangeInfo[] = pending.map((pc) => {
93
+ const cp = positions[pc.childIndex]!
94
+
95
+ const inlineRanges: ContentRange[] = []
96
+ let offset = cp.from + 1
97
+
98
+ if (pc.type === 'removed') {
99
+ for (const part of pc.parts ?? []) {
100
+ if (part.type === 'delete') {
101
+ inlineRanges.push({ from: offset, to: offset + part.value.length })
102
+ offset += part.value.length
103
+ } else if (part.type === 'equal') {
104
+ offset += part.value.length
105
+ }
106
+ }
107
+ return {
108
+ type: 'removed',
109
+ from: cp.from,
110
+ oldText: pc.oldText,
111
+ newText: '',
112
+ blockRange: { from: cp.from, to: cp.to },
113
+ inlineRanges,
114
+ chunkId: pc.chunkId,
115
+ }
116
+ }
117
+
118
+ // pc.type === 'added'
119
+ for (const part of pc.parts ?? []) {
120
+ if (part.type === 'insert') {
121
+ inlineRanges.push({ from: offset, to: offset + part.value.length })
122
+ offset += part.value.length
123
+ } else if (part.type === 'equal') {
124
+ offset += part.value.length
125
+ }
126
+ }
127
+ return {
128
+ type: 'added',
129
+ from: cp.from,
130
+ oldText: '',
131
+ newText: pc.newText,
132
+ blockRange: { from: cp.from, to: cp.to },
133
+ inlineRanges,
134
+ chunkId: pc.chunkId,
135
+ }
136
+ })
137
+
138
+ return { doc, changes }
139
+ }