v-code-diff 0.3.12 → 1.0.0

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/src/utils.ts ADDED
@@ -0,0 +1,418 @@
1
+ import * as Diff from 'diff'
2
+ import type { Change } from 'diff'
3
+ import hljs from './highlight'
4
+ import { DiffType } from './types'
5
+ import type { DiffLine, SplitViewerChange, UnifiedViewerChange } from './types'
6
+
7
+ const MODIFIED_START_TAG = '<code-diff-modified>'
8
+ const MODIFIED_CLOSE_TAG = '</code-diff-modified>'
9
+
10
+ const startEntity = MODIFIED_START_TAG.replace('<', '&lt;').replace('>', '&gt;')
11
+ const closeEntity = MODIFIED_CLOSE_TAG.replace('<', '&lt;').replace('>', '&gt;')
12
+
13
+ const lineType = (diff: Diff.Change): DiffType => {
14
+ if (diff === undefined)
15
+ return DiffType.EQUAL
16
+ if (diff.added)
17
+ return DiffType.ADD
18
+ if (diff.removed)
19
+ return DiffType.DELETE
20
+ return DiffType.EQUAL
21
+ }
22
+
23
+ const renderWords = (prev?: string, current?: string, diffStyle = 'word'): string => {
24
+ if (typeof prev === 'undefined')
25
+ return current!
26
+ if (typeof current === 'undefined')
27
+ return prev!
28
+
29
+ type RenderFunctionType = (prev: string, old: string) => Change[]
30
+ const func: RenderFunctionType = diffStyle === 'char' ? Diff.diffChars : Diff.diffWords
31
+ return func(prev, current)
32
+ .filter(word => lineType(word) !== DiffType.DELETE)
33
+ .map(word =>
34
+ lineType(word) === DiffType.ADD ? `${MODIFIED_START_TAG}${word.value}${MODIFIED_CLOSE_TAG}` : word.value,
35
+ )
36
+ .join('')
37
+ }
38
+
39
+ const getHighlightCode = (language: string, code: string) => {
40
+ const hasModifiedTags = code.match(new RegExp(`(${MODIFIED_START_TAG}|${MODIFIED_CLOSE_TAG})`, 'g'))
41
+
42
+ if (!hasModifiedTags)
43
+ return hljs.highlight(code, { language }).value
44
+
45
+ /**
46
+ * Explore highlight DOM extracted from pure code and compare the text with the original code to generate the highlight code
47
+ */
48
+ let originalCode = code // original code with modified tags
49
+ const pureCode = code.replace(new RegExp(`(${MODIFIED_START_TAG}|${MODIFIED_CLOSE_TAG})`, 'g'), '') // Without modified tags
50
+ const pureElement = document.createElement('div')
51
+ pureElement.innerHTML = hljs.highlight(pureCode, { language }).value // Highlight DOM without modified tags
52
+
53
+ // Modified span is created per highlight operator and causes it to continue
54
+ let innerModifiedTag = false
55
+
56
+ const diffElements = (node: HTMLElement) => {
57
+ node.childNodes.forEach((child) => {
58
+ if (child.nodeType === Node.ELEMENT_NODE)
59
+ diffElements(child as HTMLElement)
60
+
61
+ // Compare text nodes and check changed text
62
+ if (child.nodeType === Node.TEXT_NODE) {
63
+ if (!child.textContent)
64
+ return
65
+
66
+ let oldContent = child.textContent
67
+ let newContent = ''
68
+
69
+ if (innerModifiedTag) {
70
+ // If it continues within the modified range
71
+ newContent = newContent + MODIFIED_START_TAG
72
+ }
73
+
74
+ while (oldContent.length) {
75
+ if (originalCode.startsWith(MODIFIED_START_TAG)) {
76
+ // Add modified start tag
77
+ originalCode = originalCode.slice(MODIFIED_START_TAG.length)
78
+ newContent = newContent + MODIFIED_START_TAG
79
+ innerModifiedTag = true // Start modified
80
+ continue
81
+ }
82
+ if (originalCode.startsWith(MODIFIED_CLOSE_TAG)) {
83
+ // Add modified close tag
84
+ originalCode = originalCode.slice(MODIFIED_CLOSE_TAG.length)
85
+ newContent = newContent + MODIFIED_CLOSE_TAG
86
+ innerModifiedTag = false // End modified
87
+ continue
88
+ }
89
+
90
+ // Add words before modified tag
91
+ const hasModifiedTag = originalCode.match(new RegExp(`(${MODIFIED_START_TAG}|${MODIFIED_CLOSE_TAG})`))
92
+ const originalCodeDiffLength = (hasModifiedTag && hasModifiedTag.index) ? hasModifiedTag.index : originalCode.length
93
+ const nextDiffsLength = Math.min(originalCodeDiffLength, oldContent.length)
94
+
95
+ newContent = newContent + originalCode.substring(0, nextDiffsLength)
96
+ originalCode = originalCode.slice(nextDiffsLength)
97
+ oldContent = oldContent.slice(nextDiffsLength)
98
+ }
99
+
100
+ if (innerModifiedTag) {
101
+ // If the loop is finished without a modified close, it is still within the modified range.
102
+ newContent = newContent + MODIFIED_CLOSE_TAG
103
+ }
104
+
105
+ child.textContent = newContent // put as entity code because change textContent
106
+ }
107
+ })
108
+ }
109
+ diffElements(pureElement)
110
+
111
+ return pureElement.innerHTML
112
+ .replace(new RegExp(startEntity, 'g'), '<span class="x">')
113
+ .replace(new RegExp(closeEntity, 'g'), '</span>')
114
+ }
115
+
116
+ export function createSplitDiff(
117
+ oldString: string,
118
+ newString: string,
119
+ language = 'plaintext',
120
+ diffStyle = 'word',
121
+ context = 10,
122
+ ) {
123
+ const newEmptySplitDiff = (): DiffLine => ({ type: DiffType.EMPTY })
124
+ const newSplitDiff = (type: DiffType, num: number, code: string): DiffLine => ({ type, num, code })
125
+
126
+ const changes = Diff.diffLines(oldString, newString)
127
+
128
+ let delNum = 0
129
+ let addNum = 0
130
+ let skip = false
131
+
132
+ const result: SplitViewerChange = []
133
+ for (let i = 0; i < changes.length; i++) {
134
+ if (skip) {
135
+ skip = false
136
+ continue
137
+ }
138
+
139
+ const [cur, next] = [changes[i], changes[i + 1]]
140
+ const [curType, nextType] = [lineType(cur), lineType(next)]
141
+
142
+ const curLines = cur.value.replace(/\n$/, '').split('\n')
143
+
144
+ // 最后一处 diff 的特殊处理
145
+ if (next === undefined) {
146
+ for (const line of curLines) {
147
+ let left: DiffLine = newEmptySplitDiff()
148
+ let right: DiffLine = newEmptySplitDiff()
149
+
150
+ const highlightCode = getHighlightCode(language, line)
151
+ if (curType === DiffType.EQUAL) {
152
+ delNum++
153
+ addNum++
154
+
155
+ left = newSplitDiff(DiffType.EQUAL, delNum, highlightCode)
156
+ right = newSplitDiff(DiffType.EQUAL, addNum, highlightCode)
157
+ }
158
+ if (curType === DiffType.DELETE) {
159
+ delNum++
160
+
161
+ left = newSplitDiff(DiffType.DELETE, delNum, highlightCode)
162
+ right = newEmptySplitDiff()
163
+ }
164
+ if (curType === DiffType.ADD) {
165
+ addNum++
166
+
167
+ left = newEmptySplitDiff()
168
+ right = newSplitDiff(DiffType.ADD, addNum, highlightCode)
169
+ }
170
+
171
+ result.push({ left, right })
172
+ }
173
+ break
174
+ }
175
+
176
+ // 正常逻辑
177
+ // 处理当前 diff 为相等的情况
178
+ if (curType === DiffType.EQUAL) {
179
+ for (const line of curLines) {
180
+ delNum++
181
+ addNum++
182
+
183
+ const highlightCode = getHighlightCode(language, line)
184
+ result.push({
185
+ left: newSplitDiff(DiffType.EQUAL, delNum, highlightCode),
186
+ right: newSplitDiff(DiffType.EQUAL, addNum, highlightCode),
187
+ })
188
+ }
189
+ }
190
+
191
+ const nextLines = next.value.replace(/\n$/, '').split('\n')
192
+ // 处理当前 diff 为删除的情况
193
+ if (curType === DiffType.DELETE) {
194
+ if (nextType === DiffType.EQUAL) {
195
+ for (const line of curLines) {
196
+ delNum++
197
+
198
+ result.push({
199
+ left: newSplitDiff(DiffType.DELETE, delNum, getHighlightCode(language, line)),
200
+ right: newEmptySplitDiff(),
201
+ })
202
+ }
203
+ }
204
+ if (nextType === DiffType.ADD) {
205
+ skip = true
206
+ const maxCount = Math.max(cur.count!, next.count!)
207
+ for (let j = 0; j < maxCount; j++) {
208
+ if (j < cur.count!)
209
+ delNum++
210
+
211
+ if (j < next.count!)
212
+ addNum++
213
+
214
+ const [curLine, nextLine] = [curLines[j], nextLines[j]]
215
+
216
+ const leftLine = curLines.length === nextLines.length ? renderWords(nextLine, curLine, diffStyle) : curLine
217
+ const rightLine = curLines.length === nextLines.length ? renderWords(curLine, nextLine, diffStyle) : nextLine
218
+
219
+ const left
220
+ = j < cur.count!
221
+ ? newSplitDiff(DiffType.DELETE, delNum, getHighlightCode(language, leftLine))
222
+ : newEmptySplitDiff()
223
+ const right
224
+ = j < next.count!
225
+ ? newSplitDiff(DiffType.ADD, addNum, getHighlightCode(language, rightLine))
226
+ : newEmptySplitDiff()
227
+
228
+ result.push({ left, right })
229
+ }
230
+ }
231
+ }
232
+ // 处理当前 diff 为添加的情况
233
+ if (curType === DiffType.ADD) {
234
+ for (const line of curLines) {
235
+ addNum++
236
+ result.push({
237
+ left: newEmptySplitDiff(),
238
+ right: newSplitDiff(DiffType.ADD, addNum, getHighlightCode(language, line)),
239
+ })
240
+ }
241
+ }
242
+ }
243
+
244
+ if (oldString === newString) {
245
+ for (let i = 0; i < result.length; i++)
246
+ result[i].fold = false
247
+
248
+ return result
249
+ }
250
+
251
+ for (let i = 0; i < result.length; i++) {
252
+ const line = result[i]
253
+ if (line.left.type === DiffType.DELETE || line.right.type === DiffType.ADD) {
254
+ const [start, end] = [Math.max(i - context, 0), Math.min(i + context + 1, result.length)]
255
+ for (let j = start; j < end; j++)
256
+ result[j].fold = false
257
+ }
258
+ if (line.fold === undefined)
259
+ line.fold = true
260
+ }
261
+
262
+ const realResult = []
263
+ for (let i = 0; i < result.length; i++) {
264
+ const line = result[i]
265
+ if (line.fold === false) {
266
+ realResult.push(line)
267
+ continue
268
+ }
269
+ if (line.fold === true) {
270
+ if (realResult[realResult.length - 1]?.fold !== true)
271
+ realResult.push(line)
272
+ }
273
+ }
274
+
275
+ return realResult
276
+ }
277
+
278
+ export function createUnifiedDiff(
279
+ oldString: string,
280
+ newString: string,
281
+ language = 'plaintext',
282
+ diffStyle = 'word',
283
+ context = 2,
284
+ ) {
285
+ const changes = Diff.diffLines(oldString, newString)
286
+
287
+ let delNum = 0
288
+ let addNum = 0
289
+ let skip = false
290
+
291
+ const result: UnifiedViewerChange = []
292
+ for (let i = 0; i < changes.length; i++) {
293
+ if (skip) {
294
+ skip = false
295
+ continue
296
+ }
297
+
298
+ const [cur, next] = [changes[i], changes[i + 1]]
299
+ const [curType, nextType] = [lineType(cur), lineType(next)]
300
+
301
+ const curLines = cur.value.replace(/\n$/, '').split('\n')
302
+
303
+ // 最后一行的特殊处理
304
+ if (next === undefined) {
305
+ for (const line of curLines) {
306
+ if (curType === DiffType.EQUAL) {
307
+ delNum++
308
+ addNum++
309
+ }
310
+ if (curType === DiffType.DELETE)
311
+ delNum++
312
+
313
+ if (curType === DiffType.ADD)
314
+ addNum++
315
+
316
+ const code = getHighlightCode(language, line)
317
+
318
+ result.push({
319
+ type: curType,
320
+ code,
321
+ addNum: curType === DiffType.DELETE ? undefined : addNum,
322
+ delNum: curType === DiffType.ADD ? undefined : delNum,
323
+ })
324
+ }
325
+ break
326
+ }
327
+
328
+ // 正常逻辑
329
+ // 处理当前 diff 为相等的情况
330
+ if (curType === DiffType.EQUAL) {
331
+ for (const line of curLines) {
332
+ delNum++
333
+ addNum++
334
+ const code = getHighlightCode(language, line)
335
+
336
+ result.push({ type: DiffType.EQUAL, code, delNum, addNum })
337
+ }
338
+ }
339
+
340
+ const nextLines = next.value.replace(/\n$/, '').split('\n')
341
+ // 处理当前 diff 为删除的情况
342
+ if (curType === DiffType.DELETE) {
343
+ // 下一处差异为新增,且删除与新增行数相同时,对每行依次 diff
344
+ if (nextType === DiffType.ADD && curLines.length === nextLines.length) {
345
+ for (let j = 0; j < curLines.length; j++) {
346
+ const curLine = curLines[j]
347
+ const nextLine = nextLines[j]
348
+ delNum++
349
+
350
+ const code = getHighlightCode(language, renderWords(nextLine, curLine, diffStyle))
351
+ result.push({ type: DiffType.DELETE, code, delNum })
352
+ }
353
+
354
+ for (let j = 0; j < nextLines.length; j++) {
355
+ const curLine = curLines[j]
356
+ const nextLine = nextLines[j]
357
+ addNum++
358
+
359
+ const code = getHighlightCode(language, renderWords(curLine, nextLine, diffStyle))
360
+ result.push({ type: DiffType.ADD, code, addNum })
361
+ }
362
+
363
+ skip = true
364
+ }
365
+ else {
366
+ // 否则单独渲染每行
367
+ for (const line of curLines) {
368
+ delNum++
369
+
370
+ const code = getHighlightCode(language, line)
371
+ result.push({ type: DiffType.DELETE, code, delNum })
372
+ }
373
+ }
374
+ }
375
+ // 处理当前 diff 为添加的情况
376
+ if (curType === DiffType.ADD) {
377
+ for (const line of curLines) {
378
+ addNum++
379
+ const code = getHighlightCode(language, line)
380
+
381
+ result.push({ type: DiffType.ADD, code, addNum })
382
+ }
383
+ }
384
+ }
385
+
386
+ for (let i = 0; i < result.length; i++) {
387
+ const line = result[i]
388
+ if (line.type === DiffType.DELETE || line.type === DiffType.ADD) {
389
+ const [start, end] = [Math.max(i - context, 0), Math.min(i + context + 1, result.length)]
390
+ for (let j = start; j < end; j++)
391
+ result[j].fold = false
392
+ }
393
+ if (line.fold === undefined)
394
+ line.fold = true
395
+ }
396
+
397
+ if (oldString === newString) {
398
+ for (let i = 0; i < result.length; i++)
399
+ result[i].fold = false
400
+
401
+ return result
402
+ }
403
+
404
+ const realResult = []
405
+ for (let i = 0; i < result.length; i++) {
406
+ const line = result[i]
407
+ if (line.fold === false) {
408
+ realResult.push(line)
409
+ continue
410
+ }
411
+ if (line.fold === true) {
412
+ if (i === 0 || realResult[realResult.length - 1]?.fold !== true)
413
+ realResult.push(line)
414
+ }
415
+ }
416
+
417
+ return realResult
418
+ }