text-compare-vue3
Version:
A powerful text comparison plugin for Vue.js with character-level diff support
209 lines (184 loc) • 5.83 kB
text/typescript
import { DiffOptions, DiffResult, DiffPart } from '../types'
export class DiffEngine {
private options: Required<DiffOptions>
constructor(options: DiffOptions = {}) {
this.options = {
ignoreCase: options.ignoreCase ?? false,
ignoreSpace: options.ignoreSpace ?? false,
timeout: options.timeout ?? 5000,
ignoreRepeat: options.ignoreRepeat ?? true
}
}
public compare(oldText: string, newText: string): DiffResult {
// 预处理文本
if (this.options.ignoreCase) {
oldText = oldText.toLowerCase()
newText = newText.toLowerCase()
}
if (this.options.ignoreSpace) {
oldText = oldText.trim()
newText = newText.trim()
}
// 检查重复内容
if (!this.options.ignoreRepeat) {
const duplicates = this.countDuplicates(oldText, newText)
if (duplicates > 0) {
return {
oldParts: [{ value: oldText, removed: true }],
newParts: [{ value: newText, added: true }],
statistics: {
additions: 1,
deletions: 1,
changes: 0,
duplicates
}
}
}
}
// 如果文本完全相同,直接返回
if (oldText === newText) {
return {
oldParts: [{ value: oldText }],
newParts: [{ value: newText }],
statistics: {
additions: 0,
deletions: 0,
changes: 0,
duplicates: 0
}
}
}
// 如果其中一个文本为空,直接返回
if (!oldText || !newText) {
return {
oldParts: [{ value: oldText || '', removed: !oldText }],
newParts: [{ value: newText || '', added: !newText }],
statistics: {
additions: !oldText ? 1 : 0,
deletions: !newText ? 1 : 0,
changes: 0,
duplicates: 0
}
}
}
// 计算差异
const startTime = Date.now()
const matrix = this.createLCSMatrix(oldText, newText)
const diff = this.backtrack(matrix, oldText, newText)
// 检查是否超时
if (Date.now() - startTime > this.options.timeout) {
console.warn('Diff computation timed out')
return {
oldParts: [{ value: oldText, removed: true }],
newParts: [{ value: newText, added: true }],
statistics: {
additions: 1,
deletions: 1,
changes: 0,
duplicates: 0
}
}
}
return diff
}
private createLCSMatrix(oldText: string, newText: string): number[][] {
const matrix: number[][] = Array(oldText.length + 1)
.fill(0)
.map(() => Array(newText.length + 1).fill(0))
for (let i = 1; i <= oldText.length; i++) {
for (let j = 1; j <= newText.length; j++) {
if (oldText[i - 1] === newText[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1] + 1
} else {
matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1])
}
}
}
return matrix
}
private backtrack(matrix: number[][], oldText: string, newText: string): DiffResult {
let i = oldText.length
let j = newText.length
const oldParts: DiffPart[] = []
const newParts: DiffPart[] = []
let currentOldPart = ''
let currentNewPart = ''
let additions = 0
let deletions = 0
let changes = 0
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && oldText[i - 1] === newText[j - 1]) {
currentOldPart = oldText[i - 1] + currentOldPart
currentNewPart = newText[j - 1] + currentNewPart
i--
j--
} else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
if (currentOldPart) {
oldParts.unshift({ value: currentOldPart })
newParts.unshift({ value: currentNewPart })
currentOldPart = ''
currentNewPart = ''
}
currentNewPart = newText[j - 1] + currentNewPart
additions++
j--
} else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
if (currentOldPart) {
oldParts.unshift({ value: currentOldPart })
newParts.unshift({ value: currentNewPart })
currentOldPart = ''
currentNewPart = ''
}
currentOldPart = oldText[i - 1] + currentOldPart
deletions++
i--
}
}
if (currentOldPart || currentNewPart) {
oldParts.unshift({ value: currentOldPart })
newParts.unshift({ value: currentNewPart })
}
// 合并连续的相同类型部分
const mergedOldParts = this.mergeParts(oldParts, true)
const mergedNewParts = this.mergeParts(newParts, false)
return {
oldParts: mergedOldParts,
newParts: mergedNewParts,
statistics: {
additions,
deletions,
changes,
duplicates: this.countDuplicates(oldText, newText)
}
}
}
private mergeParts(parts: DiffPart[], isOld: boolean): DiffPart[] {
const merged: DiffPart[] = []
let current: DiffPart | null = null
for (const part of parts) {
if (!part.value) continue
const type = isOld ? 'removed' : 'added'
const isDiff = part[type]
if (!current || current[type] !== isDiff) {
if (current) merged.push(current)
current = { value: part.value }
if (isDiff) current[type] = true
} else {
current.value += part.value
}
}
if (current) merged.push(current)
return merged
}
private countDuplicates(oldText: string, newText: string): number {
const oldWords = oldText.split(/\s+/).filter(Boolean)
const newWords = newText.split(/\s+/).filter(Boolean)
const oldSet = new Set(oldWords)
const newSet = new Set(newWords)
let duplicates = 0
for (const word of oldSet) {
if (newSet.has(word)) duplicates++
}
return duplicates
}
}