Commit 7f96abe1 by qlintonger xeno

feat: 使用diff_match_patch来对比完毕+1

parent 98bdf0a0
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"diff": "^7.0.0",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
"less": "^4.2.0", "less": "^4.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
...@@ -1949,6 +1950,15 @@ ...@@ -1949,6 +1950,15 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true "dev": true
}, },
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dlv": { "node_modules/dlv": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"diff": "^7.0.0",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
"less": "^4.2.0", "less": "^4.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
......
...@@ -2,14 +2,15 @@ ...@@ -2,14 +2,15 @@
<!--Arbortext, Inc., 1988-2013, v.4002--> <!--Arbortext, Inc., 1988-2013, v.4002-->
<!DOCTYPE JOBCARD PUBLIC "-//CEA-TEXT//DTD JOBCARD-VER1//EN" "JOBCARD.dtd" [ <!DOCTYPE JOBCARD PUBLIC "-//CEA-TEXT//DTD JOBCARD-VER1//EN" "JOBCARD.dtd" [
<!ENTITY nbsp "&#160;"> <!ENTITY nbsp "&#160;">
<!ENTITY rsquo "&#8217;"> <!ENTITY rsquo "&#8217;">
]> ]>
<JOBCARD> <JOBCARD>
<EOTK-HEADER></EOTK-HEADER> <EOTK-HEADER></EOTK-HEADER>
<CEP> <CEP>
<EFFECT EFFRG="001999"></EFFECT> <EFFECT EFFRG="001999"></EFFECT>
<TITLEC>发动机QEC拆卸(V2500-A5系列)</TITLEC> <TITLEC>发动机QEC拆卸(V2500-A5系列)</TITLEC>
<TITLE>Remove the Engine's QEC(V2500-A5 series)XXXX</TITLE> <TITLE>Remove the Engine's QEC(V2500-A5 series)XXX</TITLE>
<TITLE>Remove the Engine's QEC(V2500-A5 series)XXX</TITLE>
<TOPIC CK-LEVEL="C"> <TOPIC CK-LEVEL="C">
<TITLEC>飞机/发动机基本信息</TITLEC> <TITLEC>飞机/发动机基本信息</TITLEC>
<TITLE>AIRCRAFT/ENGINE INFORMATION</TITLE> <TITLE>AIRCRAFT/ENGINE INFORMATION</TITLE>
...@@ -26,6 +27,10 @@ ...@@ -26,6 +27,10 @@
</NOTE> </NOTE>
<SIGNOFF/> <SIGNOFF/>
</STEP> </STEP>
</TOPIC>
<TOPIC CK-LEVEL="C">
<TITLEC>飞机/发动机基本信息</TITLEC>
<TITLE>AIRCRAFT/ENGINE INFORMATION</TITLE>
<STEP CK-LEVEL="C"> <STEP CK-LEVEL="C">
<EFFECT EFFRG="001999"></EFFECT> <EFFECT EFFRG="001999"></EFFECT>
<RECORD-LINE> <RECORD-LINE>
......
// 引入 TreeRenderResult 类型,该类型定义在 @/lib/XMLProcessor/src/typing 模块中 // 引入 TreeRenderResult 类型,该类型定义在 @/lib/XMLProcessor/src/typing 模块中
import { NewTreeModification, OldTreeModification, TreeRenderResult, TreeRenderResultFlatted } from '@/lib/XMLProcessor/src/typing' import { TreeReconstructed, TreeRenderResult, TreeRenderResultFlatted } from '@/lib/XMLProcessor/src/typing'
import { md5 } from 'js-md5' import { md5 } from 'js-md5'
import { getUUID } from '../utils/uuid.ts' import { getUUID } from '../utils/uuid.ts'
import {diff_match_patch} from '@/views/Compare/lib/DiffMatchPatch.js' // @ts-ignore
import { diffLines } from 'diff'
function dualCompareFromHashString(stringA: string, stringB: string) { import {reconstructTree} from '@/lib/XMLProcessor/src/core/buildTree.ts'
const af = new diff_match_patch();
const diff_result = af.diff_main(stringA, stringB)
console.log('after-diff', af.diff_main(stringA, stringB), {stringA, stringB})
}
// 定义 Processing 类,用于处理 XML 数据 // 定义 Processing 类,用于处理 XML 数据
export class Processing { export class Processing {
...@@ -45,58 +41,7 @@ export class Processing { ...@@ -45,58 +41,7 @@ export class Processing {
return resp return resp
} }
private buildTreeFromFlatted( dualCompareFromString(xmlStringOld: string, xmlStringNew: string, bothHandledNode: string[], contentHoldNode: string[]) {
flatted: TreeRenderResultFlatted[],
): TreeRenderResult[] {
const result: TreeRenderResult[] = []
if (!flatted.length) {
return result
}
const flattedTreeSorted = flatted.toSorted((a, b) => {
return a.chained.length - b.chained.length;
});
const buildTree = (singleItem: TreeRenderResultFlatted, flattedDataSet: TreeRenderResultFlatted[]) => {
const singleItemRendered: TreeRenderResult = Object.assign({}, singleItem, {children: []});
const currentChainedLength = singleItemRendered.chained.length;
const maxChainedLength = Math.max(...flattedDataSet.map(a=>a.chained.length));
const currentChainedPrefix = singleItemRendered.chained.join('.');
for (let i = currentChainedLength + 1; i<= maxChainedLength; i++) {
const allSubItems = flattedDataSet.filter(a=>a.chained.length === i && a.chained.join('.').startsWith(currentChainedPrefix));
if (allSubItems.length) {
for (let j = flattedDataSet.length - 1; j>=0; j--) {
if (allSubItems.includes(flattedDataSet[j])) {
flattedDataSet.splice(j, 1)
}
}
for (const item of allSubItems) {
const subTree = buildTree(item, flattedDataSet);
singleItemRendered.children?.push(subTree)
}
}
}
return singleItemRendered
}
for (const item of flattedTreeSorted) {
result.push(buildTree(item, flattedTreeSorted))
}
return result
}
dualCompareFromString(
xmlStringOld: string,
xmlStringNew: string,
bothHandledNode: string[],
contentHoldNode: string[]
): {
dataForNew: NewTreeModification
dataForOld: OldTreeModification
domOld: Document
domNew: Document
treeOld: TreeRenderResult[]
treeNew: TreeRenderResult[]
xmlContentOld: string
xmlContentNew: string
} {
const treeA = this.domParser.parseFromString(xmlStringOld, 'text/xml') const treeA = this.domParser.parseFromString(xmlStringOld, 'text/xml')
const treeB = this.domParser.parseFromString(xmlStringNew, 'text/xml') const treeB = this.domParser.parseFromString(xmlStringNew, 'text/xml')
const resultA = this.innerHandle(treeA.documentElement, bothHandledNode, contentHoldNode) const resultA = this.innerHandle(treeA.documentElement, bothHandledNode, contentHoldNode)
...@@ -107,34 +52,203 @@ export class Processing { ...@@ -107,34 +52,203 @@ export class Processing {
const resultAFlatted = this.flattenTree(resultA) const resultAFlatted = this.flattenTree(resultA)
const resultBFlatted = this.flattenTree(resultB) const resultBFlatted = this.flattenTree(resultB)
const resultATree = resultAFlatted.map(item => `${item.hash}`).join('\r\n'); const resultATree = resultAFlatted.map((item) => `${item.hash}`)
const resultBTree = resultBFlatted.map(item => `${item.hash}`).join('\r\n'); const resultBTree = resultBFlatted.map((item) => `${item.hash}`)
dualCompareFromHashString(resultATree, resultBTree)
const results: any[] = []
// 分组构建树 const diffResp = diffLines(resultATree.join('\n'), resultBTree.join('\n'), {
const allDeletedTree = results.filter((item) => item.type === 'deleted').map(a=>a.item); ignoreWhitespace: true,
ignoreNewlineAtEof: true,
stripTrailingCr: true,
newlineIsToken: false
}).map((item: any) => ({
...item,
value: item.value.split('\n').filter((a: string) => a.length > 0)
}))
const allAddedTree = results.filter((item) => item.type === 'added').map(a=>a.item); let constructNodeA: TreeReconstructed[] = []
let constructNodeB: TreeReconstructed[] = []
const dataForOld: OldTreeModification = { for (let i = 0; i < diffResp.length; i++) {
Deleted: this.buildTreeFromFlatted(allDeletedTree) const currentItem = diffResp[i]
if (!currentItem.added && !currentItem.removed) {
// 由于完全相同,构建相同节点即可
let treeStartForA = resultAFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeA.find((v) => v.key === a.key))
if (treeStartForA !== -1) {
let aMapped = currentItem.value
.map((_: any, index: number) => {
return resultAFlatted[treeStartForA + index]
})
.map((a: any) => Object.assign({}, a, { type: null }))
constructNodeA.push(...aMapped)
}
let treeStartForB = resultBFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeB.find((v) => v.key === a.key))
if (treeStartForB !== -1) {
let bMapped = currentItem.value
.map((_: any, index: number) => {
return resultBFlatted[treeStartForB + index]
})
.map((a: any) => Object.assign({}, a, { type: null }))
constructNodeB.push(...bMapped)
} }
const dataForNew: NewTreeModification = { continue
Added: this.buildTreeFromFlatted(allAddedTree)
} }
const nextTerm = diffResp[i + 1]
if (!nextTerm) {
if (currentItem.added) {
let treeStartA = resultBFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeB.find((v) => v.key === a.key))
if (treeStartA !== -1) {
let aMapped = currentItem.value
.map((_: any, index: number) => {
return resultBFlatted[treeStartA + index]
})
.map((a: any) => Object.assign({}, a, { type: 'added' }))
constructNodeB.push(...aMapped)
constructNodeA.push(...aMapped.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
} else if (currentItem.removed) {
let treeStartB = resultAFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeA.find((v) => v.key === a.key))
if (treeStartB !== -1) {
let bMapped = currentItem.value
.map((_: any, index: number) => {
return resultAFlatted[treeStartB + index]
})
.map((a: any) => Object.assign({}, a, { type: 'removed' }))
constructNodeA.push(...bMapped)
constructNodeB.push(...bMapped.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
}
continue
}
// 出现修改,则应该出现added和removed成对出现的情况
if (currentItem.added && nextTerm.removed) {
const minLength = Math.min(currentItem.value.length, nextTerm.value.length)
let treeStartA = resultBFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeB.find((v) => v.key === a.key))
if (treeStartA !== -1) {
let aMapped = currentItem.value
.slice(0, minLength)
.map((_: any, index: number) => {
return resultBFlatted[treeStartA + index]
})
.map((a: any) => Object.assign({}, a, { type: 'deleted' }))
constructNodeB.push(...aMapped)
}
let treeStartB = resultAFlatted.findIndex((a) => a.hash === nextTerm.value[0] && !constructNodeA.find((v) => v.key === a.key))
if (treeStartB !== -1) {
let bMapped = nextTerm.value
.slice(0, minLength)
.map((_: any, index: number) => {
return resultAFlatted[treeStartB + index]
})
.map((a: any) => Object.assign({}, a, { type: 'added' }))
constructNodeA.push(...bMapped)
}
// 根据情况插入对齐
if (currentItem.value.length > nextTerm.value.length) {
const allExtraAdded = currentItem.value.slice(minLength)
let treeAddedForA = resultBFlatted.findIndex((a) => a.hash === allExtraAdded[0] && !constructNodeB.find((v) => v.key === a.key))
if (treeAddedForA !== -1) {
let aMapped = allExtraAdded
.map((_: any, index: number) => {
return resultBFlatted[treeAddedForA + index]
})
.map((a: any) => Object.assign({}, a, { type: null }))
constructNodeB.push(...aMapped)
// 再为A补齐
constructNodeA.push(...aMapped.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
} else if (currentItem.value.length < nextTerm.value.length) {
const allExtraRemoved = nextTerm.value.slice(minLength)
let treeAddedForB = resultAFlatted.findIndex((a) => a.hash === allExtraRemoved[0] && !constructNodeA.find((v) => v.key === a.key))
if (treeAddedForB !== -1) {
let bMapped = allExtraRemoved
.map((_: any, index: number) => {
return resultAFlatted[treeAddedForB + index]
})
.map((a: any) => Object.assign({}, a, { type: 'deleted' }))
constructNodeA.push(...bMapped)
constructNodeB.push(...bMapped.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
}
i++
} else if (currentItem.removed && nextTerm.added) {
const minLength = Math.min(currentItem.value.length, nextTerm.value.length)
let treeStartA = resultAFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeA.find((v) => v.key === a.key))
if (treeStartA !== -1) {
let aMapped = currentItem.value
.slice(0, minLength)
.map((_: any, index: number) => {
return resultAFlatted[treeStartA + index]
})
.map((a: any) => Object.assign({}, a, { type: 'removed' }))
constructNodeA.push(...aMapped)
}
let treeStartB = resultBFlatted.findIndex((a) => a.hash === nextTerm.value[0] && !constructNodeB.find((v) => v.key === a.key))
if (treeStartB !== -1) {
let bMapped = nextTerm.value
.slice(0, minLength)
.map((_: any, index: number) => {
return resultBFlatted[treeStartB + index]
})
.map((a: any) => Object.assign({}, a, { type: 'added' }))
constructNodeB.push(...bMapped)
}
if (currentItem.value.length > nextTerm.value.length) {
const allExtraRemoved = currentItem.value.slice(minLength)
let treeAddedForB = resultAFlatted.findIndex((a) => a.hash === allExtraRemoved[0] && !constructNodeA.find((v) => v.key === a.key))
if (treeAddedForB !== -1) {
let bMapped = allExtraRemoved
.map((_: any, index: number) => {
return resultAFlatted[treeAddedForB + index]
})
.map((a: any) => Object.assign({}, a, { type: 'removed' }))
constructNodeA.push(...bMapped)
constructNodeB.push(...bMapped.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
} else if (currentItem.value.length < nextTerm.value.length) {
const allExtraAdded = nextTerm.value.slice(minLength)
let treeAddedForA = resultBFlatted.findIndex((a) => a.hash === allExtraAdded[0] && !constructNodeB.find((v) => v.key === a.key))
if (treeAddedForA !== -1) {
let aMapped = allExtraAdded
.map((_: any, index: number) => {
return resultBFlatted[treeAddedForA + index]
})
.map((a: any) => Object.assign({}, a, { type: 'added' }))
constructNodeB.push(...aMapped)
constructNodeA.push(...aMapped.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
}
i++
}
// 如果不是出现成对的情况,则需要直接补位
// 额外添加的,旧的树需要添加
else if (currentItem.added && !nextTerm.removed) {
const addedForNew = resultBFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeB.find((v) => v.key === a.key))
if (addedForNew > -1) {
let mappedData = currentItem.value
.map((_: any, index: number) => {
return resultBFlatted[addedForNew + index]
})
.map((a: any) => Object.assign({}, a, { type: 'added' }))
constructNodeB.push(...mappedData)
constructNodeA.push(...mappedData.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
} else if (currentItem.removed && !nextTerm.added) {
const removedForOld = resultAFlatted.findIndex((a) => a.hash === currentItem.value[0] && !constructNodeA.find((v) => v.key === a.key))
if (removedForOld > -1) {
let mappedData = currentItem.value
.map((_: any, index: number) => {
return resultAFlatted[removedForOld + index]
})
.map((a: any) => Object.assign({}, a, { type: 'removed' }))
constructNodeA.push(...mappedData)
constructNodeB.push(...mappedData.map((a: any) => Object.assign({}, a, { type: 'placeholder' })))
}
}
}
const treeOld = reconstructTree(constructNodeA)
const treeNew = reconstructTree(constructNodeB)
return { return {
dataForNew, treeOld, treeNew
dataForOld,
domOld: treeA,
domNew: treeB,
treeOld: resultA,
treeNew: resultB,
xmlContentOld: this.serializeXML(treeA),
xmlContentNew: this.serializeXML(treeB)
} }
} }
......
export function reconstructTree(data: any[]) {
// Step 1: Sort data by chained array to ensure parent-child order
const sortedData = [...data].sort((a, b) => {
const aChained = a.chained;
const bChained = b.chained;
for (let i = 0; i < Math.max(aChained.length, bChained.length); i++) {
const aVal = aChained[i] || 0;
const bVal = bChained[i] || 0;
if (aVal !== bVal) return aVal - bVal;
}
return 0;
});
// Step 2: Build a map for quick lookup and to track children
const nodeMap = new Map();
const result: any[] = [];
sortedData.forEach(item => {
nodeMap.set(item.key, { ...item, children: [] });
});
// Step 3: Assign children to parents based on chained hierarchy
sortedData.forEach(item => {
const node = nodeMap.get(item.key);
const chained = item.chained;
// Find the parent by looking for the closest shorter chained array
let parent = null;
for (let i = chained.length - 1; i > 0; i--) {
const parentChained = chained.slice(0, i);
const parentKey = sortedData.find(d =>
d.chained.length === parentChained.length &&
d.chained.every((val: any, idx: number) => val === parentChained[idx])
)?.key;
if (parentKey) {
parent = nodeMap.get(parentKey);
break;
}
}
if (parent) {
parent.children.push(node);
} else {
result.push(node);
}
});
// Step 4: Convert to XML
function createXmlNode(node: any, doc: Document) {
const element = doc.createElement(node.label);
// Set data-key attribute
element.setAttribute('data-key', node.key);
// Set data-type attribute if type is not null
if (node.type !== null) {
element.setAttribute('data-type', node.type);
}
// Set textContent for TITLE or PARA nodes
if (node.label === 'TITLE' || node.label === 'PARA') {
element.textContent = node.textContent;
}
// Recursively add children
node.children.forEach((child: any) => {
element.appendChild(createXmlNode(child, doc));
});
return element;
}
// Create XML document
const doc = document.implementation.createDocument('', '', null);
// Use the first element (JOBCARD) as the root if it exists
if (result.length > 0) {
const rootNode = createXmlNode(result[0], doc)
doc.appendChild(rootNode)
}
// Serialize to XML string
const serializer = new XMLSerializer();
return serializer.serializeToString(doc);
}
\ No newline at end of file
import { TreeRenderResultFlatted } from '@/lib/XMLProcessor/src/typing'
import { cloneDeep } from 'lodash'
export function dualCompare(oldItems: TreeRenderResultFlatted[], newItems: TreeRenderResultFlatted[]) {
const oldItemsCount = oldItems.length
const newItemsCount = newItems.length
const dp: number[][] = Array.from({ length: oldItemsCount + 1 }, () => Array(newItemsCount + 1).fill(0))
for (let i = 1; i <= oldItemsCount; i++) {
for (let j = 1; j <= newItemsCount; j++) {
if (oldItems[i - 1].hash === newItems[j - 1].hash) {
dp[i][j] = dp[i - 1][j - 1] + 1
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
}
}
}
let i = oldItemsCount,
j = newItemsCount
const result: Array<{ type: 'same' | 'deleted' | 'added' | 'changedOld' | 'changedNew'; item: TreeRenderResultFlatted }> = []
while (i > 0 && j > 0) {
if (oldItems[i - 1].hash === newItems[j - 1].hash) {
// result.unshift({ type: 'same', item: cloneDeep(oldItems[i - 1]) })
i--
j--
} else if (dp[i - 1][j] > dp[i][j - 1]) {
result.unshift({
type: 'deleted',
item: cloneDeep(oldItems[i - 1])
})
i--
} else {
result.unshift({
type: 'added',
item: cloneDeep(newItems[j - 1])
})
j--
}
}
while (i > 0) {
result.unshift({
type: 'deleted',
item: cloneDeep(oldItems[i - 1])
})
i--
}
while (j > 0) {
result.unshift({
type: 'added',
item: cloneDeep(newItems[j - 1])
})
j--
}
return result
}
...@@ -19,6 +19,10 @@ export type TreeRenderResultFlatted = { ...@@ -19,6 +19,10 @@ export type TreeRenderResultFlatted = {
node: Element node: Element
} }
export type TreeReconstructed = TreeRenderResultFlatted & {
type: 'added' | 'removed' | null | 'placeholder'
}
export type OldTreeModification = { export type OldTreeModification = {
Deleted: TreeRenderResultFlatted[] Deleted: TreeRenderResultFlatted[]
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment