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 {