How String Compare works
Most nights I'm comparing two versions of something — a config value, a translation string, a log line that looks the same until it isn't. Every diff tool I reached for either ran through someone else's server or buried the one line that mattered under a hundred that didn't. So I built a small one: paste two strings in, see exactly what changed, and nothing ever leaves the tab.
This is how it actually works — the algorithm, the tokenizing, and the one thing I'd still fix.
Diffing two strings is a pathfinding problem wearing a disguise. Picture a grid: one axis is your position in string A, the other your position in B. From any point, moving right skips a character in A — a deletion. Moving down skips one in B — an insertion. Moving diagonally means the two characters already match: free, no edit needed. The shortest edit script is just the shortest path across that grid, where diagonal moves cost nothing.
This is Eugene Myers' 1986 algorithm — the same idea behind git diff and GNU diff. The obvious approach, a dynamic-programming edit-distance table, compares every position in A against every position in B: O(N×M) time and memory. Fine for two words, hopeless for two files. Myers' version runs in O((N+M)·D), where D is the size of the edit script — cheap exactly when the two strings are mostly the same, which is the case that actually matters for a diff tool.
Take comparing “ABC” against “AC” — the honest answer is delete the B. Here's what the algorithm does, one edit-count d at a time:
d = 0 — start on diagonal k = 0. Free match: A = A, advance. Next pair, B vs C, doesn't match. Diagonal 0 stalls at x = 1.
d = 1 — try both neighboring diagonals.
k = −1 (an insert) reaches x = 1, y = 2 — no further match possible.
k = +1 (a delete) reaches x = 2, y = 1. Free match: C = C, advance to x = 3, y = 2 — the end. Found in 1 edit.
Walk that path back and you get: keep A, delete B, keep C. Every line inside myersDiff() is really just this — track how far each diagonal has reached, and whichever one reached further wins the next greedy step.
The algorithm never sees raw strings — it sees arrays of tokens, and what counts as a token depends on the granularity. Character mode uses Array.from(text), not text.split(''), because split chops multi-byte Unicode into broken halves. Word mode splits on runs of whitespace, runs of letters and digits, or runs of punctuation, so “Hello, world!” becomes five tokens, not two — whitespace has to be its own token or you lose spacing when equal runs get rejoined. Line mode keeps each line's trailing newline attached, so rejoining equal lines reproduces the exact line endings.
“Auto” just picks whichever of those is fast and useful for the input size — line-diffing anything huge or multiline, word-diffing past a few thousand characters, character-diffing otherwise.
Once the edit script exists, rendering it is the easy part: walk the operations once, build two DOM fragments with document.createDocumentFragment() so the browser doesn't reflow per token, and swap them in with replaceChildren. The similarity percentage in the corner is a Dice coefficient — twice the unchanged length over the combined length of both strings — computed on whatever's left after normalization, which is why “ignore whitespace” mode can push the number up even when the raw text looks nothing alike.
Two things I know are wrong. First: with “Ignore case” on, the highlighted diff shows the lowercased text, not what you actually typed — I diff the normalized tokens directly instead of mapping each one back to its original casing. Second: this keeps a full snapshot of the search at every step so backtracking is simple, which is the textbook version of the algorithm. Real diff tools use Myers' linear-space refinement instead — recursing from both ends of the strings toward the middle — to avoid that memory cost. Both are on the list.