@sanity/diff
Version:
Generates diffs between documents and primitive types
139 lines (123 loc) • 3.59 kB
text/typescript
import {
cleanupSemantic,
DIFF_DELETE,
DIFF_EQUAL,
DIFF_INSERT,
makeDiff,
} from '@sanity/diff-match-patch'
import {replaceProperty} from '../helpers'
import {type DiffOptions, type StringDiff, type StringDiffSegment, type StringInput} from '../types'
export function diffString<A>(
fromInput: StringInput<A>,
toInput: StringInput<A>,
options: DiffOptions,
): StringDiff<A> {
const fromValue = fromInput.value
const toValue = toInput.value
if (fromValue === toValue) {
return {
type: 'string',
action: 'unchanged',
isChanged: false,
fromValue,
toValue,
segments: [{type: 'stringSegment', action: 'unchanged', text: fromValue}],
}
}
return {
type: 'string',
action: 'changed',
isChanged: true,
fromValue,
toValue,
annotation: toInput.annotation,
// Compute and memoize string segments only when accessed
get segments(): StringDiffSegment<A>[] {
const segments = buildSegments(fromInput, toInput)
return replaceProperty(this, 'segments', segments)
},
}
}
function buildSegments<A>(
fromInput: StringInput<A>,
toInput: StringInput<A>,
): StringDiffSegment<A>[] {
const segments: StringDiffSegment<A>[] = []
const dmpDiffs = cleanupSemantic(makeDiff(fromInput.value, toInput.value))
let fromIdx = 0
let toIdx = 0
for (const [op, text] of dmpDiffs) {
switch (op) {
case DIFF_EQUAL:
segments.push({type: 'stringSegment', action: 'unchanged', text})
fromIdx += text.length
toIdx += text.length
break
case DIFF_DELETE:
for (const segment of fromInput.sliceAnnotation(fromIdx, fromIdx + text.length)) {
segments.push({
type: 'stringSegment',
action: 'removed',
text: segment.text,
annotation: segment.annotation,
})
}
fromIdx += text.length
break
case DIFF_INSERT:
for (const segment of toInput.sliceAnnotation(toIdx, toIdx + text.length)) {
segments.push({
type: 'stringSegment',
action: 'added',
text: segment.text,
annotation: segment.annotation,
})
}
toIdx += text.length
break
default:
throw new Error(`Unhandled diff-match-patch operation "${op}"`)
}
}
return segments
}
export function removedString<A>(
input: StringInput<A>,
toValue: null | undefined,
options: DiffOptions,
): StringDiff<A> & {action: 'removed'} {
return {
type: 'string',
action: 'removed',
isChanged: true,
fromValue: input.value,
toValue,
annotation: input.annotation,
get segments(): StringDiffSegment<A>[] {
const segments: StringDiffSegment<A>[] = input
.sliceAnnotation(0, input.value.length)
.map((segment) => ({type: 'stringSegment', action: 'removed', ...segment}))
return replaceProperty(this, 'segments', segments)
},
}
}
export function addedString<A>(
input: StringInput<A>,
fromValue: null | undefined,
options: DiffOptions,
): StringDiff<A> & {action: 'added'} {
return {
type: 'string',
action: 'added',
isChanged: true,
fromValue,
toValue: input.value,
annotation: input.annotation,
get segments(): StringDiffSegment<A>[] {
const segments: StringDiffSegment<A>[] = input
.sliceAnnotation(0, input.value.length)
.map((segment) => ({type: 'stringSegment', action: 'added', ...segment}))
return replaceProperty(this, 'segments', segments)
},
}
}