1
- export interface MatchedBlock {
1
+ interface MatchedBlock {
2
2
oldStart : number
3
3
oldEnd : number
4
4
newStart : number
5
5
newEnd : number
6
6
size : number
7
7
}
8
8
9
- export interface Operation {
9
+ interface Operation {
10
10
oldStart : number
11
11
oldEnd : number
12
12
newStart : number
13
13
newEnd : number
14
14
type : 'equal' | 'delete' | 'create' | 'replace'
15
15
}
16
16
17
- import {
18
- dressUpDiffContent ,
19
- htmlImgTagReg ,
20
- htmlTagReg ,
21
- htmlVideoTagReg ,
22
- } from './dress-up'
17
+ type BaseOpType = 'delete' | 'create'
18
+
19
+ interface HtmlDiffConfig {
20
+ minMatchedSize : number
21
+ classNames : {
22
+ createText : string
23
+ deleteText : string
24
+ createInline : string
25
+ deleteInline : string
26
+ createBlock : string
27
+ deleteBlock : string
28
+ }
29
+ }
30
+
31
+ export interface HtmlDiffOptions {
32
+ minMatchedSize ?: number
33
+ classNames ?: Partial < {
34
+ createText ?: string
35
+ deleteText ?: string
36
+ createInline ?: string
37
+ deleteInline ?: string
38
+ createBlock ?: string
39
+ deleteBlock ?: string
40
+ } >
41
+ }
23
42
24
43
const htmlStartTagReg = / ^ < (?< name > [ ^ \s / > ] + ) [ ^ > ] * > $ /
25
44
const htmlTagWithNameReg = / ^ < (?< isEnd > \/ ) ? (?< name > [ ^ \s > ] + ) [ ^ > ] * > $ /
26
45
46
+ const htmlTagReg = / ^ < [ ^ > ] + > /
47
+ const htmlImgTagReg = / ^ < i m g [ ^ > ] * > $ /
48
+ const htmlVideoTagReg = / ^ < v i d e o [ ^ > ] * > .* ?< \/ v i d e o > $ / ms
49
+
27
50
export default class HtmlDiff {
28
- minMatchedSize : number
51
+ readonly config : HtmlDiffConfig
29
52
readonly oldWords : string [ ] = [ ]
30
53
readonly newWords : string [ ] = [ ]
31
54
readonly matchedBlockList : MatchedBlock [ ] = [ ]
32
55
readonly operationList : Operation [ ] = [ ]
33
-
34
56
unifiedContent ?: string
35
- sideBySideContents ?: string [ ]
57
+ sideBySideContents ?: [ string , string ]
36
58
37
- constructor ( oldHtml : string , newHtml : string , minMatchedSize = 2 ) {
38
- this . minMatchedSize = minMatchedSize
59
+ constructor (
60
+ oldHtml : string ,
61
+ newHtml : string ,
62
+ {
63
+ minMatchedSize = 2 ,
64
+ classNames = {
65
+ createText : 'html-diff-create-text-wrapper' ,
66
+ deleteText : 'html-diff-delete-text-wrapper' ,
67
+ createInline : 'html-diff-create-inline-wrapper' ,
68
+ deleteInline : 'html-diff-delete-inline-wrapper' ,
69
+ createBlock : 'html-diff-create-block-wrapper' ,
70
+ deleteBlock : 'html-diff-delete-block-wrapper' ,
71
+ } ,
72
+ } : HtmlDiffOptions = { } ,
73
+ ) {
74
+ // init config
75
+ this . config = {
76
+ minMatchedSize,
77
+ classNames : {
78
+ createText : 'html-diff-create-text-wrapper' ,
79
+ deleteText : 'html-diff-delete-text-wrapper' ,
80
+ createInline : 'html-diff-create-inline-wrapper' ,
81
+ deleteInline : 'html-diff-delete-inline-wrapper' ,
82
+ createBlock : 'html-diff-create-block-wrapper' ,
83
+ deleteBlock : 'html-diff-delete-block-wrapper' ,
84
+ ...classNames ,
85
+ } ,
86
+ }
39
87
88
+ // no need to diff
40
89
if ( oldHtml === newHtml ) {
41
90
this . unifiedContent = oldHtml
42
91
this . sideBySideContents = [ oldHtml , newHtml ]
@@ -66,13 +115,13 @@ export default class HtmlDiff {
66
115
}
67
116
break
68
117
case 'delete' :
69
- result += dressUpDiffContent (
118
+ result += this . dressUpDiffContent (
70
119
'delete' ,
71
120
this . oldWords . slice ( operation . oldStart , operation . oldEnd ) ,
72
121
)
73
122
break
74
123
case 'create' :
75
- result += dressUpDiffContent (
124
+ result += this . dressUpDiffContent (
76
125
'create' ,
77
126
this . newWords . slice ( operation . newStart , operation . newEnd ) ,
78
127
)
@@ -119,7 +168,7 @@ export default class HtmlDiff {
119
168
}
120
169
121
170
// deal normal tag
122
- result += dressUpDiffContent ( 'delete' , deleteOfWords )
171
+ result += this . dressUpDiffContent ( 'delete' , deleteOfWords )
123
172
deleteOfWords . splice ( 0 )
124
173
let isTagInNewFind = false
125
174
for (
@@ -136,7 +185,7 @@ export default class HtmlDiff {
136
185
) {
137
186
// find first matched tag, but not maybe the expected tag(to optimize)
138
187
isTagInNewFind = true
139
- result += dressUpDiffContent ( 'create' , createOfWords )
188
+ result += this . dressUpDiffContent ( 'create' , createOfWords )
140
189
result += createWord
141
190
createOfWords . splice ( 0 )
142
191
createIndex = tempCreateIndex + 1
@@ -157,8 +206,8 @@ export default class HtmlDiff {
157
206
if ( createIndex < operation . newEnd ) {
158
207
createOfWords . push ( ...this . newWords . slice ( createIndex , operation . newEnd ) )
159
208
}
160
- result += dressUpDiffContent ( 'delete' , deleteOfWords )
161
- result += dressUpDiffContent ( 'create' , createOfWords )
209
+ result += this . dressUpDiffContent ( 'delete' , deleteOfWords )
210
+ result += this . dressUpDiffContent ( 'create' , createOfWords )
162
211
break
163
212
default :
164
213
const exhaustiveCheck : never = operation . type
@@ -198,31 +247,31 @@ export default class HtmlDiff {
198
247
break
199
248
case 'delete' :
200
249
const deleteWords = this . oldWords . slice ( operation . oldStart , operation . oldEnd )
201
- oldHtml += dressUpDiffContent ( 'delete' , deleteWords )
250
+ oldHtml += this . dressUpDiffContent ( 'delete' , deleteWords )
202
251
break
203
252
case 'create' :
204
253
const createWords = this . newWords . slice ( operation . newStart , operation . newEnd )
205
- newHtml += dressUpDiffContent ( 'create' , createWords )
254
+ newHtml += this . dressUpDiffContent ( 'create' , createWords )
206
255
break
207
256
case 'replace' :
208
257
const deleteOfReplaceWords = this . oldWords . slice (
209
258
operation . oldStart ,
210
259
operation . oldEnd ,
211
260
)
212
- oldHtml += dressUpDiffContent ( 'delete' , deleteOfReplaceWords )
261
+ oldHtml += this . dressUpDiffContent ( 'delete' , deleteOfReplaceWords )
213
262
const createOfReplaceWords = this . newWords . slice (
214
263
operation . newStart ,
215
264
operation . newEnd ,
216
265
)
217
- newHtml += dressUpDiffContent ( 'create' , createOfReplaceWords )
266
+ newHtml += this . dressUpDiffContent ( 'create' , createOfReplaceWords )
218
267
break
219
268
default :
220
269
const exhaustiveCheck : never = operation . type
221
270
console . error ( 'Error operation type: ' + exhaustiveCheck )
222
271
}
223
272
} )
224
273
225
- const result = [ oldHtml , newHtml ]
274
+ const result : [ string , string ] = [ oldHtml , newHtml ]
226
275
this . sideBySideContents = result
227
276
return result
228
277
}
@@ -325,7 +374,7 @@ export default class HtmlDiff {
325
374
}
326
375
}
327
376
328
- return maxSize >= this . minMatchedSize ? bestMatchedBlock : null
377
+ return maxSize >= this . config . minMatchedSize ? bestMatchedBlock : null
329
378
}
330
379
331
380
// use matchedBlockList walk the words to find change description
@@ -380,4 +429,63 @@ export default class HtmlDiff {
380
429
}
381
430
return operationList
382
431
}
432
+
433
+ private dressUpDiffContent ( type : BaseOpType , words : string [ ] ) : string {
434
+ const wordsLength = words . length
435
+ if ( ! wordsLength ) {
436
+ return ''
437
+ }
438
+
439
+ let result = ''
440
+ let textStartIndex = 0
441
+ for ( let i = 0 ; i < wordsLength ; i ++ ) {
442
+ const word = words [ i ]
443
+ // this word is html tag
444
+ if ( word . match ( htmlTagReg ) ) {
445
+ // deal text words before
446
+ if ( i > textStartIndex ) {
447
+ result += this . dressUpText ( type , words . slice ( textStartIndex , i ) )
448
+ }
449
+ // deal this tag
450
+ textStartIndex = i + 1
451
+ if ( word . match ( htmlVideoTagReg ) ) {
452
+ result += this . dressUpBlockTag ( type , word )
453
+ } else if ( [ htmlImgTagReg ] . some ( item => word . match ( item ) ) ) {
454
+ result += this . dressUpInlineTag ( type , word )
455
+ } else {
456
+ result += word
457
+ }
458
+ }
459
+ }
460
+ if ( textStartIndex < wordsLength ) {
461
+ result += this . dressUpText ( type , words . slice ( textStartIndex ) )
462
+ }
463
+ return result
464
+ }
465
+
466
+ private dressUpText ( type : BaseOpType , words : string [ ] ) : string {
467
+ const text = words . join ( '' )
468
+ if ( ! text . trim ( ) ) return ''
469
+ if ( type === 'create' )
470
+ return `<span class="${ this . config . classNames . createText } ">${ text } </span>`
471
+ if ( type === 'delete' )
472
+ return `<span class="${ this . config . classNames . deleteText } ">${ text } </span>`
473
+ return ''
474
+ }
475
+
476
+ private dressUpInlineTag ( type : BaseOpType , word : string ) : string {
477
+ if ( type === 'create' )
478
+ return `<span class="${ this . config . classNames . createInline } ">${ word } </span>`
479
+ if ( type === 'delete' )
480
+ return `<span class="${ this . config . classNames . deleteInline } ">${ word } </span>`
481
+ return ''
482
+ }
483
+
484
+ private dressUpBlockTag ( type : BaseOpType , word : string ) : string {
485
+ if ( type === 'create' )
486
+ return `<div class="${ this . config . classNames . createBlock } ">${ word } </div>`
487
+ if ( type === 'delete' )
488
+ return `<div class="${ this . config . classNames . deleteBlock } ">${ word } </div>`
489
+ return ''
490
+ }
383
491
}
0 commit comments