Skip to content

Commit

Permalink
feat: classNames config
Browse files Browse the repository at this point in the history
  • Loading branch information
Arman19941113 committed Nov 16, 2024
1 parent 37745b5 commit 655fd53
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 129 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/github-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Build example
run: cd examples/basic && pnpm run build

- name: Copy public
run: cp public/** examples/basic/dist

- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
Expand Down
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Html Diff

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

## Install

Expand All @@ -20,15 +20,23 @@ const newHtml = `<div>hello world</div>`
const diff = new HtmlDiff(oldHtml, newHtml)
const unifiedContent = diff.getUnifiedContent()
const sideBySideContents = diff.getSideBySideContents()

// custom config
const diff = new HtmlDiff(oldHtml, newHtml, {
minMatchedSize: 3,
classNames: {
createText: 'cra-txt',
deleteText: 'del-txt',
createInline: 'cra-inl',
deleteInline: 'del-inl',
createBlock: 'cra-blo',
deleteBlock: 'del-blo',
},
})
```

## Preview

### unified differences

![home](https://arman19941113.github.io/html-diff/unified.png)

### side-by-side differences

![home](https://arman19941113.github.io/html-diff/sidebyside.png)
[See online demo...](https://arman19941113.github.io/html-diff/)

![home](https://arman19941113.github.io/html-diff/doc/demo.png)
Binary file removed doc/sidebyside.png
Binary file not shown.
Binary file removed doc/unified.png
Binary file not shown.
65 changes: 0 additions & 65 deletions packages/html-diff/src/dress-up.ts

This file was deleted.

158 changes: 133 additions & 25 deletions packages/html-diff/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,91 @@
export interface MatchedBlock {
interface MatchedBlock {
oldStart: number
oldEnd: number
newStart: number
newEnd: number
size: number
}

export interface Operation {
interface Operation {
oldStart: number
oldEnd: number
newStart: number
newEnd: number
type: 'equal' | 'delete' | 'create' | 'replace'
}

import {
dressUpDiffContent,
htmlImgTagReg,
htmlTagReg,
htmlVideoTagReg,
} from './dress-up'
type BaseOpType = 'delete' | 'create'

interface HtmlDiffConfig {
minMatchedSize: number
classNames: {
createText: string
deleteText: string
createInline: string
deleteInline: string
createBlock: string
deleteBlock: string
}
}

export interface HtmlDiffOptions {
minMatchedSize?: number
classNames?: Partial<{
createText?: string
deleteText?: string
createInline?: string
deleteInline?: string
createBlock?: string
deleteBlock?: string
}>
}

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

const htmlTagReg = /^<[^>]+>/
const htmlImgTagReg = /^<img[^>]*>$/
const htmlVideoTagReg = /^<video[^>]*>.*?<\/video>$/ms

export default class HtmlDiff {
minMatchedSize: number
readonly config: HtmlDiffConfig
readonly oldWords: string[] = []
readonly newWords: string[] = []
readonly matchedBlockList: MatchedBlock[] = []
readonly operationList: Operation[] = []

unifiedContent?: string
sideBySideContents?: string[]
sideBySideContents?: [string, string]

constructor(oldHtml: string, newHtml: string, minMatchedSize = 2) {
this.minMatchedSize = minMatchedSize
constructor(
oldHtml: string,
newHtml: string,
{
minMatchedSize = 2,
classNames = {
createText: 'html-diff-create-text-wrapper',
deleteText: 'html-diff-delete-text-wrapper',
createInline: 'html-diff-create-inline-wrapper',
deleteInline: 'html-diff-delete-inline-wrapper',
createBlock: 'html-diff-create-block-wrapper',
deleteBlock: 'html-diff-delete-block-wrapper',
},
}: HtmlDiffOptions = {},
) {
// init config
this.config = {
minMatchedSize,
classNames: {
createText: 'html-diff-create-text-wrapper',
deleteText: 'html-diff-delete-text-wrapper',
createInline: 'html-diff-create-inline-wrapper',
deleteInline: 'html-diff-delete-inline-wrapper',
createBlock: 'html-diff-create-block-wrapper',
deleteBlock: 'html-diff-delete-block-wrapper',
...classNames,
},
}

// no need to diff
if (oldHtml === newHtml) {
this.unifiedContent = oldHtml
this.sideBySideContents = [oldHtml, newHtml]
Expand Down Expand Up @@ -66,13 +115,13 @@ export default class HtmlDiff {
}
break
case 'delete':
result += dressUpDiffContent(
result += this.dressUpDiffContent(
'delete',
this.oldWords.slice(operation.oldStart, operation.oldEnd),
)
break
case 'create':
result += dressUpDiffContent(
result += this.dressUpDiffContent(
'create',
this.newWords.slice(operation.newStart, operation.newEnd),
)
Expand Down Expand Up @@ -119,7 +168,7 @@ export default class HtmlDiff {
}

// deal normal tag
result += dressUpDiffContent('delete', deleteOfWords)
result += this.dressUpDiffContent('delete', deleteOfWords)
deleteOfWords.splice(0)
let isTagInNewFind = false
for (
Expand All @@ -136,7 +185,7 @@ export default class HtmlDiff {
) {
// find first matched tag, but not maybe the expected tag(to optimize)
isTagInNewFind = true
result += dressUpDiffContent('create', createOfWords)
result += this.dressUpDiffContent('create', createOfWords)
result += createWord
createOfWords.splice(0)
createIndex = tempCreateIndex + 1
Expand All @@ -157,8 +206,8 @@ export default class HtmlDiff {
if (createIndex < operation.newEnd) {
createOfWords.push(...this.newWords.slice(createIndex, operation.newEnd))
}
result += dressUpDiffContent('delete', deleteOfWords)
result += dressUpDiffContent('create', createOfWords)
result += this.dressUpDiffContent('delete', deleteOfWords)
result += this.dressUpDiffContent('create', createOfWords)
break
default:
const exhaustiveCheck: never = operation.type
Expand Down Expand Up @@ -198,31 +247,31 @@ export default class HtmlDiff {
break
case 'delete':
const deleteWords = this.oldWords.slice(operation.oldStart, operation.oldEnd)
oldHtml += dressUpDiffContent('delete', deleteWords)
oldHtml += this.dressUpDiffContent('delete', deleteWords)
break
case 'create':
const createWords = this.newWords.slice(operation.newStart, operation.newEnd)
newHtml += dressUpDiffContent('create', createWords)
newHtml += this.dressUpDiffContent('create', createWords)
break
case 'replace':
const deleteOfReplaceWords = this.oldWords.slice(
operation.oldStart,
operation.oldEnd,
)
oldHtml += dressUpDiffContent('delete', deleteOfReplaceWords)
oldHtml += this.dressUpDiffContent('delete', deleteOfReplaceWords)
const createOfReplaceWords = this.newWords.slice(
operation.newStart,
operation.newEnd,
)
newHtml += dressUpDiffContent('create', createOfReplaceWords)
newHtml += this.dressUpDiffContent('create', createOfReplaceWords)
break
default:
const exhaustiveCheck: never = operation.type
console.error('Error operation type: ' + exhaustiveCheck)
}
})

const result = [oldHtml, newHtml]
const result: [string, string] = [oldHtml, newHtml]
this.sideBySideContents = result
return result
}
Expand Down Expand Up @@ -325,7 +374,7 @@ export default class HtmlDiff {
}
}

return maxSize >= this.minMatchedSize ? bestMatchedBlock : null
return maxSize >= this.config.minMatchedSize ? bestMatchedBlock : null
}

// use matchedBlockList walk the words to find change description
Expand Down Expand Up @@ -380,4 +429,63 @@ export default class HtmlDiff {
}
return operationList
}

private dressUpDiffContent(type: BaseOpType, words: string[]): string {
const wordsLength = words.length
if (!wordsLength) {
return ''
}

let result = ''
let textStartIndex = 0
for (let i = 0; i < wordsLength; i++) {
const word = words[i]
// this word is html tag
if (word.match(htmlTagReg)) {
// deal text words before
if (i > textStartIndex) {
result += this.dressUpText(type, words.slice(textStartIndex, i))
}
// deal this tag
textStartIndex = i + 1
if (word.match(htmlVideoTagReg)) {
result += this.dressUpBlockTag(type, word)
} else if ([htmlImgTagReg].some(item => word.match(item))) {
result += this.dressUpInlineTag(type, word)
} else {
result += word
}
}
}
if (textStartIndex < wordsLength) {
result += this.dressUpText(type, words.slice(textStartIndex))
}
return result
}

private dressUpText(type: BaseOpType, words: string[]): string {
const text = words.join('')
if (!text.trim()) return ''
if (type === 'create')
return `<span class="${this.config.classNames.createText}">${text}</span>`
if (type === 'delete')
return `<span class="${this.config.classNames.deleteText}">${text}</span>`
return ''
}

private dressUpInlineTag(type: BaseOpType, word: string): string {
if (type === 'create')
return `<span class="${this.config.classNames.createInline}">${word}</span>`
if (type === 'delete')
return `<span class="${this.config.classNames.deleteInline}">${word}</span>`
return ''
}

private dressUpBlockTag(type: BaseOpType, word: string): string {
if (type === 'create')
return `<div class="${this.config.classNames.createBlock}">${word}</div>`
if (type === 'delete')
return `<div class="${this.config.classNames.deleteBlock}">${word}</div>`
return ''
}
}
Loading

0 comments on commit 655fd53

Please sign in to comment.