UNPKG

text-compare-vue3

Version:

A powerful text comparison plugin for Vue.js with character-level diff support

209 lines (184 loc) 5.83 kB
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 } }