diff
<tosi-diff> renders a unified, line-by-line diff between two strings — the kind
of view you'd see in a code review, with removed lines marked - and added
lines marked +.
Set its original and modified properties (or attributes) to the two versions
of the text; it recomputes and re-renders. (They're named original/modified
rather than before/after to avoid the native Element.before()/after()
methods.)
const { tosiDiff } = tosijsui
preview.append(
tosiDiff({
original: 'one\ntwo\nthree\nfour',
modified: 'one\nTWO\nthree\nfour\nfive',
style: { width: '100%', height: '100%' },
})
)
The diff is computed with a longest-common-subsequence pass over the lines, so
unchanged lines are shown as context and only genuine insertions/deletions are
highlighted. The pure diffLines(before, after) function is exported too, if you
just want the data.
Resolving a diff — "do I want to accept this?"
Set resolvable and each change gets a pair of buttons: keep the original, or take the
modified. value is the text those choices imply, and a change event fires whenever one
moves. That makes it a review surface for a proposed edit — an AI suggestion, a
collaborator's revision, a file that moved under you while you were editing it.
The labels are yours. "Mine"/"Theirs" is git's framing, "Current"/"Proposed" suits reviewing a suggestion, and the component's own vocabulary is original/modified — so nothing is hardcoded. Pass already-localized strings; the host owns that choice.
const { tosiDiff } = tosijsui
const diff = tosiDiff({
original: 'The cat sat on the mat.\nIt was a sunny day.\nThe end.',
modified: 'The cat sprawled across the mat.\nIt was a sunny day.\nFin.',
resolvable: true,
originalLabel: 'Mine',
modifiedLabel: 'Theirs',
style: { width: '100%' },
})
diff.addEventListener('change', () => {
result.textContent = JSON.stringify(diff.value)
})
const result = document.createElement('pre')
result.textContent = JSON.stringify(diff.value)
preview.append(diff, result)
const { tosiDiff } = tosijsui
const el = tosiDiff({
original: 'a\nKEEP\nc\nDROP\ne',
modified: 'a\nNEW1\nc\nNEW2\ne',
resolvable: true,
})
document.body.append(el)
test('accepting everything yields the modified text', () => {
expect(el.changeCount).toBe(2)
expect(el.value).toBe('a\nNEW1\nc\nNEW2\ne')
})
test('rejecting everything yields the original text exactly', () => {
el.rejectAll()
expect(el.value).toBe('a\nKEEP\nc\nDROP\ne')
el.remove()
})
The unit of decision is a change block, not a line: a multi-line edit is one choice,
because accepting half of one produces text neither side wrote. Two more exports give you
the same model without the DOM — diffBlocks(diffLines(a, b)) for the blocks, and
resolveDiff(blocks, choices) to turn choices back into text.
acceptAll() and rejectAll() do the obvious thing, changeCount tells you how many
decisions the diff is asking for (0 means the texts agree), and resolutions is readable
and writable if you want to drive it yourself. Resolutions reset when either text
changes — decisions belong to the diff they were made about.