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 . + ''; + } +}