Syncfusion Word文档对象对比:差异未完全检测的问题求助
Syncfusion Word文档文本对比功能优化方案
问题背景
我通过Syncfusion Word编辑器生成了两个SFDT格式的文档对象,需要对比文本的变更情况,但当前实现的对比功能仅能检测约50%的改动,无法识别全部差异,寻求可行的解决方案。
现有对比代码
const diff = require('deep-diff'); export function DocumentDiffer(sfdtNewest: string, sfdtOlder: string): string { if (!sfdtNewest || !sfdtOlder) return JSON.stringify(sfdtNewest); const newest = JSON.parse(sfdtNewest); const older = JSON.parse(sfdtOlder); let newestSections = newest.sections; let olderSections = older.sections; newestSections.map((newestSection: any, indexSection: number) => { newestSection.blocks.map((block: any, indexBlock: number) => { let addedIndexes: number = 0; block.inlines.map((inline: any, indexInline: number) => { let finalIndex = indexInline + addedIndexes; const lineToCompare = newestSections[indexSection].blocks[indexBlock].inlines[finalIndex]; // TODO: compare in older inlines const oldSectionExist: boolean = olderSections.length >= indexSection; const oldBlockExist: boolean = olderSections[indexSection].blocks.length >= indexBlock; const oldInlineExist: boolean = olderSections[indexSection].blocks[indexBlock].inlines.length >= finalIndex; const oldInlines: boolean = olderSections[indexSection].blocks[indexBlock].inlines[finalIndex] != undefined; if (oldSectionExist && oldBlockExist && oldInlineExist && oldInlines && lineToCompare) { const oldLine = olderSections[indexSection].blocks[indexBlock].inlines[finalIndex]; const nextLine = newestSections[indexSection].blocks[indexBlock].inlines.length > finalIndex ? newestSections[indexSection].blocks[indexBlock].inlines[finalIndex + 1] : null; const next2ndLine = newestSections[indexSection].blocks[indexBlock].inlines.length > (finalIndex + 1) ? newestSections[indexSection].blocks[indexBlock].inlines[finalIndex + 2] : null; const nextLineInOld = olderSections[indexSection].blocks[indexBlock].inlines.length > finalIndex ? olderSections[indexSection].blocks[indexBlock].inlines[finalIndex + 1] : null; const changes = diff(oldLine, lineToCompare); if (nextLine && !nextLineInOld) { // text added at the end nextLine.characterFormat.fontColor = "#45b464" newestSections[indexSection].blocks[indexBlock].inlines[finalIndex + 1] = nextLine; } else { if (changes) { changes.map((change: any) => { if (change.kind == 'E') { if (change.path[0] == 'text') { if (change.lhs != " " && change.lhs != "") { // removed if (nextLine && nextLine.text == oldLine.text || (next2ndLine && next2ndLine.text == oldLine.text)) { addedIndexes += (nextLine && nextLine.text == oldLine.text ? 1 : 2); } else { oldLine.characterFormat.fontColor = "#b44545" oldLine.characterFormat.strikethrough = "SingleStrike" const inlines = newestSections[indexSection].blocks[indexBlock].inlines; const iinlines = [ ...inlines.slice(0, finalIndex), oldLine, ...inlines.slice(finalIndex) ]; newestSections[indexSection].blocks[indexBlock].inlines = iinlines; } } if (change.rhs != " " && change.rhs != "") { // added lineToCompare.characterFormat.fontColor = "#00fff6" } } } }) } } } else {} }); }); }); newest.sections = newestSections; return JSON.stringify(newest); }
现有代码的核心问题
- 索引逻辑混乱:手动维护
addedIndexes处理插入行的方式,无法应对文本移动、多行连续修改等复杂场景,极易出现索引错位,导致大量差异漏检。 - 仅匹配同位置节点:严格按
章节→块→行内元素的相同索引位置对比,完全忽略文本块的新增、删除、顺序调整(比如段落移动),一旦文档结构变化就会失效。 - diff逻辑覆盖不全:依赖
deep-diff但只处理了文本字段的修改(kind: 'E'),完全忽略了节点新增(kind: 'N')、删除(kind: 'D'),以及单行文本内的局部修改场景。 - 特殊场景处理缺失:空格、空文本的判断逻辑混乱,多行连续新增/删除的情况完全未覆盖。
优化解决方案
1. 改用文本内容级别的diff算法
放弃按节点索引硬匹配的思路,先提取两个文档的纯文本结构(保留段落/块层级),用成熟的文本diff库(比如diff-match-patch,Google开源的文本对比算法)对比内容,再将差异映射回SFDT节点结构。
2. 分层处理文档结构变化
- 先对比章节、块的结构:新增的章节/块直接标记为绿色(新增样式),删除的章节/块插入到新文档对应位置并标记红色删除线。
- 结构一致的块,再深入对比行内文本的差异。
3. 修复索引处理逻辑
不再手动维护addedIndexes,修改SFDT节点后重新遍历更新后的节点列表,从根源避免索引错位问题。
4. 完善全类型diff处理
使用deep-diff时,除了处理修改类型(E),还要覆盖:
N:新增的行内节点,直接标记为新增样式。D:删除的行内节点,插入到新文档对应位置并标记删除样式。
优化后的示例代码
import diff from 'deep-diff'; import DiffMatchPatch from 'diff-match-patch'; export function DocumentDiffer(sfdtNewest: string, sfdtOlder: string): string { if (!sfdtNewest || !sfdtOlder) return JSON.stringify(sfdtNewest); const newest = JSON.parse(sfdtNewest); const older = JSON.parse(sfdtOlder); const dmp = new DiffMatchPatch(); // 遍历所有章节 newest.sections.forEach((newSection, sectionIdx) => { const oldSection = older.sections?.[sectionIdx]; if (!oldSection) { // 整个章节为新增,标记所有内容 newSection.blocks.forEach(block => { block.inlines.forEach(inline => { inline.characterFormat = inline.characterFormat || {}; inline.characterFormat.fontColor = "#45b464"; }); }); return; } // 遍历章节内的块 newSection.blocks.forEach((newBlock, blockIdx) => { const oldBlock = oldSection.blocks?.[blockIdx]; if (!oldBlock) { // 整个块为新增,标记所有行内元素 newBlock.inlines.forEach(inline => { inline.characterFormat = inline.characterFormat || {}; inline.characterFormat.fontColor = "#45b464"; }); return; } // 提取块内文本做内容级diff const newFullText = newBlock.inlines.map(i => i.text).join(''); const oldFullText = oldBlock.inlines.map(i => i.text).join(''); const diffs = dmp.diff_main(oldFullText, newFullText); dmp.diff_cleanupSemantic(diffs); // 将diff结果映射回行内节点 let textCursor = 0; newBlock.inlines.forEach(inline => { inline.characterFormat = inline.characterFormat || {}; const inlineLen = inline.text.length; for (const [diffType, diffText] of diffs) { if (textCursor >= inlineLen) break; if (inline.text.startsWith(diffText, textCursor)) { if (diffType === 1) { // 标记新增文本 inline.characterFormat.fontColor = "#45b464"; } else if (diffType === -1) { // 插入删除的文本节点 const deletedInline = JSON.parse(JSON.stringify(inline)); deletedInline.text = diffText; deletedInline.characterFormat.fontColor = "#b44545"; deletedInline.characterFormat.strikethrough = "SingleStrike"; const insertPos = newBlock.inlines.indexOf(inline); newBlock.inlines.splice(insertPos, 0, deletedInline); } textCursor += diffText.length; } } }); // 处理旧块存在但新块已删除的行内节点 oldBlock.inlines.forEach(oldInline => { const exists = newBlock.inlines.some(newInline => newInline.text === oldInline.text); if (!exists) { oldInline.characterFormat = oldInline.characterFormat || {}; oldInline.characterFormat.fontColor = "#b44545"; oldInline.characterFormat.strikethrough = "SingleStrike"; newBlock.inlines.push(oldInline); } }); }); }); return JSON.stringify(newest); }
内容的提问来源于stack exchange,提问作者Daniel Díaz
相关产品推荐
相关产品推荐

