Skip to content

Commit 655fd53

Browse files
committed
feat: classNames config
1 parent 37745b5 commit 655fd53

File tree

9 files changed

+202
-129
lines changed

9 files changed

+202
-129
lines changed

.github/workflows/github-ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ jobs:
4444
- name: Build example
4545
run: cd examples/basic && pnpm run build
4646

47+
- name: Copy public
48+
run: cp public/** examples/basic/dist
49+
4750
- name: Upload artifact
4851
uses: actions/upload-pages-artifact@v1
4952
with:

README.md

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Html Diff
22

3-
Generate HTML content with unified or side-by-side differences.
3+
Compare HTML and generate the differences in either a unified view or a side-by-side comparison.
44

55
## Install
66

@@ -20,15 +20,23 @@ const newHtml = `<div>hello world</div>`
2020
const diff = new HtmlDiff(oldHtml, newHtml)
2121
const unifiedContent = diff.getUnifiedContent()
2222
const sideBySideContents = diff.getSideBySideContents()
23+
24+
// custom config
25+
const diff = new HtmlDiff(oldHtml, newHtml, {
26+
minMatchedSize: 3,
27+
classNames: {
28+
createText: 'cra-txt',
29+
deleteText: 'del-txt',
30+
createInline: 'cra-inl',
31+
deleteInline: 'del-inl',
32+
createBlock: 'cra-blo',
33+
deleteBlock: 'del-blo',
34+
},
35+
})
2336
```
2437

2538
## Preview
2639

27-
### unified differences
28-
29-
![home](https://arman19941113.github.io/html-diff/unified.png)
30-
31-
### side-by-side differences
32-
33-
![home](https://arman19941113.github.io/html-diff/sidebyside.png)
40+
[See online demo...](https://arman19941113.github.io/html-diff/)
3441

42+
![home](https://arman19941113.github.io/html-diff/doc/demo.png)

doc/sidebyside.png

-1.59 MB
Binary file not shown.

doc/unified.png

-1.63 MB
Binary file not shown.

packages/html-diff/src/dress-up.ts

-65
This file was deleted.

packages/html-diff/src/index.ts

+133-25
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,91 @@
1-
export interface MatchedBlock {
1+
interface MatchedBlock {
22
oldStart: number
33
oldEnd: number
44
newStart: number
55
newEnd: number
66
size: number
77
}
88

9-
export interface Operation {
9+
interface Operation {
1010
oldStart: number
1111
oldEnd: number
1212
newStart: number
1313
newEnd: number
1414
type: 'equal' | 'delete' | 'create' | 'replace'
1515
}
1616

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+
}
2342

2443
const htmlStartTagReg = /^<(?<name>[^\s/>]+)[^>]*>$/
2544
const htmlTagWithNameReg = /^<(?<isEnd>\/)?(?<name>[^\s>]+)[^>]*>$/
2645

46+
const htmlTagReg = /^<[^>]+>/
47+
const htmlImgTagReg = /^<img[^>]*>$/
48+
const htmlVideoTagReg = /^<video[^>]*>.*?<\/video>$/ms
49+
2750
export default class HtmlDiff {
28-
minMatchedSize: number
51+
readonly config: HtmlDiffConfig
2952
readonly oldWords: string[] = []
3053
readonly newWords: string[] = []
3154
readonly matchedBlockList: MatchedBlock[] = []
3255
readonly operationList: Operation[] = []
33-
3456
unifiedContent?: string
35-
sideBySideContents?: string[]
57+
sideBySideContents?: [string, string]
3658

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+
}
3987

88+
// no need to diff
4089
if (oldHtml === newHtml) {
4190
this.unifiedContent = oldHtml
4291
this.sideBySideContents = [oldHtml, newHtml]
@@ -66,13 +115,13 @@ export default class HtmlDiff {
66115
}
67116
break
68117
case 'delete':
69-
result += dressUpDiffContent(
118+
result += this.dressUpDiffContent(
70119
'delete',
71120
this.oldWords.slice(operation.oldStart, operation.oldEnd),
72121
)
73122
break
74123
case 'create':
75-
result += dressUpDiffContent(
124+
result += this.dressUpDiffContent(
76125
'create',
77126
this.newWords.slice(operation.newStart, operation.newEnd),
78127
)
@@ -119,7 +168,7 @@ export default class HtmlDiff {
119168
}
120169

121170
// deal normal tag
122-
result += dressUpDiffContent('delete', deleteOfWords)
171+
result += this.dressUpDiffContent('delete', deleteOfWords)
123172
deleteOfWords.splice(0)
124173
let isTagInNewFind = false
125174
for (
@@ -136,7 +185,7 @@ export default class HtmlDiff {
136185
) {
137186
// find first matched tag, but not maybe the expected tag(to optimize)
138187
isTagInNewFind = true
139-
result += dressUpDiffContent('create', createOfWords)
188+
result += this.dressUpDiffContent('create', createOfWords)
140189
result += createWord
141190
createOfWords.splice(0)
142191
createIndex = tempCreateIndex + 1
@@ -157,8 +206,8 @@ export default class HtmlDiff {
157206
if (createIndex < operation.newEnd) {
158207
createOfWords.push(...this.newWords.slice(createIndex, operation.newEnd))
159208
}
160-
result += dressUpDiffContent('delete', deleteOfWords)
161-
result += dressUpDiffContent('create', createOfWords)
209+
result += this.dressUpDiffContent('delete', deleteOfWords)
210+
result += this.dressUpDiffContent('create', createOfWords)
162211
break
163212
default:
164213
const exhaustiveCheck: never = operation.type
@@ -198,31 +247,31 @@ export default class HtmlDiff {
198247
break
199248
case 'delete':
200249
const deleteWords = this.oldWords.slice(operation.oldStart, operation.oldEnd)
201-
oldHtml += dressUpDiffContent('delete', deleteWords)
250+
oldHtml += this.dressUpDiffContent('delete', deleteWords)
202251
break
203252
case 'create':
204253
const createWords = this.newWords.slice(operation.newStart, operation.newEnd)
205-
newHtml += dressUpDiffContent('create', createWords)
254+
newHtml += this.dressUpDiffContent('create', createWords)
206255
break
207256
case 'replace':
208257
const deleteOfReplaceWords = this.oldWords.slice(
209258
operation.oldStart,
210259
operation.oldEnd,
211260
)
212-
oldHtml += dressUpDiffContent('delete', deleteOfReplaceWords)
261+
oldHtml += this.dressUpDiffContent('delete', deleteOfReplaceWords)
213262
const createOfReplaceWords = this.newWords.slice(
214263
operation.newStart,
215264
operation.newEnd,
216265
)
217-
newHtml += dressUpDiffContent('create', createOfReplaceWords)
266+
newHtml += this.dressUpDiffContent('create', createOfReplaceWords)
218267
break
219268
default:
220269
const exhaustiveCheck: never = operation.type
221270
console.error('Error operation type: ' + exhaustiveCheck)
222271
}
223272
})
224273

225-
const result = [oldHtml, newHtml]
274+
const result: [string, string] = [oldHtml, newHtml]
226275
this.sideBySideContents = result
227276
return result
228277
}
@@ -325,7 +374,7 @@ export default class HtmlDiff {
325374
}
326375
}
327376

328-
return maxSize >= this.minMatchedSize ? bestMatchedBlock : null
377+
return maxSize >= this.config.minMatchedSize ? bestMatchedBlock : null
329378
}
330379

331380
// use matchedBlockList walk the words to find change description
@@ -380,4 +429,63 @@ export default class HtmlDiff {
380429
}
381430
return operationList
382431
}
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+
}
383491
}

0 commit comments

Comments
 (0)