fix: preserve retained formatting when aligning DOCX review text
This commit is contained in:
1 parent
104534a3a3
commit
fdb028d125
2 files changed
+95
-4
No files matched your search
@@ -311,6 +311,67 @@ const appendTokens = (paragraph: Element, selected: SelectedToken[]): void => {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer retained formatting only when it keeps the same maximal text alignment. */
|
||||
const diffTokens = (
|
||||
before: InlineToken[],
|
||||
after: InlineToken[]
|
||||
): ReturnType<typeof sequenceDiff> => {
|
||||
const oldKeys = before.map((token) => token.key),
|
||||
newKeys = after.map((token) => token.key)
|
||||
const plain = sequenceDiff(oldKeys, newKeys)
|
||||
const score = (chunks: ReturnType<typeof sequenceDiff>): { text: number; format: number } => {
|
||||
let oldIndex = 0,
|
||||
newIndex = 0,
|
||||
text = 0,
|
||||
format = 0
|
||||
for (const chunk of chunks) {
|
||||
if (!chunk.added && !chunk.removed)
|
||||
for (let index = 0; index < chunk.count; index++) {
|
||||
text++
|
||||
if (before[oldIndex + index]!.format === after[newIndex + index]!.format) format++
|
||||
}
|
||||
if (!chunk.added) oldIndex += chunk.count
|
||||
if (!chunk.removed) newIndex += chunk.count
|
||||
}
|
||||
return { text, format }
|
||||
}
|
||||
const plainScore = score(plain)
|
||||
if (plainScore.text === plainScore.format) return plain
|
||||
|
||||
const styled = sequenceDiff(
|
||||
before.map((token) => JSON.stringify([token.key, token.format])),
|
||||
after.map((token) => JSON.stringify([token.key, token.format]))
|
||||
)
|
||||
const candidate: ReturnType<typeof sequenceDiff> = []
|
||||
let oldIndex = 0,
|
||||
newIndex = 0,
|
||||
oldStart = 0,
|
||||
newStart = 0
|
||||
const appendGap = (): void => {
|
||||
// Re-align gaps as text so genuine format changes remain format-only changes.
|
||||
for (const chunk of sequenceDiff(
|
||||
oldKeys.slice(oldStart, oldIndex),
|
||||
newKeys.slice(newStart, newIndex)
|
||||
))
|
||||
candidate.push(chunk)
|
||||
}
|
||||
for (const chunk of styled) {
|
||||
if (!chunk.added && !chunk.removed) {
|
||||
appendGap()
|
||||
candidate.push({ value: newKeys.slice(newIndex, newIndex + chunk.count), count: chunk.count })
|
||||
oldStart = oldIndex + chunk.count
|
||||
newStart = newIndex + chunk.count
|
||||
}
|
||||
if (!chunk.added) oldIndex += chunk.count
|
||||
if (!chunk.removed) newIndex += chunk.count
|
||||
}
|
||||
appendGap()
|
||||
const candidateScore = score(candidate)
|
||||
return candidateScore.text === plainScore.text && candidateScore.format > plainScore.format
|
||||
? candidate
|
||||
: plain
|
||||
}
|
||||
|
||||
export type InlineResult = { element: Element; textChanged: boolean; formatChanged: boolean }
|
||||
export const renderParagraphDiff = (
|
||||
before: Element,
|
||||
@@ -380,10 +441,7 @@ export const renderParagraphDiff = (
|
||||
newIndex = 0,
|
||||
textChanged = false,
|
||||
formatChanged = paragraphFormatChanged
|
||||
const chunks = sequenceDiff(
|
||||
oldTokens.map((t) => t.key),
|
||||
newTokens.map((t) => t.key)
|
||||
)
|
||||
const chunks = diffTokens(oldTokens, newTokens)
|
||||
// Myers can emit insertion before deletion; replacements always show old before new.
|
||||
for (let i = 0; i < chunks.length - 1; i++)
|
||||
if (chunks[i]!.added && chunks[i + 1]!.removed)
|
||||
|
||||
@@ -114,6 +114,39 @@ describe('DOCX first/final net review', () => {
|
||||
expect(result.stats).toEqual({ changeCount: 1, insertCount: 0, deleteCount: 0 })
|
||||
expect(storyText(result.document)).toEqual(['前中后'])
|
||||
})
|
||||
it('aligns repeated text with the retained formatting when simplification removes an earlier occurrence', async () => {
|
||||
const before =
|
||||
'<w:p><w:r><w:t>本产品是一套面向社区公开资料整理与分享场景的 </w:t></w:r><w:r><w:rPr><w:b/></w:rPr><w:t>公开资料整理工作台</w:t></w:r><w:r><w:t>,支持资料检索。</w:t></w:r></w:p>'
|
||||
const after =
|
||||
'<w:p><w:r><w:t>本产品是一套</w:t></w:r><w:r><w:rPr><w:b/></w:rPr><w:t>公开资料整理工作台</w:t></w:r><w:r><w:t>,支持资料检索。</w:t></w:r></w:p>'
|
||||
const result = await compare(before, after)
|
||||
expect(storyText(result.document, false)).toEqual([
|
||||
'本产品是一套面向社区公开资料整理与分享场景的 公开资料整理工作台,支持资料检索。'
|
||||
])
|
||||
expect(storyText(result.document, true)).toEqual([
|
||||
'本产品是一套公开资料整理工作台,支持资料检索。'
|
||||
])
|
||||
expect(reviewRuns(result.document).filter((run) => run.kind === 'formatted')).toEqual([])
|
||||
expect(
|
||||
reviewRuns(result.document)
|
||||
.filter((run) => run.kind === 'deleted')
|
||||
.map((run) => run.text)
|
||||
.join('')
|
||||
).toBe('面向社区公开资料整理与分享场景的 ')
|
||||
})
|
||||
it('keeps real format changes when matching repeated formatting would remove unchanged text', async () => {
|
||||
const result = await compare(
|
||||
'<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>同</w:t></w:r><w:r><w:t>同</w:t></w:r></w:p>',
|
||||
'<w:p><w:r><w:t>同</w:t></w:r><w:r><w:rPr><w:b/></w:rPr><w:t>同</w:t></w:r></w:p>'
|
||||
)
|
||||
const runs = reviewRuns(result.document)
|
||||
expect(runs.every((run) => run.kind === 'formatted')).toBe(true)
|
||||
expect(runs.map((run) => run.text).join('')).toBe('同同')
|
||||
expect(result.stats.insertCount).toBe(0)
|
||||
expect(result.stats.deleteCount).toBe(0)
|
||||
expect(storyText(result.document, false)).toEqual(['同同'])
|
||||
expect(storyText(result.document, true)).toEqual(['同同'])
|
||||
})
|
||||
it('uses border when background has been removed', async () => {
|
||||
const result = await compare(p('背景', '<w:shd w:fill="FFFF00"/>'), p('背景'))
|
||||
expect(
|
||||
|
||||
Reference in new issue
Block a user