v-code-diff 1.13.1 → 1.14.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 CHANGED
@@ -7,10 +7,23 @@ import type { DiffLine, DiffStat, SplitLineChange, SplitLineUnchanges, SplitView
7
7
 
8
8
  const MODIFIED_START_TAG = '<code-diff-modified>'
9
9
  const MODIFIED_CLOSE_TAG = '</code-diff-modified>'
10
+ const MAX_INLINE_DIFF_LENGTH = 10_000
11
+ export const RENDER_BATCH_SIZE = 1_000
10
12
 
11
13
  const startEntity = MODIFIED_START_TAG.replace('<', '&lt;').replace('>', '&gt;')
12
14
  const closeEntity = MODIFIED_CLOSE_TAG.replace('<', '&lt;').replace('>', '&gt;')
13
15
 
16
+ function escapeHtml(code: string): string {
17
+ const entities: Record<string, string> = {
18
+ '&': '&amp;',
19
+ '<': '&lt;',
20
+ '>': '&gt;',
21
+ '"': '&quot;',
22
+ '\'': '&#39;',
23
+ }
24
+ return code.replace(/[&<>"']/g, char => entities[char])
25
+ }
26
+
14
27
  function lineType(diff: Diff.Change): DiffType {
15
28
  if (diff === undefined)
16
29
  return DiffType.EQUAL
@@ -21,44 +34,117 @@ function lineType(diff: Diff.Change): DiffType {
21
34
  return DiffType.EQUAL
22
35
  }
23
36
 
24
- function renderWords(prev?: string, current?: string, diffStyle = 'word'): string {
25
- if (typeof prev === 'undefined')
26
- return current!
27
- if (typeof current === 'undefined')
28
- return prev!
37
+ function renderChangedLines(prev?: string, current?: string, diffStyle = 'word', force = false): [string | undefined, string | undefined] {
38
+ if (typeof prev === 'undefined' || typeof current === 'undefined')
39
+ return [prev, current]
40
+ if (!force && prev.length + current.length > MAX_INLINE_DIFF_LENGTH)
41
+ return [prev, current]
29
42
 
30
43
  type RenderFunctionType = (prev: string, old: string) => Change[]
31
44
  const func: RenderFunctionType = diffStyle === 'char' ? Diff.diffChars : Diff.diffWords
32
- return func(prev, current)
45
+ const changes = func(prev, current)
46
+ const oldLine = changes
47
+ .filter(word => lineType(word) !== DiffType.ADD)
48
+ .map(word =>
49
+ lineType(word) === DiffType.DELETE ? `${MODIFIED_START_TAG}${word.value}${MODIFIED_CLOSE_TAG}` : word.value,
50
+ )
51
+ .join('')
52
+ const newLine = changes
33
53
  .filter(word => lineType(word) !== DiffType.DELETE)
34
54
  .map(word =>
35
55
  lineType(word) === DiffType.ADD ? `${MODIFIED_START_TAG}${word.value}${MODIFIED_CLOSE_TAG}` : word.value,
36
56
  )
37
57
  .join('')
58
+ return [oldLine, newLine]
38
59
  }
39
60
 
40
61
  function diffLines(prev: string, current: string) {
41
- const dmp = new DiffMatchPatch()
42
- const a = dmp.diff_linesToChars_(prev, current)
43
- const linePrev = a.chars1
44
- const lineCurrent = a.chars2
45
- const lineArray = a.lineArray
46
- const diffs: any[] = dmp.diff_main(linePrev, lineCurrent, false)
47
- dmp.diff_charsToLines_(diffs, lineArray)
48
- return diffs.map((x) => {
49
- const [type, text] = x
50
- const count = text.replace(/\n$/, '').split('\n').length
51
- const change: Diff.Change = {
52
- count,
53
- value: text,
54
- removed: type === DIFF_DELETE,
55
- added: type === DIFF_INSERT,
62
+ if (prev === current) {
63
+ return prev
64
+ ? [{ count: prev.replace(/\n$/, '').split('\n').length, value: prev }]
65
+ : []
66
+ }
67
+
68
+ const prevHasFinalNewline = prev.endsWith('\n')
69
+ const currentHasFinalNewline = current.endsWith('\n')
70
+ if (prevHasFinalNewline !== currentHasFinalNewline) {
71
+ if (prevHasFinalNewline)
72
+ prev = prev.slice(0, -1)
73
+ else
74
+ current = current.slice(0, -1)
75
+ }
76
+
77
+ let changes: Diff.Change[] | undefined
78
+ if (prev.length + current.length >= 100_000 && prev.includes('\n') && current.includes('\n')) {
79
+ const prevLines = prev ? prev.replace(/\n$/, '').split('\n') : []
80
+ const currentLines = current ? current.replace(/\n$/, '').split('\n') : []
81
+ const prevLineSet = new Set(prevLines)
82
+ const currentLineSet = new Set(currentLines)
83
+ const [smallerSet, largerSet] = prevLineSet.size < currentLineSet.size
84
+ ? [prevLineSet, currentLineSet]
85
+ : [currentLineSet, prevLineSet]
86
+ let sharedLines = 0
87
+ for (const line of smallerSet) {
88
+ if (largerSet.has(line))
89
+ sharedLines++
90
+ }
91
+ const overlap = smallerSet.size ? sharedLines / smallerSet.size : 0
92
+ const exceedsDmpLineLimit = prevLineSet.size >= 40_000 || prevLineSet.size + currentLineSet.size - sharedLines >= 65_535
93
+ const changedUniqueLines = prevLineSet.size + currentLineSet.size - sharedLines * 2
94
+
95
+ if (overlap < 0.01 || (exceedsDmpLineLimit && changedUniqueLines > 200)) {
96
+ // ponytail: complex large inputs use whole-file replacements; revisit if detailed move detection becomes necessary.
97
+ changes = []
98
+ if (prev)
99
+ changes.push({ count: prevLines.length, value: prev, removed: true })
100
+ if (current)
101
+ changes.push({ count: currentLines.length, value: current, added: true })
102
+ }
103
+ else if (exceedsDmpLineLimit) {
104
+ changes = Diff.diffLines(prev, current)
56
105
  }
57
- return change
58
- })
106
+ }
107
+
108
+ if (!changes) {
109
+ const dmp = new DiffMatchPatch()
110
+ const a = dmp.diff_linesToChars_(prev, current)
111
+ const linePrev = a.chars1
112
+ const lineCurrent = a.chars2
113
+ const lineArray = a.lineArray
114
+ const diffs: any[] = dmp.diff_main(linePrev, lineCurrent, false)
115
+ dmp.diff_charsToLines_(diffs, lineArray)
116
+ changes = diffs.map((x) => {
117
+ const [type, text] = x
118
+ const count = text.replace(/\n$/, '').split('\n').length
119
+ const change: Diff.Change = {
120
+ count,
121
+ value: text,
122
+ removed: type === DIFF_DELETE,
123
+ added: type === DIFF_INSERT,
124
+ }
125
+ return change
126
+ })
127
+ }
128
+
129
+ if (prevHasFinalNewline !== currentHasFinalNewline) {
130
+ changes.push({
131
+ count: 1,
132
+ value: '',
133
+ removed: prevHasFinalNewline,
134
+ added: currentHasFinalNewline,
135
+ })
136
+ }
137
+
138
+ return changes
59
139
  }
60
140
 
61
141
  function getHighlightCode(language: string, code: string) {
142
+ if (typeof document === 'undefined' || language === 'plaintext') {
143
+ return escapeHtml(code)
144
+ .replace(new RegExp(startEntity, 'g'), '<span class="x">')
145
+ .replace(new RegExp(closeEntity, 'g'), '</span>')
146
+ }
147
+
62
148
  const hasModifiedTags = code.match(new RegExp(`(${MODIFIED_START_TAG}|${MODIFIED_CLOSE_TAG})`, 'g'))
63
149
 
64
150
  if (!hasModifiedTags)
@@ -135,6 +221,28 @@ function getHighlightCode(language: string, code: string) {
135
221
  .replace(new RegExp(closeEntity, 'g'), '</span>')
136
222
  }
137
223
 
224
+ export function highlightUnifiedLine(line: UnifiedLineChange, language: string) {
225
+ if (line.highlighted)
226
+ return
227
+ line.code = getHighlightCode(language, line.code)
228
+ line.highlighted = true
229
+ }
230
+
231
+ export function highlightSplitLine(line: SplitLineChange, language: string) {
232
+ if (line.highlighted)
233
+ return
234
+ const leftCode = line.left.code
235
+ if (leftCode !== undefined)
236
+ line.left.code = getHighlightCode(language, leftCode)
237
+
238
+ if (line.right.code !== undefined) {
239
+ line.right.code = line.left.type === DiffType.EQUAL && line.right.type === DiffType.EQUAL && line.right.code === leftCode
240
+ ? line.left.code
241
+ : getHighlightCode(language, line.right.code)
242
+ }
243
+ line.highlighted = true
244
+ }
245
+
138
246
  function calcDiffStat(changes: Change[], ignoreRegex?: RegExp): DiffStat {
139
247
  const count = (s: string, c: string) => (s.match(new RegExp(c, 'g')) || []).length
140
248
  const ignoreCount = (lines: string[]) => lines.filter(line => ignoreRegex?.test(line)).length
@@ -145,12 +253,20 @@ function calcDiffStat(changes: Change[], ignoreRegex?: RegExp): DiffStat {
145
253
  let ignoreDeletionsNum = 0
146
254
  for (const change of changes) {
147
255
  if (change.added) {
256
+ if (!ignoreRegex) {
257
+ additionsNum += change.count ?? 0
258
+ continue
259
+ }
148
260
  const ignoreNum = ignoreCount(change.value.trim().split('\n'))
149
261
  additionsNum += count(change.value.trim(), '\n') + 1 - ignoreNum
150
262
  ignoreAdditionsNum += ignoreNum
151
263
  continue
152
264
  }
153
265
  if (change.removed) {
266
+ if (!ignoreRegex) {
267
+ deletionsNum += change.count ?? 0
268
+ continue
269
+ }
154
270
  const ignoreNum = ignoreCount(change.value.trim().split('\n'))
155
271
  deletionsNum += count(change.value.trim(), '\n') + 1 - ignoreNum
156
272
  ignoreDeletionsNum += ignoreNum
@@ -207,25 +323,24 @@ export function createSplitDiff(
207
323
  let left: DiffLine = newEmptySplitDiff()
208
324
  let right: DiffLine = newEmptySplitDiff()
209
325
 
210
- const highlightCode = getHighlightCode(language, line)
211
326
  if (curType === DiffType.EQUAL) {
212
327
  delNum++
213
328
  addNum++
214
329
 
215
- left = newSplitDiff(DiffType.EQUAL, delNum, highlightCode)
216
- right = newSplitDiff(DiffType.EQUAL, addNum, highlightCode)
330
+ left = newSplitDiff(DiffType.EQUAL, delNum, line)
331
+ right = newSplitDiff(DiffType.EQUAL, addNum, line)
217
332
  }
218
333
  if (curType === DiffType.DELETE) {
219
334
  delNum++
220
335
 
221
- left = newSplitDiff(DiffType.DELETE, delNum, highlightCode)
336
+ left = newSplitDiff(DiffType.DELETE, delNum, line)
222
337
  right = newEmptySplitDiff()
223
338
  }
224
339
  if (curType === DiffType.ADD) {
225
340
  addNum++
226
341
 
227
342
  left = newEmptySplitDiff()
228
- right = newSplitDiff(DiffType.ADD, addNum, highlightCode)
343
+ right = newSplitDiff(DiffType.ADD, addNum, line)
229
344
  }
230
345
  rawChanges.push({ left, right })
231
346
  }
@@ -239,10 +354,9 @@ export function createSplitDiff(
239
354
  delNum++
240
355
  addNum++
241
356
 
242
- const highlightCode = getHighlightCode(language, line)
243
357
  rawChanges.push({
244
- left: newSplitDiff(DiffType.EQUAL, delNum, highlightCode),
245
- right: newSplitDiff(DiffType.EQUAL, addNum, highlightCode),
358
+ left: newSplitDiff(DiffType.EQUAL, delNum, line),
359
+ right: newSplitDiff(DiffType.EQUAL, addNum, line),
246
360
  })
247
361
  }
248
362
  }
@@ -255,7 +369,7 @@ export function createSplitDiff(
255
369
  delNum++
256
370
 
257
371
  rawChanges.push({
258
- left: newSplitDiff(DiffType.DELETE, delNum, getHighlightCode(language, line)),
372
+ left: newSplitDiff(DiffType.DELETE, delNum, line),
259
373
  right: newEmptySplitDiff(),
260
374
  })
261
375
  }
@@ -272,8 +386,9 @@ export function createSplitDiff(
272
386
 
273
387
  const [curLine, nextLine] = [curLines[j], nextLines[j]]
274
388
  const shouldRenderWords = forceInlineComparison || curLines.length === nextLines.length
275
- const leftLine = shouldRenderWords ? renderWords(nextLine, curLine, diffStyle) : curLine
276
- const rightLine = shouldRenderWords ? renderWords(curLine, nextLine, diffStyle) : nextLine
389
+ const [leftLine, rightLine] = shouldRenderWords
390
+ ? renderChangedLines(curLine, nextLine, diffStyle, forceInlineComparison)
391
+ : [curLine, nextLine]
277
392
 
278
393
  // 忽略匹配的行等价于相等
279
394
  const leftDiffType = ignoreRegex?.test(curLine) ? DiffType.EQUAL : DiffType.DELETE
@@ -281,11 +396,11 @@ export function createSplitDiff(
281
396
 
282
397
  const left
283
398
  = j < cur.count!
284
- ? newSplitDiff(leftDiffType, delNum, getHighlightCode(language, leftLine))
399
+ ? newSplitDiff(leftDiffType, delNum, leftLine!)
285
400
  : newEmptySplitDiff()
286
401
  const right
287
402
  = j < next.count!
288
- ? newSplitDiff(rightDiffType, addNum, getHighlightCode(language, rightLine))
403
+ ? newSplitDiff(rightDiffType, addNum, rightLine!)
289
404
  : newEmptySplitDiff()
290
405
 
291
406
  rawChanges.push({ left, right })
@@ -298,7 +413,7 @@ export function createSplitDiff(
298
413
  addNum++
299
414
  rawChanges.push({
300
415
  left: newEmptySplitDiff(),
301
- right: newSplitDiff(DiffType.ADD, addNum, getHighlightCode(language, line)),
416
+ right: newSplitDiff(DiffType.ADD, addNum, line),
302
417
  })
303
418
  }
304
419
  }
@@ -307,6 +422,7 @@ export function createSplitDiff(
307
422
  if (oldString === newString) {
308
423
  for (let i = 0; i < rawChanges.length; i++)
309
424
  rawChanges[i].fold = false
425
+ rawChanges.slice(0, RENDER_BATCH_SIZE).forEach(line => highlightSplitLine(line, language))
310
426
 
311
427
  return result
312
428
  }
@@ -353,6 +469,7 @@ export function createSplitDiff(
353
469
  unchanges = []
354
470
  }
355
471
  result.changes = processedChanges
472
+ result.changes.filter(line => !line.hide).slice(0, RENDER_BATCH_SIZE).forEach(line => highlightSplitLine(line, language))
356
473
 
357
474
  return result
358
475
  }
@@ -404,11 +521,9 @@ export function createUnifiedDiff(
404
521
  if (curType === DiffType.ADD)
405
522
  addNum++
406
523
 
407
- const code = getHighlightCode(language, line)
408
-
409
524
  rawChanges.push({
410
525
  type: curType,
411
- code,
526
+ code: line,
412
527
  addNum: curType === DiffType.DELETE ? undefined : addNum,
413
528
  delNum: curType === DiffType.ADD ? undefined : delNum,
414
529
  })
@@ -422,9 +537,7 @@ export function createUnifiedDiff(
422
537
  for (const line of curLines) {
423
538
  delNum++
424
539
  addNum++
425
- const code = getHighlightCode(language, line)
426
-
427
- rawChanges.push({ type: DiffType.EQUAL, code, delNum, addNum })
540
+ rawChanges.push({ type: DiffType.EQUAL, code: line, delNum, addNum })
428
541
  }
429
542
  }
430
543
 
@@ -433,28 +546,26 @@ export function createUnifiedDiff(
433
546
  if (curType === DiffType.DELETE) {
434
547
  // 下一处差异为新增,且删除与新增行数相同时,对每行依次 diff
435
548
  if (nextType === DiffType.ADD && (curLines.length === nextLines.length || forceInlineComparison)) {
549
+ const maxCount = Math.max(curLines.length, nextLines.length)
550
+ const renderedLines = Array.from({ length: maxCount }, (_, index) => renderChangedLines(curLines[index], nextLines[index], diffStyle, forceInlineComparison))
436
551
  for (let j = 0; j < curLines.length; j++) {
437
552
  const curLine = curLines[j]
438
- const nextLine = nextLines[j]
439
553
  delNum++
440
554
 
441
- const code = getHighlightCode(language, renderWords(nextLine, curLine, diffStyle))
442
555
  rawChanges.push({
443
556
  type: ignoreRegex?.test(curLine) ? DiffType.EQUAL : DiffType.DELETE,
444
- code,
557
+ code: renderedLines[j][0]!,
445
558
  delNum,
446
559
  })
447
560
  }
448
561
 
449
562
  for (let j = 0; j < nextLines.length; j++) {
450
- const curLine = curLines[j]
451
563
  const nextLine = nextLines[j]
452
564
  addNum++
453
565
 
454
- const code = getHighlightCode(language, renderWords(curLine, nextLine, diffStyle))
455
566
  rawChanges.push({
456
567
  type: ignoreRegex?.test(nextLine) ? DiffType.EQUAL : DiffType.ADD,
457
- code,
568
+ code: renderedLines[j][1]!,
458
569
  addNum,
459
570
  })
460
571
  }
@@ -466,8 +577,7 @@ export function createUnifiedDiff(
466
577
  for (const line of curLines) {
467
578
  delNum++
468
579
 
469
- const code = getHighlightCode(language, line)
470
- rawChanges.push({ type: DiffType.DELETE, code, delNum })
580
+ rawChanges.push({ type: DiffType.DELETE, code: line, delNum })
471
581
  }
472
582
  }
473
583
  }
@@ -475,9 +585,7 @@ export function createUnifiedDiff(
475
585
  if (curType === DiffType.ADD) {
476
586
  for (const line of curLines) {
477
587
  addNum++
478
- const code = getHighlightCode(language, line)
479
-
480
- rawChanges.push({ type: DiffType.ADD, code, addNum })
588
+ rawChanges.push({ type: DiffType.ADD, code: line, addNum })
481
589
  }
482
590
  }
483
591
  }
@@ -496,6 +604,7 @@ export function createUnifiedDiff(
496
604
  if (oldString === newString) {
497
605
  for (let i = 0; i < rawChanges.length; i++)
498
606
  rawChanges[i].fold = false
607
+ rawChanges.slice(0, RENDER_BATCH_SIZE).forEach(line => highlightUnifiedLine(line, language))
499
608
 
500
609
  return result
501
610
  }
@@ -534,6 +643,7 @@ export function createUnifiedDiff(
534
643
  unchanges = []
535
644
  }
536
645
  result.changes = processedChanges
646
+ result.changes.filter(line => !line.hide).slice(0, RENDER_BATCH_SIZE).forEach(line => highlightUnifiedLine(line, language))
537
647
 
538
648
  return result
539
649
  }