diff --git a/src/Renderer/Html/InteractiveSideBySide.php b/src/Renderer/Html/InteractiveSideBySide.php
new file mode 100644
index 0000000..a3fbbf1
--- /dev/null
+++ b/src/Renderer/Html/InteractiveSideBySide.php
@@ -0,0 +1,408 @@
+ 'Interactive side by side',
+ 'type' => 'Html',
+ ];
+
+ /**
+ * Builds an encoded value for the HTML attribute.
+ *
+ * @param string|null $value The value to build.
+ *
+ * @return string The built value.
+ */
+ protected function buildValueForAttribute(?string $value): string {
+ // Encapsulate the value into a structured array to support null value
+ // in the input value.
+ if ($value === null) {
+ $jsonEncapsulatedValue = json_encode(['value' => null]);
+ } else {
+ $jsonEncapsulatedValue = json_encode(['value' => $value]);
+ }
+
+ // Encode the value for HTML attribute.
+ return $this->encode($jsonEncapsulatedValue);
+ }
+
+ /**
+ * Encode string.
+ *
+ * @param string $string The string to encode.
+ * @param bool $forInputName (Optional) Whether the string is for input name. Defaults to false.
+ *
+ * @return string The encoded string.
+ */
+ protected function encode(string $string, bool $forInputName = false): string {
+ $encodeForInputName = function (string $input): string {
+ // PHP converts the dot in the input name to underscore in the $_POST array,
+ // so we need to convert the dot to something else.
+ return str_replace(['.'], [''], $input);
+ };
+
+ return htmlspecialchars(
+ $forInputName ? $encodeForInputName($string) : $string,
+ ENT_QUOTES,
+ 'UTF-8');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redererChanges(array $changes): string
+ {
+ if (empty($changes)) {
+ return $this->getResultForIdenticals();
+ }
+
+ $wrapperClasses = [
+ ...$this->options['wrapperClasses'],
+ 'diff', 'diff-html', 'diff-interactive-side-by-side',
+ ];
+
+ $this->currentDiffLineNumber = 0;
+
+ return
+ '' .
+ $this->renderTableHeader() .
+ $this->renderTableHunks($changes) .
+ '
' .
+ $this->renderMetadata();
+ }
+
+ /**
+ * Renders metadata not directly related to the diff.
+ *
+ * Dev note: This can be moved out of this class, as metadata
+ * are not directly related to the diff.
+ * I.e. move the implementation to the caller which can then
+ * specify the metadata to render (by injecting callbacks).
+ *
+ * @return string The rendered metadata.
+ */
+ protected function renderMetadata(): string {
+ $metadata = '';
+
+ if (!empty($this->options['uses_php_serialized_processor'])) {
+ // Add a hidden input to tell that the field is reserialized
+ // by the PHP serialized processor.
+ $inputName = ($this->options['diff_id'] ?? '');
+
+ if (!empty($inputName)) {
+ $metadata .= '';
+ }
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * Renders the radio input.
+ *
+ * @param string $inputName The input name.
+ * @param string|null $inputValue The input value. May be null.
+ * @param string|null $checkedValue (Optional) The checked value, if any.
+ *
+ * @return string The rendered radio input.
+ */
+ protected function renderRadioInput(string $inputName, ?string $inputValue, ?string $checkedValue = null): string
+ {
+ $attrChecked = $inputValue === $checkedValue ? ' checked="checked"' : '';
+
+ $attrName = ' name="merge[' . $this->encode($inputName, true) . ']"';
+
+ $attrValue = ' value="' . $this->buildValueForAttribute($inputValue) . '"';
+
+ return '';
+ }
+
+ /**
+ * Renderer the table header.
+ */
+ protected function renderTableHeader(): string
+ {
+ if (!$this->options['showHeader']) {
+ return '';
+ }
+
+ return
+ '' .
+ '' .
+ '| ' . $this->_('old_version') . ' | ' .
+ ($this->options['lineNumbers'] ? ' | ' : '') .
+ ' | ' .
+ ' | ' .
+ '' . $this->_('new_version') . ' | ' .
+ ($this->options['lineNumbers'] ? ' | ' : '') .
+ '
' .
+ '';
+ }
+
+ /**
+ * Renderer the table separate block.
+ */
+ protected function renderTableSeparateBlock(): string
+ {
+ $colspan = $this->options['lineNumbers'] ? '6' : '4';
+
+ return
+ '' .
+ '' .
+ ' | ' .
+ '
' .
+ '';
+ }
+
+ /**
+ * Renderer table hunks.
+ *
+ * @param array[][] $hunks each hunk has many blocks
+ */
+ protected function renderTableHunks(array $hunks): string
+ {
+ $ret = '';
+
+ foreach ($hunks as $i => $hunk) {
+ if ($i > 0 && $this->options['separateBlock']) {
+ $ret .= $this->renderTableSeparateBlock();
+ }
+
+ foreach ($hunk as $block) {
+ $ret .= $this->renderTableBlock($block);
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlock(array $block): string
+ {
+ switch ($block['tag']) {
+ case SequenceMatcher::OP_EQ:
+ $content = $this->renderTableBlockEqual($block);
+ break;
+ case SequenceMatcher::OP_INS:
+ $content = $this->renderTableBlockInsert($block);
+ break;
+ case SequenceMatcher::OP_DEL:
+ $content = $this->renderTableBlockDelete($block);
+ break;
+ case SequenceMatcher::OP_REP:
+ $content = $this->renderTableBlockReplace($block);
+ break;
+ default:
+ $content = '';
+ }
+
+ return '' . $content . '';
+ }
+
+ /**
+ * Renderer the table block: equal.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockEqual(array $block): string
+ {
+ $ret = '';
+
+ $rowCount = \count($block['new']['lines']);
+
+ for ($no = 0; $no < $rowCount; ++$no) {
+ $ret .= $this->renderTableRow(
+ $block['old']['lines'][$no],
+ $block['new']['lines'][$no],
+ $block['old']['offset'] + $no + 1,
+ $block['new']['offset'] + $no + 1,
+ $block['old']['raw_lines'][$no] ?? null,
+ $block['new']['raw_lines'][$no] ?? null,
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: insert.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockInsert(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['new']['lines'] as $no => $newLine) {
+ $ret .= $this->renderTableRow(
+ null,
+ $newLine,
+ null,
+ $block['new']['offset'] + $no + 1,
+ null,
+ $block['new']['raw_lines'][$no] ?? null,
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: delete.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockDelete(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['old']['lines'] as $no => $oldLine) {
+ $ret .= $this->renderTableRow(
+ $oldLine,
+ null,
+ $block['old']['offset'] + $no + 1,
+ null,
+ $block['old']['raw_lines'][$no] ?? null,
+ null
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: replace.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockReplace(array $block): string
+ {
+ $ret = '';
+
+ $lineCountMax = max(\count($block['old']['lines']), \count($block['new']['lines']));
+
+ for ($no = 0; $no < $lineCountMax; ++$no) {
+ if (isset($block['old']['lines'][$no])) {
+ $oldLineNum = $block['old']['offset'] + $no + 1;
+ $oldLine = $block['old']['lines'][$no];
+ $oldLineRaw = $block['old']['raw_lines'][$no] ?? null;
+ } else {
+ $oldLineNum = $oldLine = $oldLineRaw = null;
+ }
+
+ if (isset($block['new']['lines'][$no])) {
+ $newLineNum = $block['new']['offset'] + $no + 1;
+ $newLine = $block['new']['lines'][$no];
+ $newLineRaw = $block['new']['raw_lines'][$no] ?? null;
+ } else {
+ $newLineNum = $newLine = $newLineRaw = null;
+ }
+
+ $ret .= $this->renderTableRow($oldLine, $newLine, $oldLineNum, $newLineNum, $oldLineRaw, $newLineRaw);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer a content row of the output table.
+ *
+ * @param null|string $oldLine the old line
+ * @param null|string $newLine the new line
+ * @param null|int $oldLineNum the old line number
+ * @param null|int $newLineNum the new line number
+ * @param null|string $oldLineRaw the old line raw
+ * @param null|string $newLineRaw the new line raw
+ */
+ protected function renderTableRow(
+ ?string $oldLine,
+ ?string $newLine,
+ ?int $oldLineNum,
+ ?int $newLineNum,
+ ?string $oldLineRaw,
+ ?string $newLineRaw
+ ): string {
+ $diffLineId = ($this->options['diff_id'] ?? '') . ':diff-line_' . ++$this->currentDiffLineNumber;
+
+ if ( $oldLine === $newLine ) {
+ // No change means we can include the value in a hidden input.
+ $hiddenInput = '';
+ $middleColumns = $this->renderLineContentColumn('unchanged', $hiddenInput) . $this->renderLineContentColumn('empty', '');
+ } else {
+ // Provide the radio buttons for the user to select the value.
+ $middleColumns =
+ $this->renderLineContentColumn('radio-old', $this->renderRadioInput($diffLineId, $oldLineRaw)) .
+ $this->renderLineContentColumn('radio-new', $this->renderRadioInput($diffLineId, $newLineRaw, $newLineRaw));
+ }
+
+ return
+ '' .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumn('old', $oldLineNum)
+ : ''
+ ) .
+ $this->renderLineContentColumn('old', $oldLine) .
+ $middleColumns .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumn('new', $newLineNum)
+ : ''
+ ) .
+ $this->renderLineContentColumn('new', $newLine) .
+ '
';
+ }
+
+ /**
+ * Renderer the line number column.
+ *
+ * @param string $type the diff type
+ * @param null|int $lineNum the line number
+ */
+ protected function renderLineNumberColumn(string $type, ?int $lineNum): string
+ {
+ return isset($lineNum)
+ ? '' . $lineNum . ' | '
+ : ' | ';
+ }
+
+ /**
+ * Renderer the line content column.
+ *
+ * @param string $type the diff type
+ * @param null|string $content the line content
+ */
+ protected function renderLineContentColumn(string $type, ?string $content): string
+ {
+ return
+ '' .
+ $content .
+ ' | ';
+ }
+}
diff --git a/src/Renderer/Html/JsonHtmlWithRaw.php b/src/Renderer/Html/JsonHtmlWithRaw.php
new file mode 100644
index 0000000..5c17f64
--- /dev/null
+++ b/src/Renderer/Html/JsonHtmlWithRaw.php
@@ -0,0 +1,64 @@
+options['detailLevel'],
+ $differ->getOptions(),
+ $this->options,
+ );
+
+ $old = $differ->getOld();
+ $new = $differ->getNew();
+ $oldRaw = $differ->getOld();
+ $newRaw = $differ->getNew();
+
+ $changes = [];
+
+ foreach ($differ->getGroupedOpcodes() as $hunk) {
+ $change = [];
+
+ foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
+ $change[] = $this->getDefaultBlock($op, $i1, $j1);
+ $block = &$change[\count($change) - 1];
+
+ // if there are same amount of lines replaced
+ // we can render the inner detailed changes with corresponding lines
+ // @todo or use LineRenderer to do the job regardless different line counts?
+ if ($op === SequenceMatcher::OP_REP && $i2 - $i1 === $j2 - $j1) {
+ for ($k = $i2 - $i1 - 1; $k >= 0; --$k) {
+ $this->renderChangedExtent($lineRenderer, $old[$i1 + $k], $new[$j1 + $k]);
+ }
+ }
+
+ // The next two lines are the only addition to the parent class.
+ $block['old']['raw_lines'] = \array_slice($oldRaw, $i1, $i2 - $i1);
+ $block['new']['raw_lines'] = \array_slice($newRaw, $j1, $j2 - $j1);
+ $block['old']['lines'] = \array_slice($old, $i1, $i2 - $i1);
+ $block['new']['lines'] = \array_slice($new, $j1, $j2 - $j1);
+ }
+ unset($block);
+
+ $changes[] = $change;
+ }
+
+ if (static::AUTO_FORMAT_CHANGES) {
+ $this->formatChanges($changes);
+ }
+
+ return $changes;
+ }
+}
diff --git a/src/Renderer/Html/DscSideBySide.php b/src/Renderer/Html/DscSideBySide.php
new file mode 100644
index 0000000..3d892dc
--- /dev/null
+++ b/src/Renderer/Html/DscSideBySide.php
@@ -0,0 +1,286 @@
+ 'Dsc side by side',
+ 'type' => 'Html',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redererChanges(array $changes): string
+ {
+ if (empty($changes)) {
+ return $this->getResultForIdenticals();
+ }
+
+ $wrapperClasses = [
+ ...$this->options['wrapperClasses'],
+ 'diff', 'diff-html', 'diff-side-by-side',
+ ];
+
+ return
+ '' .
+ $this->renderTableHeader() .
+ $this->renderTableHunks($changes) .
+ '
';
+ }
+
+ /**
+ * Renderer the table header.
+ */
+ protected function renderTableHeader(): string
+ {
+ if (!$this->options['showHeader']) {
+ return '';
+ }
+
+ return
+ '' .
+ '' .
+ '| ' . $this->_('old_version') . ' | ' .
+ ($this->options['lineNumbers'] ? ' | ' : '') .
+ '' . $this->_('new_version') . ' | ' .
+ ($this->options['lineNumbers'] ? ' | ' : '') .
+ '
' .
+ '';
+ }
+
+ /**
+ * Renderer the table separate block.
+ */
+ protected function renderTableSeparateBlock(): string
+ {
+ $colspan = $this->options['lineNumbers'] ? '4' : '2';
+
+ return
+ '' .
+ '' .
+ ' | ' .
+ '
' .
+ '';
+ }
+
+ /**
+ * Renderer table hunks.
+ *
+ * @param array[][] $hunks each hunk has many blocks
+ */
+ protected function renderTableHunks(array $hunks): string
+ {
+ $ret = '';
+
+ foreach ($hunks as $i => $hunk) {
+ if ($i > 0 && $this->options['separateBlock']) {
+ $ret .= $this->renderTableSeparateBlock();
+ }
+
+ foreach ($hunk as $block) {
+ $ret .= $this->renderTableBlock($block);
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlock(array $block): string
+ {
+ switch ($block['tag']) {
+ case SequenceMatcher::OP_EQ:
+ $content = $this->renderTableBlockEqual($block);
+ break;
+ case SequenceMatcher::OP_INS:
+ $content = $this->renderTableBlockInsert($block);
+ break;
+ case SequenceMatcher::OP_DEL:
+ $content = $this->renderTableBlockDelete($block);
+ break;
+ case SequenceMatcher::OP_REP:
+ $content = $this->renderTableBlockReplace($block);
+ break;
+ default:
+ $content = '';
+ }
+
+ return '' . $content . '';
+ }
+
+ /**
+ * Renderer the table block: equal.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockEqual(array $block): string
+ {
+ $ret = '';
+
+ $rowCount = \count($block['new']['lines']);
+
+ for ($no = 0; $no < $rowCount; ++$no) {
+ $ret .= $this->renderTableRow(
+ $block['old']['lines'][$no],
+ $block['new']['lines'][$no],
+ $block['old']['offset'] + $no + 1,
+ $block['new']['offset'] + $no + 1,
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: insert.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockInsert(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['new']['lines'] as $no => $newLine) {
+ $ret .= $this->renderTableRow(
+ null,
+ $newLine,
+ null,
+ $block['new']['offset'] + $no + 1,
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: delete.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockDelete(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['old']['lines'] as $no => $oldLine) {
+ $ret .= $this->renderTableRow(
+ $oldLine,
+ null,
+ $block['old']['offset'] + $no + 1,
+ null,
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: replace.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockReplace(array $block): string
+ {
+ $ret = '';
+
+ $lineCountMax = max(\count($block['old']['lines']), \count($block['new']['lines']));
+
+ for ($no = 0; $no < $lineCountMax; ++$no) {
+ if (isset($block['old']['lines'][$no])) {
+ $oldLineNum = $block['old']['offset'] + $no + 1;
+ $oldLine = $block['old']['lines'][$no];
+ } else {
+ $oldLineNum = $oldLine = null;
+ }
+
+ if (isset($block['new']['lines'][$no])) {
+ $newLineNum = $block['new']['offset'] + $no + 1;
+ $newLine = $block['new']['lines'][$no];
+ } else {
+ $newLineNum = $newLine = null;
+ }
+
+ $ret .= $this->renderTableRow($oldLine, $newLine, $oldLineNum, $newLineNum);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer a content row of the output table.
+ *
+ * @param null|string $oldLine the old line
+ * @param null|string $newLine the new line
+ * @param null|int $oldLineNum the old line number
+ * @param null|int $newLineNum the new line number
+ */
+ protected function renderTableRow(
+ ?string $oldLine,
+ ?string $newLine,
+ ?int $oldLineNum,
+ ?int $newLineNum
+ ): string {
+ return
+ '' .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumn('old', $oldLineNum)
+ : ''
+ ) .
+ $this->renderLineContentColumn('old', $oldLine) .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumn('new', $newLineNum)
+ : ''
+ ) .
+ $this->renderLineContentColumn('new', $newLine) .
+ '
';
+ }
+
+ /**
+ * Renderer the line number column.
+ *
+ * @param string $type the diff type
+ * @param null|int $lineNum the line number
+ */
+ protected function renderLineNumberColumn(string $type, ?int $lineNum): string
+ {
+ return isset($lineNum)
+ ? '' . $lineNum . ' | '
+ : ' | ';
+ }
+
+ /**
+ * Renderer the line content column.
+ *
+ * @param string $type the diff type
+ * @param null|string $content the line content
+ */
+ protected function renderLineContentColumn(string $type, ?string $content): string
+ {
+ return
+ '' .
+ $content .
+ ' | ';
+ }
+}