` and `` — enough to show a translator which sentence of the
* original to look at again.
*/
final class TextDiff
{
/**
* Above this many words on either side the comparison is skipped: it is
* quadratic, and a text that long changed is better reread whole.
*/
protected const MAX_WORDS = 1500;
public static function html(?string $old, ?string $new): ?string
{
$a = self::words($old);
$b = self::words($new);
if (count($a) > self::MAX_WORDS || count($b) > self::MAX_WORDS) {
return null;
}
// Longest common subsequence, then walked back into runs.
$m = count($a);
$n = count($b);
$lcs = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
for ($i = $m - 1; $i >= 0; $i--) {
for ($j = $n - 1; $j >= 0; $j--) {
$lcs[$i][$j] = $a[$i] === $b[$j] ? $lcs[$i + 1][$j + 1] + 1 : max($lcs[$i + 1][$j], $lcs[$i][$j + 1]);
}
}
$out = [];
$i = $j = 0;
while ($i < $m || $j < $n) {
if ($i < $m && $j < $n && $a[$i] === $b[$j]) {
$out[] = ['=', $a[$i]];
$i++;
$j++;
} elseif ($i < $m && ($j === $n || $lcs[$i + 1][$j] >= $lcs[$i][$j + 1])) {
// What went comes before what replaced it.
$out[] = ['-', $a[$i]];
$i++;
} else {
$out[] = ['+', $b[$j]];
$j++;
}
}
$html = '';
$run = null;
$buffer = [];
$flush = function () use (&$html, &$run, &$buffer): void {
if ($buffer === []) {
return;
}
$text = e(implode(' ', $buffer));
$html .= match ($run) {
'+' => "{$text} ",
'-' => "{$text} ",
default => "{$text} ",
};
$buffer = [];
};
foreach ($out as [$op, $word]) {
if ($op !== $run) {
$flush();
$run = $op;
}
$buffer[] = $word;
}
$flush();
return trim($html);
}
/**
* @return array