diff --git a/src/Renderer/Html/AbstractHtml.php b/src/Renderer/Html/AbstractHtml.php index 1ac4c2ce..f56e2b48 100644 --- a/src/Renderer/Html/AbstractHtml.php +++ b/src/Renderer/Html/AbstractHtml.php @@ -119,7 +119,9 @@ public function getChanges(Differ $differ): array */ protected function renderWorker(Differ $differ): string { - return $this->redererChanges($this->getChanges($differ)); + $rendered = $this->redererChanges($this->getChanges($differ)); + + return $this->cleanUpDummyHtmlClosures($rendered); } /** @@ -129,7 +131,9 @@ protected function renderArrayWorker(array $differArray): string { $this->ensureChangesUseIntTag($differArray); - return $this->redererChanges($differArray); + $rendered = $this->redererChanges($differArray); + + return $this->cleanUpDummyHtmlClosures($rendered); } /** @@ -317,4 +321,21 @@ protected function ensureChangesUseIntTag(array &$changes): void } } } + + /** + * Clean up empty HTML closures in the given string. + * + * @param string $string the string + */ + protected function cleanUpDummyHtmlClosures(string $string): string + { + return \str_replace( + [ + RendererConstant::HTML_CLOSURES_DEL[0] . RendererConstant::HTML_CLOSURES_DEL[1], + RendererConstant::HTML_CLOSURES_INS[0] . RendererConstant::HTML_CLOSURES_INS[1], + ], + '', + $string + ); + } } diff --git a/src/Renderer/Html/LineRenderer/Word.php b/src/Renderer/Html/LineRenderer/Word.php index f9f1964a..8ee57f9a 100644 --- a/src/Renderer/Html/LineRenderer/Word.php +++ b/src/Renderer/Html/LineRenderer/Word.php @@ -20,6 +20,7 @@ final class Word extends AbstractLineRenderer public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface { static $splitRegex = '/([' . RendererConstant::PUNCTUATIONS_RANGE . '])/uS'; + static $dummyHtmlClosure = RendererConstant::HTML_CLOSURES[0] . RendererConstant::HTML_CLOSURES[1]; // using PREG_SPLIT_NO_EMPTY will make "wordGlues" work wrongly under some rare cases // failure case: @@ -33,18 +34,37 @@ public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface $hunk = $this->getChangedExtentSegments($oldWords, $newWords); // reversely iterate hunk + $dummyDelIdxes = $dummyInsIdxes = []; foreach (ReverseIterator::fromArray($hunk) as [$op, $i1, $i2, $j1, $j2]) { if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) { $oldWords[$i1] = RendererConstant::HTML_CLOSURES[0] . $oldWords[$i1]; $oldWords[$i2 - 1] .= RendererConstant::HTML_CLOSURES[1]; + + if ($op === SequenceMatcher::OP_DEL) { + $dummyInsIdxes[] = $j1; + } } if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) { $newWords[$j1] = RendererConstant::HTML_CLOSURES[0] . $newWords[$j1]; $newWords[$j2 - 1] .= RendererConstant::HTML_CLOSURES[1]; + + if ($op === SequenceMatcher::OP_INS) { + $dummyDelIdxes[] = $i1; + } } } + // insert dummy HTML closure to make sure there are always + // the same amounts of HTML closures in $oldWords and $newWords + // thus, this should ensure that "wordGlues" works correctly + foreach (ReverseIterator::fromArray($dummyDelIdxes) as $idx) { + \array_splice($oldWords, $idx, 0, [$dummyHtmlClosure]); + } + foreach (ReverseIterator::fromArray($dummyInsIdxes) as $idx) { + \array_splice($newWords, $idx, 0, [$dummyHtmlClosure]); + } + if (!empty($hunk) && !empty($this->rendererOptions['wordGlues'])) { $regexGlues = \array_map( function (string $glue): string {