From f326877863bb97f37c237ae9098b228238aafdcd Mon Sep 17 00:00:00 2001 From: JBlond Date: Sat, 19 Jan 2019 17:04:53 +0100 Subject: [PATCH] tabs vs space --- .editorconfig | 8 + lib/Autoloader.php | 38 +- lib/jblond/Diff.php | 262 ++-- lib/jblond/Diff/Renderer/Html/HtmlArray.php | 490 +++--- lib/jblond/Diff/Renderer/Html/Inline.php | 328 ++-- lib/jblond/Diff/Renderer/Html/SideBySide.php | 362 ++--- lib/jblond/Diff/Renderer/RendererAbstract.php | 64 +- lib/jblond/Diff/Renderer/Text/Context.php | 146 +- lib/jblond/Diff/Renderer/Text/Unified.php | 72 +- lib/jblond/Diff/SequenceMatcher.php | 1346 ++++++++--------- phpcs.xml | 5 +- tests/Diff/Renderer/ArrayTest.php | 144 +- 12 files changed, 1635 insertions(+), 1630 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a30621a4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = space diff --git a/lib/Autoloader.php b/lib/Autoloader.php index 284f5048..1cc14011 100644 --- a/lib/Autoloader.php +++ b/lib/Autoloader.php @@ -8,24 +8,24 @@ class Autoloader { - /** - * Autoloader constructor. - */ - public function __construct() - { - spl_autoload_register(array($this, '__autoload')); - } + /** + * Autoloader constructor. + */ + public function __construct() + { + spl_autoload_register(array($this, '__autoload')); + } - /** - * @param string $class - */ - private function __autoload($class) - { - $class = str_replace('\\', '/', $class); // revert path for old PHP on Linux - $dir = str_replace('\\', '/', __DIR__); - if (file_exists($dir . '/' . $class . '.php')) { - /** @noinspection PhpIncludeInspection */ - require_once $dir . '/' . $class . '.php'; - } - } + /** + * @param string $class + */ + private function __autoload($class) + { + $class = str_replace('\\', '/', $class); // revert path for old PHP on Linux + $dir = str_replace('\\', '/', __DIR__); + if (file_exists($dir . '/' . $class . '.php')) { + /** @noinspection PhpIncludeInspection */ + require_once $dir . '/' . $class . '.php'; + } + } } diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index c2d43679..e9a8747e 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -49,135 +49,135 @@ */ class Diff { - /** - * @var array The "old" sequence to use as the basis for the comparison. - */ - private $a = null; - - /** - * @var array The "new" sequence to generate the changes for. - */ - private $b = null; - - /** - * @var array Array containing the generated op codes for the differences between the two items. - */ - private $groupedCodes = null; - - /** - * @var array Associative array of the default options available for the diff class and their default value. - */ - private $defaultOptions = array( - 'context' => 3, - 'ignoreNewLines' => false, - 'ignoreWhitespace' => false, - 'ignoreCase' => false, - 'labelDifferences'=>'Differences' - ); - - /** - * @var array Array of the options that have been applied for generating the diff. - */ - public $options = array(); - - /** - * The constructor. - * - * @param array $a Array containing the lines of the first string to compare. - * @param array $b Array containing the lines for the second string to compare. - * @param array $options Array for the options - */ - public function __construct($a, $b, $options = array()) - { - $this->a = $a; - $this->b = $b; - - if (is_array($options)) { - $this->options = array_merge($this->defaultOptions, $options); - } else { - $this->options = $this->defaultOptions; - } - } - - - /** - * Render a diff using the supplied rendering class and return it. - * - * @param object $renderer object $renderer An instance of the rendering object to use for generating the diff. - * @return mixed The generated diff. Exact return value depends on the rendered. - */ - public function render($renderer) - { - $renderer->diff = $this; - return $renderer->render(); - } - - /** - * Get a range of lines from $start to $end from the first comparison string - * and return them as an array. If no values are supplied, the entire string - * is returned. It's also possible to specify just one line to return only - * that line. - * - * @param int $start The starting number. - * @param int $end The ending number. If not supplied, only the item in $start will be returned. - * @return array Array of all of the lines between the specified range. - */ - public function getA($start = 0, $end = null) : array - { - if ($start == 0 && $end === null) { - return $this->a; - } - - if ($end === null) { - $length = 1; - } else { - $length = $end - $start; - } - - return array_slice($this->a, $start, $length); - } - - /** - * Get a range of lines from $start to $end from the second comparison string - * and return them as an array. If no values are supplied, the entire string - * is returned. It's also possible to specify just one line to return only - * that line. - * - * @param int $start The starting number. - * @param int $end The ending number. If not supplied, only the item in $start will be returned. - * @return array Array of all of the lines between the specified range. - */ - public function getB($start = 0, $end = null) : array - { - if ($start == 0 && $end === null) { - return $this->b; - } - - if ($end === null) { - $length = 1; - } else { - $length = $end - $start; - } - - return array_slice($this->b, $start, $length); - } - - /** - * Generate a list of the compiled and grouped op codes for the differences between the - * two strings. Generally called by the renderer, this class instantiates the sequence - * matcher and performs the actual diff generation and return an array of the op codes - * for it. Once generated, the results are cached in the diff class instance. - * - * @return array Array of the grouped op codes for the generated diff. - */ - public function getGroupedOpcodes() : array - { - if (!is_null($this->groupedCodes)) { - return $this->groupedCodes; - } - - $sequenceMatcher = new SequenceMatcher($this->a, $this->b, $this->options, null); - $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']); - return $this->groupedCodes; - } + /** + * @var array The "old" sequence to use as the basis for the comparison. + */ + private $a = null; + + /** + * @var array The "new" sequence to generate the changes for. + */ + private $b = null; + + /** + * @var array Array containing the generated op codes for the differences between the two items. + */ + private $groupedCodes = null; + + /** + * @var array Associative array of the default options available for the diff class and their default value. + */ + private $defaultOptions = array( + 'context' => 3, + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false, + 'labelDifferences'=>'Differences' + ); + + /** + * @var array Array of the options that have been applied for generating the diff. + */ + public $options = array(); + + /** + * The constructor. + * + * @param array $a Array containing the lines of the first string to compare. + * @param array $b Array containing the lines for the second string to compare. + * @param array $options Array for the options + */ + public function __construct($a, $b, $options = array()) + { + $this->a = $a; + $this->b = $b; + + if (is_array($options)) { + $this->options = array_merge($this->defaultOptions, $options); + } else { + $this->options = $this->defaultOptions; + } + } + + + /** + * Render a diff using the supplied rendering class and return it. + * + * @param object $renderer object $renderer An instance of the rendering object to use for generating the diff. + * @return mixed The generated diff. Exact return value depends on the rendered. + */ + public function render($renderer) + { + $renderer->diff = $this; + return $renderer->render(); + } + + /** + * Get a range of lines from $start to $end from the first comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getA($start = 0, $end = null) : array + { + if ($start == 0 && $end === null) { + return $this->a; + } + + if ($end === null) { + $length = 1; + } else { + $length = $end - $start; + } + + return array_slice($this->a, $start, $length); + } + + /** + * Get a range of lines from $start to $end from the second comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getB($start = 0, $end = null) : array + { + if ($start == 0 && $end === null) { + return $this->b; + } + + if ($end === null) { + $length = 1; + } else { + $length = $end - $start; + } + + return array_slice($this->b, $start, $length); + } + + /** + * Generate a list of the compiled and grouped op codes for the differences between the + * two strings. Generally called by the renderer, this class instantiates the sequence + * matcher and performs the actual diff generation and return an array of the op codes + * for it. Once generated, the results are cached in the diff class instance. + * + * @return array Array of the grouped op codes for the generated diff. + */ + public function getGroupedOpcodes() : array + { + if (!is_null($this->groupedCodes)) { + return $this->groupedCodes; + } + + $sequenceMatcher = new SequenceMatcher($this->a, $this->b, $this->options, null); + $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']); + return $this->groupedCodes; + } } diff --git a/lib/jblond/Diff/Renderer/Html/HtmlArray.php b/lib/jblond/Diff/Renderer/Html/HtmlArray.php index b788c24e..cce0d95f 100644 --- a/lib/jblond/Diff/Renderer/Html/HtmlArray.php +++ b/lib/jblond/Diff/Renderer/Html/HtmlArray.php @@ -50,272 +50,272 @@ */ class HtmlArray extends RendererAbstract { - /** - * @var array Array of the default options that apply to this renderer. - */ - protected $defaultOptions = array( - 'tabSize' => 4, + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array( + 'tabSize' => 4, 'title_a' => 'Old Version', 'title_b' => 'New Version', - ); + ); - /** - * From https://gist.github.com/stemar/8287074 - * @param mixed $string The input string. - * @param mixed $replacement The replacement string. - * @param mixed $start If start is positive, the replacing will begin at the start'th offset into string. - * If start is negative, the replacing will begin at the start'th character from the end of string. - * @param mixed $length If given and is positive, it represents the length of the portion of string which is to - * be replaced. If it is negative, it represents the number of characters from the end of string at which to - * stop replacing. If it is not given, then it will default to strlen( string ); i.e. end the replacing at the - * end of string. Of course, if length is zero then this function will have the effect of inserting replacement - * into string at the given start offset. - * @return string|array The result string is returned. If string is an array then array is returned. - */ - public function mbSubstrReplace($string, $replacement, $start, $length = null) - { - if (is_array($string)) { - $num = count($string); - // $replacement - if (is_array($replacement)) { - $replacement = array_slice($replacement, 0, $num); - } else { - $replacement = array_pad(array($replacement), $num, $replacement); - } + /** + * From https://gist.github.com/stemar/8287074 + * @param mixed $string The input string. + * @param mixed $replacement The replacement string. + * @param mixed $start If start is positive, the replacing will begin at the start'th offset into string. + * If start is negative, the replacing will begin at the start'th character from the end of string. + * @param mixed $length If given and is positive, it represents the length of the portion of string which is to + * be replaced. If it is negative, it represents the number of characters from the end of string at which to + * stop replacing. If it is not given, then it will default to strlen( string ); i.e. end the replacing at the + * end of string. Of course, if length is zero then this function will have the effect of inserting replacement + * into string at the given start offset. + * @return string|array The result string is returned. If string is an array then array is returned. + */ + public function mbSubstrReplace($string, $replacement, $start, $length = null) + { + if (is_array($string)) { + $num = count($string); + // $replacement + if (is_array($replacement)) { + $replacement = array_slice($replacement, 0, $num); + } else { + $replacement = array_pad(array($replacement), $num, $replacement); + } - // $start - if (is_array($start)) { - $start = array_slice($start, 0, $num); - foreach ($start as $key => $value) { - $start[$key] = is_int($value) ? $value : 0; - } - } else { - $start = array_pad(array($start), $num, $start); - } - // $length - if (!isset($length)) { - $length = array_fill(0, $num, 0); - } elseif (is_array($length)) { - $length = array_slice($length, 0, $num); - foreach ($length as $key => $value) { - $length[$key] = isset($value) ? (is_int($value) ? $value : $num) : 0; - } - } else { - $length = array_pad(array($length), $num, $length); - } - // Recursive call - return array_map(array($this, 'mbSubstrReplace'), $string, $replacement, $start, $length); - } - preg_match_all('/./us', (string)$string, $smatches); - preg_match_all('/./us', (string)$replacement, $rmatches); - if ($length === null) { - $length = mb_strlen($string); - } - array_splice($smatches['0'], $start, $length, $rmatches[0]); - return join($smatches['0']); - } + // $start + if (is_array($start)) { + $start = array_slice($start, 0, $num); + foreach ($start as $key => $value) { + $start[$key] = is_int($value) ? $value : 0; + } + } else { + $start = array_pad(array($start), $num, $start); + } + // $length + if (!isset($length)) { + $length = array_fill(0, $num, 0); + } elseif (is_array($length)) { + $length = array_slice($length, 0, $num); + foreach ($length as $key => $value) { + $length[$key] = isset($value) ? (is_int($value) ? $value : $num) : 0; + } + } else { + $length = array_pad(array($length), $num, $length); + } + // Recursive call + return array_map(array($this, 'mbSubstrReplace'), $string, $replacement, $start, $length); + } + preg_match_all('/./us', (string)$string, $smatches); + preg_match_all('/./us', (string)$replacement, $rmatches); + if ($length === null) { + $length = mb_strlen($string); + } + array_splice($smatches['0'], $start, $length, $rmatches[0]); + return join($smatches['0']); + } - /** - * Render and return an array structure suitable for generating HTML - * based differences. Generally called by subclasses that generate a - * HTML based diff and return an array of the changes to show in the diff. - * - * @return array|string An array of the generated changes, suitable for presentation in HTML. - */ - public function render() - { - // As we'll be modifying a & b to include our change markers, - // we need to get the contents and store them here. That way - // we're not going to destroy the original data - $a = $this->diff->getA(); - $b = $this->diff->getB(); + /** + * Render and return an array structure suitable for generating HTML + * based differences. Generally called by subclasses that generate a + * HTML based diff and return an array of the changes to show in the diff. + * + * @return array|string An array of the generated changes, suitable for presentation in HTML. + */ + public function render() + { + // As we'll be modifying a & b to include our change markers, + // we need to get the contents and store them here. That way + // we're not going to destroy the original data + $a = $this->diff->getA(); + $b = $this->diff->getB(); - $changes = array(); - $opCodes = $this->diff->getGroupedOpcodes(); - foreach ($opCodes as $group) { - $blocks = array(); - $lastTag = null; - $lastBlock = 0; - foreach ($group as $code) { - list($tag, $i1, $i2, $j1, $j2) = $code; + $changes = array(); + $opCodes = $this->diff->getGroupedOpcodes(); + foreach ($opCodes as $group) { + $blocks = array(); + $lastTag = null; + $lastBlock = 0; + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; - if ($tag == 'replace' && $i2 - $i1 == $j2 - $j1) { - for ($i = 0; $i < ($i2 - $i1); ++$i) { - $fromLine = $a[$i1 + $i]; - $toLine = $b[$j1 + $i]; + if ($tag == 'replace' && $i2 - $i1 == $j2 - $j1) { + for ($i = 0; $i < ($i2 - $i1); ++$i) { + $fromLine = $a[$i1 + $i]; + $toLine = $b[$j1 + $i]; - list($start, $end) = $this->getChangeExtent($fromLine, $toLine); - if ($start != 0 || $end != 0) { - $realEnd = mb_strlen($fromLine) + $end; + list($start, $end) = $this->getChangeExtent($fromLine, $toLine); + if ($start != 0 || $end != 0) { + $realEnd = mb_strlen($fromLine) + $end; - $fromLine = mb_substr($fromLine, 0, $start) . "\0" . - mb_substr($fromLine, $start, $realEnd - $start) . "\1" . mb_substr($fromLine, $realEnd); + $fromLine = mb_substr($fromLine, 0, $start) . "\0" . + mb_substr($fromLine, $start, $realEnd - $start) . "\1" . mb_substr($fromLine, $realEnd); - $realEnd = mb_strlen($toLine) + $end; + $realEnd = mb_strlen($toLine) + $end; - $toLine = mb_substr($toLine, 0, $start) . - "\0" . mb_substr($toLine, $start, $realEnd - $start) . "\1" . - mb_substr($toLine, $realEnd); + $toLine = mb_substr($toLine, 0, $start) . + "\0" . mb_substr($toLine, $start, $realEnd - $start) . "\1" . + mb_substr($toLine, $realEnd); - $a[$i1 + $i] = $fromLine; - $b[$j1 + $i] = $toLine; - } - } - } + $a[$i1 + $i] = $fromLine; + $b[$j1 + $i] = $toLine; + } + } + } - if ($tag != $lastTag) { - $blocks[] = $this->getDefaultArray($tag, $i1, $j1); - $lastBlock = count($blocks)-1; - } + if ($tag != $lastTag) { + $blocks[] = $this->getDefaultArray($tag, $i1, $j1); + $lastBlock = count($blocks)-1; + } - $lastTag = $tag; + $lastTag = $tag; - if ($tag == 'equal') { - $lines = array_slice($a, $i1, ($i2 - $i1)); - $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); - $lines = array_slice($b, $j1, ($j2 - $j1)); - $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); - } else { - if ($tag == 'replace' || $tag == 'delete') { - $lines = array_slice($a, $i1, ($i2 - $i1)); - $lines = $this->formatLines($lines); - $lines = str_replace(array("\0", "\1"), array('', ''), $lines); - $blocks[$lastBlock]['base']['lines'] += $lines; - } + if ($tag == 'equal') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); + $lines = array_slice($b, $j1, ($j2 - $j1)); + $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); + } else { + if ($tag == 'replace' || $tag == 'delete') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('', ''), $lines); + $blocks[$lastBlock]['base']['lines'] += $lines; + } - if ($tag == 'replace' || $tag == 'insert') { - $lines = array_slice($b, $j1, ($j2 - $j1)); - $lines = $this->formatLines($lines); - $lines = str_replace(array("\0", "\1"), array('', ''), $lines); - $blocks[$lastBlock]['changed']['lines'] += $lines; - } - } - } - $changes[] = $blocks; - } - return $changes; - } + if ($tag == 'replace' || $tag == 'insert') { + $lines = array_slice($b, $j1, ($j2 - $j1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('', ''), $lines); + $blocks[$lastBlock]['changed']['lines'] += $lines; + } + } + } + $changes[] = $blocks; + } + return $changes; + } - /** - * Given two strings, determine where the changes in the two strings - * begin, and where the changes in the two strings end. - * - * @param string $fromLine The first string. - * @param string $toLine The second string. - * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) - */ - private function getChangeExtent($fromLine, $toLine) - { - $start = 0; - $limit = min(mb_strlen($fromLine), mb_strlen($toLine)); - while ($start < $limit && mb_substr($fromLine, $start, 1) == mb_substr($toLine, $start, 1)) { - ++$start; - } - $end = -1; - $limit = $limit - $start; - while (-$end <= $limit && mb_substr($fromLine, $end, 1) == mb_substr($toLine, $end, 1)) { - --$end; - } - return array( - $start, - $end + 1 - ); - } + /** + * Given two strings, determine where the changes in the two strings + * begin, and where the changes in the two strings end. + * + * @param string $fromLine The first string. + * @param string $toLine The second string. + * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) + */ + private function getChangeExtent($fromLine, $toLine) + { + $start = 0; + $limit = min(mb_strlen($fromLine), mb_strlen($toLine)); + while ($start < $limit && mb_substr($fromLine, $start, 1) == mb_substr($toLine, $start, 1)) { + ++$start; + } + $end = -1; + $limit = $limit - $start; + while (-$end <= $limit && mb_substr($fromLine, $end, 1) == mb_substr($toLine, $end, 1)) { + --$end; + } + return array( + $start, + $end + 1 + ); + } - /** - * Format a series of lines suitable for output in a HTML rendered diff. - * This involves replacing tab characters with spaces, making the HTML safe - * for output, ensuring that double spaces are replaced with   etc. - * - * @param array $lines Array of lines to format. - * @return array Array of the formatted lines. - */ - protected function formatLines($lines) - { - if ($this->options['tabSize'] !== false) { - $lines = array_map(array($this, 'ExpandTabs'), $lines); - } - $lines = array_map(array($this, 'HtmlSafe'), $lines); - foreach ($lines as &$line) { - $line = preg_replace_callback('# ( +)|^ #', array($this, 'fixSpaces'), $line); - } - return $lines; - } + /** + * Format a series of lines suitable for output in a HTML rendered diff. + * This involves replacing tab characters with spaces, making the HTML safe + * for output, ensuring that double spaces are replaced with   etc. + * + * @param array $lines Array of lines to format. + * @return array Array of the formatted lines. + */ + protected function formatLines($lines) + { + if ($this->options['tabSize'] !== false) { + $lines = array_map(array($this, 'ExpandTabs'), $lines); + } + $lines = array_map(array($this, 'HtmlSafe'), $lines); + foreach ($lines as &$line) { + $line = preg_replace_callback('# ( +)|^ #', array($this, 'fixSpaces'), $line); + } + return $lines; + } - /** - * Replace a string containing spaces with a HTML representation using  . - * - * @param array $matches The string of spaces. - * @return string The HTML representation of the string. - */ - protected function fixSpaces($matches) - { - $buffer = ''; - $count = 0; - foreach ($matches as $spaces) { - $count = strlen($spaces); - if ($count == 0) { - continue; - } - $div = (int) ($count / 2); - $mod = $count % 2; - $buffer .= str_repeat('  ', $div).str_repeat(' ', $mod); - } + /** + * Replace a string containing spaces with a HTML representation using  . + * + * @param array $matches The string of spaces. + * @return string The HTML representation of the string. + */ + protected function fixSpaces($matches) + { + $buffer = ''; + $count = 0; + foreach ($matches as $spaces) { + $count = strlen($spaces); + if ($count == 0) { + continue; + } + $div = (int) ($count / 2); + $mod = $count % 2; + $buffer .= str_repeat('  ', $div).str_repeat(' ', $mod); + } - $div = (int) ($count / 2); - $mod = $count % 2; - return str_repeat('  ', $div).str_repeat(' ', $mod); - } + $div = (int) ($count / 2); + $mod = $count % 2; + return str_repeat('  ', $div).str_repeat(' ', $mod); + } - /** - * Replace tabs in a single line with a number of spaces as defined by the tabSize option. - * - * @param string $line The containing tabs to convert. - * @return string The line with the tabs converted to spaces. - */ - private function expandTabs($line) - { - $tabSize = $this->options['tabSize']; - while (($pos = strpos($line, "\t")) !== false) { - $left = substr($line, 0, $pos); - $right = substr($line, $pos + 1); - $length = $tabSize - ($pos % $tabSize); - $spaces = str_repeat(' ', $length); - $line = $left . $spaces . $right; - } - return $line; - } + /** + * Replace tabs in a single line with a number of spaces as defined by the tabSize option. + * + * @param string $line The containing tabs to convert. + * @return string The line with the tabs converted to spaces. + */ + private function expandTabs($line) + { + $tabSize = $this->options['tabSize']; + while (($pos = strpos($line, "\t")) !== false) { + $left = substr($line, 0, $pos); + $right = substr($line, $pos + 1); + $length = $tabSize - ($pos % $tabSize); + $spaces = str_repeat(' ', $length); + $line = $left . $spaces . $right; + } + return $line; + } - /** - * Make a string containing HTML safe for output on a page. - * - * @param string $string The string. - * @return string The string with the HTML characters replaced by entities. - */ - private function htmlSafe($string) - { - return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); - } + /** + * Make a string containing HTML safe for output on a page. + * + * @param string $string The string. + * @return string The string with the HTML characters replaced by entities. + */ + private function htmlSafe($string) + { + return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); + } - /** - * @param string $tag - * @param integer $i1 - * @param integer $j1 - * @return array - */ - private function getDefaultArray($tag, $i1, $j1) - { - return array - ( - 'tag' => $tag, - 'base' => array( - 'offset' => $i1, - 'lines' => array() - ), - 'changed' => array( - 'offset' => $j1, - 'lines' => array() - ) - ); - } + /** + * @param string $tag + * @param integer $i1 + * @param integer $j1 + * @return array + */ + private function getDefaultArray($tag, $i1, $j1) + { + return array + ( + 'tag' => $tag, + 'base' => array( + 'offset' => $i1, + 'lines' => array() + ), + 'changed' => array( + 'offset' => $j1, + 'lines' => array() + ) + ); + } } diff --git a/lib/jblond/Diff/Renderer/Html/Inline.php b/lib/jblond/Diff/Renderer/Html/Inline.php index c3f7835c..a46f25d4 100644 --- a/lib/jblond/Diff/Renderer/Html/Inline.php +++ b/lib/jblond/Diff/Renderer/Html/Inline.php @@ -48,181 +48,181 @@ */ class Inline extends HtmlArray { - /** - * Render a and return diff with changes between the two sequences - * displayed inline (under each other) - * - * @return string The generated inline diff. - */ - public function render() : string - { - $changes = parent::render(); - $html = ''; - if (empty($changes)) { - return $html; - } + /** + * Render a and return diff with changes between the two sequences + * displayed inline (under each other) + * + * @return string The generated inline diff. + */ + public function render() : string + { + $changes = parent::render(); + $html = ''; + if (empty($changes)) { + return $html; + } - $html .= $this->generateTableHeader(); + $html .= $this->generateTableHeader(); - foreach ($changes as $i => $blocks) { - // If this is a separate block, we're condensing code so output ..., - // indicating a significant portion of the code has been collapsed as - // it is the same - if ($i > 0) { - $html .= $this->generateSkippedTable(); - } + foreach ($changes as $i => $blocks) { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as + // it is the same + if ($i > 0) { + $html .= $this->generateSkippedTable(); + } - foreach ($blocks as $change) { - $html .= ''; - switch ($change['tag']) { - // Equal changes should be shown on both sides of the diff - case 'equal': - $html .= $this->generateTableRowsEqual($change); - break; - // Added lines only on the right side - case 'insert': - $html .= $this->generateTableRowsInsert($change); - break; - // Show deleted lines only on the left side - case 'delete': - $html .= $this->generateTableRowsDelete($change); - break; - // Show modified lines on both sides - case 'replace': - $html .= $this->generateTableRowsReplace($change); - break; - } - $html .= ''; - } - } - $html .= ''; - return $html; - } + foreach ($blocks as $change) { + $html .= ''; + switch ($change['tag']) { + // Equal changes should be shown on both sides of the diff + case 'equal': + $html .= $this->generateTableRowsEqual($change); + break; + // Added lines only on the right side + case 'insert': + $html .= $this->generateTableRowsInsert($change); + break; + // Show deleted lines only on the left side + case 'delete': + $html .= $this->generateTableRowsDelete($change); + break; + // Show modified lines on both sides + case 'replace': + $html .= $this->generateTableRowsReplace($change); + break; + } + $html .= ''; + } + } + $html .= ''; + return $html; + } - /** - * Generates a string representation of a predefined table and its head with - * titles from options. - * - * @return string Html code representation of the table's header. - */ - private function generateTableHeader() : string - { - $html = ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - return $html; - } + /** + * Generates a string representation of a predefined table and its head with + * titles from options. + * + * @return string Html code representation of the table's header. + */ + private function generateTableHeader() : string + { + $html = '
OldNewDifferences
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + return $html; + } - /** - * Generates a string representation of empty table body. - * - * @return string Html code representing empty table body. - */ - private function generateSkippedTable() : string - { - $html = ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - return $html; - } + /** + * Generates a string representation of empty table body. + * + * @return string Html code representing empty table body. + */ + private function generateSkippedTable() : string + { + $html = ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines of text with no difference. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of text with no difference. - */ - private function generateTableRowsEqual(&$change) : string - { - $html = ""; - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $toLine = $change['changed']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - return $html; - } + /** + * Generates a string representation of one or more rows of a table of lines of text with no difference. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of text with no difference. + */ + private function generateTableRowsEqual(&$change) : string + { + $html = ""; + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines, where new text was added. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of added text. - */ - private function generateTableRowsInsert(&$change) : string - { - $html = ""; - foreach ($change['changed']['lines'] as $no => $line) { - $toLine = $change['changed']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - return $html; - } + /** + * Generates a string representation of one or more rows of a table of lines, where new text was added. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of added text. + */ + private function generateTableRowsInsert(&$change) : string + { + $html = ""; + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines, where text was removed. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of removed text. - */ - private function generateTableRowsDelete(&$change) : string - { - $html = ""; - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - return $html; - } + /** + * Generates a string representation of one or more rows of a table of lines, where text was removed. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of removed text. + */ + private function generateTableRowsDelete(&$change) : string + { + $html = ""; + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines, where text was partially modified. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of modified. - */ - private function generateTableRowsReplace(&$change) : string - { - $html = ""; + /** + * Generates a string representation of one or more rows of a table of lines, where text was partially modified. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of modified. + */ + private function generateTableRowsReplace(&$change) : string + { + $html = ""; - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } - foreach ($change['changed']['lines'] as $no => $line) { - $toLine = $change['changed']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } - return $html; - } + return $html; + } } diff --git a/lib/jblond/Diff/Renderer/Html/SideBySide.php b/lib/jblond/Diff/Renderer/Html/SideBySide.php index 20c83c62..57c9fd09 100644 --- a/lib/jblond/Diff/Renderer/Html/SideBySide.php +++ b/lib/jblond/Diff/Renderer/Html/SideBySide.php @@ -48,197 +48,197 @@ */ class SideBySide extends HtmlArray { - /** - * Render a and return diff with changes between the two sequences - * displayed side by side. - * - * @return string The generated side by side diff. - */ - public function render() : string - { - $changes = parent::render(); + /** + * Render a and return diff with changes between the two sequences + * displayed side by side. + * + * @return string The generated side by side diff. + */ + public function render() : string + { + $changes = parent::render(); - $html = ''; - if (empty($changes)) { - return $html; - } + $html = ''; + if (empty($changes)) { + return $html; + } - $html .= $this->generateTableHeader(); + $html .= $this->generateTableHeader(); - foreach ($changes as $i => $blocks) { - if ($i > 0) { - $html .= $this->generateSkippedTable(); - } + foreach ($changes as $i => $blocks) { + if ($i > 0) { + $html .= $this->generateSkippedTable(); + } - foreach ($blocks as $change) { - $html .= ''; - switch ($change['tag']) { - // Equal changes should be shown on both sides of the diff - case 'equal': - $html .= $this->generateTableRowsEqual($change); - break; - // Added lines only on the right side - case 'insert': - $html .= $this->generateTableRowsInsert($change); - break; - // Show deleted lines only on the left side - case 'delete': - $html .= $this->generateTableRowsDelete($change); - break; - // Show modified lines on both sides - case 'replace': - $html .= $this->generateTableRowsReplace($change); - break; - } - $html .= ''; - } - } - $html .= '
OldNewDifferences
 
 
'.$fromLine.''.$toLine.''.$line.'
'.$fromLine.''.$toLine.''.$line.'
 '.$toLine.''.$line.' 
 '.$toLine.''.$line.' 
'.$fromLine.' '.$line.' 
'.$fromLine.' '.$line.' 
'.$fromLine.' '.$line.'
'.$fromLine.' '.$line.'
 '.$toLine.''.$line.'
 '.$toLine.''.$line.'
'; - return $html; - } + foreach ($blocks as $change) { + $html .= ''; + switch ($change['tag']) { + // Equal changes should be shown on both sides of the diff + case 'equal': + $html .= $this->generateTableRowsEqual($change); + break; + // Added lines only on the right side + case 'insert': + $html .= $this->generateTableRowsInsert($change); + break; + // Show deleted lines only on the left side + case 'delete': + $html .= $this->generateTableRowsDelete($change); + break; + // Show modified lines on both sides + case 'replace': + $html .= $this->generateTableRowsReplace($change); + break; + } + $html .= ''; + } + } + $html .= ''; + return $html; + } - /** - * Generates a string representation of a predefined table and its head with - * titles from options. - * - * @return string Html code representation of the table's header. - */ - private function generateTableHeader() : string - { - $html = ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - return $html; - } + /** + * Generates a string representation of a predefined table and its head with + * titles from options. + * + * @return string Html code representation of the table's header. + */ + private function generateTableHeader() : string + { + $html = '
'.$this->options['title_a'].''.$this->options['title_b'].'
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + return $html; + } - /** - * Generates a string representation of empty table body. - * - * @return string Html code representing empty table body. - */ - private function generateSkippedTable() : string - { - $html = ''; - $html .= ''; - $html .= ''; - $html .= ''; - return $html; - } + /** + * Generates a string representation of empty table body. + * + * @return string Html code representing empty table body. + */ + private function generateSkippedTable() : string + { + $html = ''; + $html .= ''; + $html .= ''; + $html .= ''; + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines of text with no difference. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of text with no difference. - */ - private function generateTableRowsEqual(&$change) : string - { - $html = ""; - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $toLine = $change['changed']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - return $html; - } + /** + * Generates a string representation of one or more rows of a table of lines of text with no difference. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of text with no difference. + */ + private function generateTableRowsEqual(&$change) : string + { + $html = ""; + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines, where new text was added. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of added text. - */ - private function generateTableRowsInsert(&$change) : string - { - $html = ""; - foreach ($change['changed']['lines'] as $no => $line) { - $toLine = $change['changed']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - return $html; - } + /** + * Generates a string representation of one or more rows of a table of lines, where new text was added. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of added text. + */ + private function generateTableRowsInsert(&$change) : string + { + $html = ""; + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines, where text was removed. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of removed text. - */ - private function generateTableRowsDelete(&$change) : string - { - $html = ""; - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - return $html; - } + /** + * Generates a string representation of one or more rows of a table of lines, where text was removed. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of removed text. + */ + private function generateTableRowsDelete(&$change) : string + { + $html = ""; + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + return $html; + } - /** - * Generates a string representation of one or more rows of a table of lines, where text was partially modified. - * - * @param array &$change Array with data about changes. - * @return string Html code representing one or more rows of modified. - */ - private function generateTableRowsReplace(&$change) : string - { - $html = ""; + /** + * Generates a string representation of one or more rows of a table of lines, where text was partially modified. + * + * @param array &$change Array with data about changes. + * @return string Html code representing one or more rows of modified. + */ + private function generateTableRowsReplace(&$change) : string + { + $html = ""; - if (count($change['base']['lines']) >= count($change['changed']['lines'])) { - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - if (!isset($change['changed']['lines'][$no])) { - $toLine = ' '; - $changedLine = ' '; - } else { - $toLine = $change['changed']['offset'] + $no + 1; - $changedLine = ''.$change['changed']['lines'][$no].''; - } - $html .= ''; - $html .= ''; - $html .= ''; - } - } else { - foreach ($change['changed']['lines'] as $no => $changedLine) { - if (!isset($change['base']['lines'][$no])) { - $fromLine = ' '; - $line = ' '; - } else { - $fromLine = $change['base']['offset'] + $no + 1; - $line = ''.$change['base']['lines'][$no].''; - } - $html .= ''; - $html .= ''; - $html .= ''; - $toLine = $change['changed']['offset'] + $no + 1; - $html .= ''; - $html .= ''; - $html .= ''; - } - } + if (count($change['base']['lines']) >= count($change['changed']['lines'])) { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + if (!isset($change['changed']['lines'][$no])) { + $toLine = ' '; + $changedLine = ' '; + } else { + $toLine = $change['changed']['offset'] + $no + 1; + $changedLine = ''.$change['changed']['lines'][$no].''; + } + $html .= ''; + $html .= ''; + $html .= ''; + } + } else { + foreach ($change['changed']['lines'] as $no => $changedLine) { + if (!isset($change['base']['lines'][$no])) { + $fromLine = ' '; + $line = ' '; + } else { + $fromLine = $change['base']['offset'] + $no + 1; + $line = ''.$change['base']['lines'][$no].''; + } + $html .= ''; + $html .= ''; + $html .= ''; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= ''; + $html .= ''; + $html .= ''; + } + } - return $html; - } + return $html; + } } diff --git a/lib/jblond/Diff/Renderer/RendererAbstract.php b/lib/jblond/Diff/Renderer/RendererAbstract.php index 895e629a..401b3e53 100644 --- a/lib/jblond/Diff/Renderer/RendererAbstract.php +++ b/lib/jblond/Diff/Renderer/RendererAbstract.php @@ -44,44 +44,44 @@ */ abstract class RendererAbstract { - /** - * @var object Instance of the diff class that this renderer is generating the rendered diff for. - */ - public $diff; + /** + * @var object Instance of the diff class that this renderer is generating the rendered diff for. + */ + public $diff; - /** - * @var array Array of the default options that apply to this renderer. - */ + /** + * @var array Array of the default options that apply to this renderer. + */ protected $defaultOptions = array( 'title_a' => 'Old Version', 'title_b' => 'New Version', ); - /** - * @var array Array containing the user applied and merged default options for the renderer. - */ - protected $options = array(); + /** + * @var array Array containing the user applied and merged default options for the renderer. + */ + protected $options = array(); - /** - * The constructor. Instantiates the rendering engine and if options are passed, - * sets the options for the renderer. - * - * @param array $options Optionally, an array of the options for the renderer. - */ - public function __construct(array $options = array()) - { - $this->setOptions($options); - } + /** + * The constructor. Instantiates the rendering engine and if options are passed, + * sets the options for the renderer. + * + * @param array $options Optionally, an array of the options for the renderer. + */ + public function __construct(array $options = array()) + { + $this->setOptions($options); + } - /** - * Set the options of the renderer to those supplied in the passed in array. - * Options are merged with the default to ensure that there aren't any missing - * options. - * - * @param array $options Array of options to set. - */ - public function setOptions(array $options) - { - $this->options = array_merge($this->defaultOptions, $options); - } + /** + * Set the options of the renderer to those supplied in the passed in array. + * Options are merged with the default to ensure that there aren't any missing + * options. + * + * @param array $options Array of options to set. + */ + public function setOptions(array $options) + { + $this->options = array_merge($this->defaultOptions, $options); + } } diff --git a/lib/jblond/Diff/Renderer/Text/Context.php b/lib/jblond/Diff/Renderer/Text/Context.php index 65d71e9f..8865c9b6 100644 --- a/lib/jblond/Diff/Renderer/Text/Context.php +++ b/lib/jblond/Diff/Renderer/Text/Context.php @@ -50,85 +50,85 @@ */ class Context extends RendererAbstract { - /** - * @var array Array of the different op code tags and how they map to the context diff equivalent. - */ - private $tagMap = array( - 'insert' => '+', - 'delete' => '-', - 'replace' => '!', - 'equal' => ' ' - ); + /** + * @var array Array of the different op code tags and how they map to the context diff equivalent. + */ + private $tagMap = array( + 'insert' => '+', + 'delete' => '-', + 'replace' => '!', + 'equal' => ' ' + ); - /** - * Render and return a context formatted (old school!) diff file. - * - * @return string The generated context diff. - */ - public function render() : string - { - $diff = ''; - $opCodes = $this->diff->getGroupedOpcodes(); - foreach ($opCodes as $group) { - $diff .= "***************\n"; - $lastItem = count($group)-1; - $i1 = $group['0']['1']; - $i2 = $group[$lastItem]['2']; - $j1 = $group['0']['3']; - $j2 = $group[$lastItem]['4']; + /** + * Render and return a context formatted (old school!) diff file. + * + * @return string The generated context diff. + */ + public function render() : string + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach ($opCodes as $group) { + $diff .= "***************\n"; + $lastItem = count($group)-1; + $i1 = $group['0']['1']; + $i2 = $group[$lastItem]['2']; + $j1 = $group['0']['3']; + $j2 = $group[$lastItem]['4']; - if ($i2 - $i1 >= 2) { - $diff .= '*** '.($group['0']['1'] + 1).','.$i2." ****\n"; - } else { - $diff .= '*** '.$i2." ****\n"; - } + if ($i2 - $i1 >= 2) { + $diff .= '*** '.($group['0']['1'] + 1).','.$i2." ****\n"; + } else { + $diff .= '*** '.$i2." ****\n"; + } - if ($j2 - $j1 >= 2) { - $separator = '--- '.($j1 + 1).','.$j2." ----\n"; - } else { - $separator = '--- '.$j2." ----\n"; - } + if ($j2 - $j1 >= 2) { + $separator = '--- '.($j1 + 1).','.$j2." ----\n"; + } else { + $separator = '--- '.$j2." ----\n"; + } - $hasVisible = false; - foreach ($group as $code) { - if ($code['0'] == 'replace' || $code['0'] == 'delete') { - $hasVisible = true; - break; - } - } + $hasVisible = false; + foreach ($group as $code) { + if ($code['0'] == 'replace' || $code['0'] == 'delete') { + $hasVisible = true; + break; + } + } - if ($hasVisible) { - foreach ($group as $code) { - list($tag, $i1, $i2, $j1, $j2) = $code; - if ($tag == 'insert') { - continue; - } - $diff .= $this->tagMap[$tag] . ' ' . - implode("\n" . $this->tagMap[$tag] .' ', $this->diff->GetA($i1, $i2)) . "\n"; - } - } + if ($hasVisible) { + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag == 'insert') { + continue; + } + $diff .= $this->tagMap[$tag] . ' ' . + implode("\n" . $this->tagMap[$tag] .' ', $this->diff->GetA($i1, $i2)) . "\n"; + } + } - $hasVisible = false; - foreach ($group as $code) { - if ($code['0'] == 'replace' || $code['0'] == 'insert') { - $hasVisible = true; - break; - } - } + $hasVisible = false; + foreach ($group as $code) { + if ($code['0'] == 'replace' || $code['0'] == 'insert') { + $hasVisible = true; + break; + } + } - $diff .= $separator; + $diff .= $separator; - if ($hasVisible) { - foreach ($group as $code) { - list($tag, $i1, $i2, $j1, $j2) = $code; - if ($tag == 'delete') { - continue; - } - $diff .= $this->tagMap[$tag] . ' ' . - implode("\n".$this->tagMap[$tag] . ' ', $this->diff->GetB($j1, $j2)) . "\n"; - } - } - } - return $diff; - } + if ($hasVisible) { + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag == 'delete') { + continue; + } + $diff .= $this->tagMap[$tag] . ' ' . + implode("\n".$this->tagMap[$tag] . ' ', $this->diff->GetB($j1, $j2)) . "\n"; + } + } + } + return $diff; + } } diff --git a/lib/jblond/Diff/Renderer/Text/Unified.php b/lib/jblond/Diff/Renderer/Text/Unified.php index 295a7ad1..901a95b6 100644 --- a/lib/jblond/Diff/Renderer/Text/Unified.php +++ b/lib/jblond/Diff/Renderer/Text/Unified.php @@ -50,43 +50,43 @@ */ class Unified extends RendererAbstract { - /** - * Render and return a unified diff. - * - * @return string The unified diff. - */ - public function render() : string - { - $diff = ''; - $opCodes = $this->diff->getGroupedOpcodes(); - foreach ($opCodes as $group) { - $lastItem = count($group)-1; - $i1 = $group['0']['1']; - $i2 = $group[$lastItem]['2']; - $j1 = $group['0']['3']; - $j2 = $group[$lastItem]['4']; + /** + * Render and return a unified diff. + * + * @return string The unified diff. + */ + public function render() : string + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach ($opCodes as $group) { + $lastItem = count($group)-1; + $i1 = $group['0']['1']; + $i2 = $group[$lastItem]['2']; + $j1 = $group['0']['3']; + $j2 = $group[$lastItem]['4']; - if ($i1 == 0 && $i2 == 0) { - $i1 = -1; - $i2 = -1; - } + if ($i1 == 0 && $i2 == 0) { + $i1 = -1; + $i2 = -1; + } - $diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n"; - foreach ($group as $code) { - list($tag, $i1, $i2, $j1, $j2) = $code; - if ($tag == 'equal') { - $diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n"; - } else { - if ($tag == 'replace' || $tag == 'delete') { - $diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n"; - } + $diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n"; + foreach ($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag == 'equal') { + $diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n"; + } else { + if ($tag == 'replace' || $tag == 'delete') { + $diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n"; + } - if ($tag == 'replace' || $tag == 'insert') { - $diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n"; - } - } - } - } - return $diff; - } + if ($tag == 'replace' || $tag == 'insert') { + $diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n"; + } + } + } + } + return $diff; + } } diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index 1791cfc3..5d3f5a15 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -44,677 +44,677 @@ */ class SequenceMatcher { - /** - * @var string|array Either a string or an array containing a callback function to determine - * if a line is "junk" or not. - */ - private $junkCallback = null; - - /** - * @var array The first sequence to compare against. - */ - private $a = array(); - - /** - * @var array The second sequence. - */ - private $b = array(); - - /** - * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. - */ - private $junkDict = array(); - - /** - * @var array Array of indices that do not contain junk elements. - */ - private $b2j = array(); - - /** - * @var array - */ - private $options = array(); - - /** - * @var null|array - */ - private $opCodes; - - /** - * @var null|array - */ - private $matchingBlocks; - - /** - * @var null|array - */ - private $fullBCount; - - /** - * @var array - */ - private $defaultOptions = array( - 'ignoreNewLines' => false, - 'ignoreWhitespace' => false, - 'ignoreCase' => false - ); - - /** - * The constructor. With the sequences being passed, they'll be set for the - * sequence matcher and it will perform a basic cleanup & calculate junk - * elements. - * - * @param string|array $a A string or array containing the lines to compare against. - * @param string|array $b A string or array containing the lines to compare. - * @param array $options - * @param string|array|null $junkCallback Either an array or string that references a callback function - * (if there is one) to determine 'junk' characters. - */ - public function __construct($a, $b, $options, $junkCallback = null) - { - $this->a = array(); - $this->b = array(); - $this->junkCallback = $junkCallback; - $this->setOptions($options); - $this->setSequences($a, $b); - } - - /** - * @param array $options - */ - public function setOptions($options) - { - $this->options = array_merge($this->defaultOptions, $options); - } - - /** - * Set the first and second sequences to use with the sequence matcher. - * - * @param string|array $a A string or array containing the lines to compare against. - * @param string|array $b A string or array containing the lines to compare. - */ - public function setSequences($a, $b) - { - $this->setSeq1($a); - $this->setSeq2($b); - } - - /** - * Set the first sequence ($a) and reset any internal caches to indicate that - * when calling the calculation methods, we need to recalculate them. - * - * @param string|array $a The sequence to set as the first sequence. - */ - public function setSeq1($a) - { - if (!is_array($a)) { - $a = str_split($a); - } - if ($a == $this->a) { - return; - } - - $this->a = $a; - $this->matchingBlocks = null; - $this->opCodes = null; - } - - /** - * Set the second sequence ($b) and reset any internal caches to indicate that - * when calling the calculation methods, we need to recalculate them. - * - * @param string|array $b The sequence to set as the second sequence. - */ - public function setSeq2($b) - { - if (!is_array($b)) { - $b = str_split($b); - } - if ($b == $this->b) { - return; - } - - $this->b = $b; - $this->matchingBlocks = null; - $this->opCodes = null; - $this->fullBCount = null; - $this->chainB(); - } - - /** - * Generate the internal arrays containing the list of junk and non-junk - * characters for the second ($b) sequence. - */ - private function chainB() - { - $length = count($this->b); - $this->b2j = array(); - $popularDict = array(); - - for ($i = 0; $i < $length; ++$i) { - $char = $this->b[$i]; - if (isset($this->b2j[$char])) { - if ($length >= 200 && count($this->b2j[$char]) * 100 > $length) { - $popularDict[$char] = 1; - unset($this->b2j[$char]); - } else { - $this->b2j[$char][] = $i; - } - } else { - $this->b2j[$char] = array( - $i - ); - } - } - - // Remove leftovers - foreach (array_keys($popularDict) as $char) { - unset($this->b2j[$char]); - } - - $this->junkDict = array(); - if (is_callable($this->junkCallback)) { - foreach (array_keys($popularDict) as $char) { - if (call_user_func($this->junkCallback, $char)) { - $this->junkDict[$char] = 1; - unset($popularDict[$char]); - } - } - - foreach (array_keys($this->b2j) as $char) { - if (call_user_func($this->junkCallback, $char)) { - $this->junkDict[$char] = 1; - unset($this->b2j[$char]); - } - } - } - } - - /** - * Checks if a particular character is in the junk dictionary - * for the list of junk characters. - * - * @param string $b - * @return bool $b True if the character is considered junk. False if not. - */ - private function isBJunk($b) : bool - { - if (isset($this->junkDict[$b])) { - return true; - } - - return false; - } - - /** - * Find the longest matching block in the two sequences, as defined by the - * lower and upper constraints for each sequence. (for the first sequence, - * $alo - $ahi and for the second sequence, $blo - $bhi) - * - * Essentially, of all of the maximal matching blocks, return the one that - * starts earliest in $a, and all of those maximal matching blocks that - * start earliest in $a, return the one that starts earliest in $b. - * - * If the junk callback is defined, do the above but with the restriction - * that the junk element appears in the block. Extend it as far as possible - * by matching only junk elements in both $a and $b. - * - * @param int $alo The lower constraint for the first sequence. - * @param int $ahi The upper constraint for the first sequence. - * @param int $blo The lower constraint for the second sequence. - * @param int $bhi The upper constraint for the second sequence. - * @return array Array containing the longest match that includes the starting position in $a, - * start in $b and the length/size. - */ - public function findLongestMatch($alo, $ahi, $blo, $bhi) : array - { - $a = $this->a; - $b = $this->b; - - $bestI = $alo; - $bestJ = $blo; - $bestSize = 0; - - $j2Len = array(); - $nothing = array(); - - for ($i = $alo; $i < $ahi; ++$i) { - $newJ2Len = array(); - $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing); - foreach ($jDict as $jKey => $j) { - if ($j < $blo) { - continue; - } elseif ($j >= $bhi) { - break; - } - - $k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1; - $newJ2Len[$j] = $k; - if ($k > $bestSize) { - $bestI = $i - $k + 1; - $bestJ = $j - $k + 1; - $bestSize = $k; - } - } - - $j2Len = $newJ2Len; - } - - while ($bestI > $alo && - $bestJ > $blo && - !$this->isBJunk($b[$bestJ - 1]) && - !$this->linesAreDifferent($bestI - 1, $bestJ - 1) - ) { - --$bestI; - --$bestJ; - ++$bestSize; - } - - while ($bestI + $bestSize < $ahi && - ($bestJ + $bestSize) < $bhi && - !$this->isBJunk($b[$bestJ + $bestSize]) && - !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { - ++$bestSize; - } - - while ($bestI > $alo && - $bestJ > $blo && - $this->isBJunk($b[$bestJ - 1]) && - !$this->linesAreDifferent($bestI - 1, $bestJ - 1) - ) { - --$bestI; - --$bestJ; - ++$bestSize; - } - - while ($bestI + $bestSize < $ahi && - $bestJ + $bestSize < $bhi && - $this->isBJunk($b[$bestJ + $bestSize]) && - !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize) - ) { - ++$bestSize; - } - - return array( - $bestI, - $bestJ, - $bestSize - ); - } - - /** - * Check if the two lines at the given indexes are different or not. - * - * @param int $aIndex Line number to check against in a. - * @param int $bIndex Line number to check against in b. - * @return bool True if the lines are different and false if not. - */ - public function linesAreDifferent($aIndex, $bIndex) : bool - { - $lineA = $this->a[$aIndex]; - $lineB = $this->b[$bIndex]; - - if ($this->options['ignoreWhitespace']) { - $replace = array("\t", ' '); - $lineA = str_replace($replace, '', $lineA); - $lineB = str_replace($replace, '', $lineB); - } - - if ($this->options['ignoreCase']) { - $lineA = strtolower($lineA); - $lineB = strtolower($lineB); - } - - if ($lineA != $lineB) { - return true; - } - - return false; - } - - /** - * Return a nested set of arrays for all of the matching sub-sequences - * in the strings $a and $b. - * - * Each block contains the lower constraint of the block in $a, the lower - * constraint of the block in $b and finally the number of lines that the - * block continues for. - * - * @return array Nested array of the matching blocks, as described by the function. - */ - public function getMatchingBlocks() : array - { - if (!empty($this->matchingBlocks)) { - return $this->matchingBlocks; - } - - $aLength = count($this->a); - $bLength = count($this->b); - - $queue = array( - array( - 0, - $aLength, - 0, - $bLength - ) - ); - - $matchingBlocks = array(); - while (!empty($queue)) { - list($alo, $ahi, $blo, $bhi) = array_pop($queue); - $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi); - list($i, $j, $k) = $x; - if ($k) { - $matchingBlocks[] = $x; - if ($alo < $i && $blo < $j) { - $queue[] = array( - $alo, - $i, - $blo, - $j - ); - } - - if ($i + $k < $ahi && $j + $k < $bhi) { - $queue[] = array( - $i + $k, - $ahi, - $j + $k, - $bhi - ); - } - } - } - - usort($matchingBlocks, array($this, 'tupleSort')); - - $i1 = 0; - $j1 = 0; - $k1 = 0; - $nonAdjacent = array(); - foreach ($matchingBlocks as $block) { - list($i2, $j2, $k2) = $block; - if ($i1 + $k1 == $i2 && $j1 + $k1 == $j2) { - $k1 += $k2; - } else { - if ($k1) { - $nonAdjacent[] = array( - $i1, - $j1, - $k1 - ); - } - - $i1 = $i2; - $j1 = $j2; - $k1 = $k2; - } - } - - if ($k1) { - $nonAdjacent[] = array( - $i1, - $j1, - $k1 - ); - } - - $nonAdjacent[] = array( - $aLength, - $bLength, - 0 - ); - - $this->matchingBlocks = $nonAdjacent; - return $this->matchingBlocks; - } - - /** - * Return a list of all of the op codes for the differences between the - * two strings. - * - * The nested array returned contains an array describing the op code - * which includes: - * 0 - The type of tag (as described below) for the op code. - * 1 - The beginning line in the first sequence. - * 2 - The end line in the first sequence. - * 3 - The beginning line in the second sequence. - * 4 - The end line in the second sequence. - * - * The different types of tags include: - * replace - The string from $i1 to $i2 in $a should be replaced by - * the string in $b from $j1 to $j2. - * delete - The string in $a from $i1 to $j2 should be deleted. - * insert - The string in $b from $j1 to $j2 should be inserted at - * $i1 in $a. - * equal - The two strings with the specified ranges are equal. - * - * @return array Array of the opcodes describing the differences between the strings. - */ - public function getOpCodes() : array - { - if (!empty($this->opCodes)) { - return $this->opCodes; - } - - $i = 0; - $j = 0; - $this->opCodes = array(); - - $blocks = $this->getMatchingBlocks(); - foreach ($blocks as $block) { - list($ai, $bj, $size) = $block; - $tag = ''; - if ($i < $ai && $j < $bj) { - $tag = 'replace'; - } elseif ($i < $ai) { - $tag = 'delete'; - } elseif ($j < $bj) { - $tag = 'insert'; - } - - if ($tag) { - $this->opCodes[] = array( - $tag, - $i, - $ai, - $j, - $bj - ); - } - - $i = $ai + $size; - $j = $bj + $size; - - if ($size) { - $this->opCodes[] = array( - 'equal', - $ai, - $i, - $bj, - $j - ); - } - } - return $this->opCodes; - } - - /** - * Return a series of nested arrays containing different groups of generated - * op codes for the differences between the strings with up to $context lines - * of surrounding content. - * - * Essentially what happens here is any big equal blocks of strings are stripped - * out, the smaller subsets of changes are then arranged in to their groups. - * This means that the sequence matcher and diffs do not need to include the full - * content of the different files but can still provide context as to where the - * changes are. - * - * @param int $context The number of lines of context to provide around the groups. - * @return array Nested array of all of the grouped op codes. - */ - public function getGroupedOpcodes($context = 3) : array - { - $opCodes = $this->getOpCodes(); - if (empty($opCodes)) { - $opCodes = array( - array( - 'equal', - 0, - 1, - 0, - 1 - ) - ); - } - - if ($opCodes['0']['0'] == 'equal') { - $opCodes['0'] = array( - $opCodes['0']['0'], - max($opCodes['0']['1'], $opCodes['0']['2'] - $context), - $opCodes['0']['2'], - max($opCodes['0']['3'], $opCodes['0']['4'] - $context), - $opCodes['0']['4'] - ); - } - - $lastItem = count($opCodes) - 1; - if ($opCodes[$lastItem]['0'] == 'equal') { - list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem]; - $opCodes[$lastItem] = array( - $tag, - $i1, - min($i2, $i1 + $context), - $j1, - min($j2, $j1 + $context) - ); - } - - $maxRange = $context * 2; - $groups = array(); - $group = array(); - - foreach ($opCodes as $code) { - list($tag, $i1, $i2, $j1, $j2) = $code; - if ($tag == 'equal' && $i2 - $i1 > $maxRange) { - $group[] = array( - $tag, - $i1, - min($i2, $i1 + $context), - $j1, - min($j2, $j1 + $context) - ); - $groups[] = $group; - $group = array(); - $i1 = max($i1, $i2 - $context); - $j1 = max($j1, $j2 - $context); - } - $group[] = array( - $tag, - $i1, - $i2, - $j1, - $j2 - ); - } - - if (!empty($group) && !(count($group) == 1 && $group['0']['0'] == 'equal')) { - $groups[] = $group; - } - - return $groups; - } - - /** - * Return a measure of the similarity between the two sequences. - * This will be a float value between 0 and 1. - * - * Out of all of the ratio calculation functions, this is the most - * expensive to call if getMatchingBlocks or getOpCodes is yet to be - * called. - * - * The ratio is calculated as (2 * number of matches) / total number of - * elements in both sequences. - * - * @return float The calculated ratio. - */ - public function ratio() : float - { - $matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0); - return $this->calculateRatio($matches, count($this->a) + count($this->b)); - } - - /** - * Helper function to calculate the number of matches for Ratio(). - * - * @param int $sum The running total for the number of matches. - * @param array $triple Array containing the matching block triple to add to the running total. - * @return int The new running total for the number of matches. - */ - private function ratioReduce($sum, $triple) : int - { - return $sum + ($triple[count($triple) - 1]); - } - - /** - * Helper function for calculating the ratio to measure similarity for the strings. - * The ratio is defined as being 2 * (number of matches / total length) - * - * @param int $matches The number of matches in the two strings. - * @param int $length The length of the two strings. - * @return float The calculated ratio. - */ - private function calculateRatio($matches, $length = 0) : float - { - if ($length) { - return 2 * ($matches / $length); - } - return 1; - } - - /** - * Helper function that provides the ability to return the value for a key - * in an array of it exists, or if it doesn't then return a default value. - * Essentially cleaner than doing a series of if (isset()) {} else {} calls. - * - * @param array $array The array to search. - * @param string|int $key The key to check that exists. - * @param mixed $default The value to return as the default value if the key doesn't exist. - * @return mixed The value from the array if the key exists or otherwise the default. - */ - private function arrayGetDefault($array, $key, $default) - { - if (isset($array[$key])) { - return $array[$key]; - } - return $default; - } - - /** - * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks - * - * @param array $a First array to compare. - * @param array $b Second array to compare. - * @return int -1, 0 or 1, as expected by the usort function. - */ - private function tupleSort($a, $b) : int - { - $max = max(count($a), count($b)); - for ($i = 0; $i < $max; ++$i) { - if ($a[$i] < $b[$i]) { - return -1; - } elseif ($a[$i] > $b[$i]) { - return 1; - } - } - - if (count($a) == count($b)) { - return 0; - } - if (count($a) < count($b)) { - return -1; - } - return 1; - } + /** + * @var string|array Either a string or an array containing a callback function to determine + * if a line is "junk" or not. + */ + private $junkCallback = null; + + /** + * @var array The first sequence to compare against. + */ + private $a = array(); + + /** + * @var array The second sequence. + */ + private $b = array(); + + /** + * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. + */ + private $junkDict = array(); + + /** + * @var array Array of indices that do not contain junk elements. + */ + private $b2j = array(); + + /** + * @var array + */ + private $options = array(); + + /** + * @var null|array + */ + private $opCodes; + + /** + * @var null|array + */ + private $matchingBlocks; + + /** + * @var null|array + */ + private $fullBCount; + + /** + * @var array + */ + private $defaultOptions = array( + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * The constructor. With the sequences being passed, they'll be set for the + * sequence matcher and it will perform a basic cleanup & calculate junk + * elements. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + * @param array $options + * @param string|array|null $junkCallback Either an array or string that references a callback function + * (if there is one) to determine 'junk' characters. + */ + public function __construct($a, $b, $options, $junkCallback = null) + { + $this->a = array(); + $this->b = array(); + $this->junkCallback = $junkCallback; + $this->setOptions($options); + $this->setSequences($a, $b); + } + + /** + * @param array $options + */ + public function setOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + /** + * Set the first and second sequences to use with the sequence matcher. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + */ + public function setSequences($a, $b) + { + $this->setSeq1($a); + $this->setSeq2($b); + } + + /** + * Set the first sequence ($a) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $a The sequence to set as the first sequence. + */ + public function setSeq1($a) + { + if (!is_array($a)) { + $a = str_split($a); + } + if ($a == $this->a) { + return; + } + + $this->a = $a; + $this->matchingBlocks = null; + $this->opCodes = null; + } + + /** + * Set the second sequence ($b) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $b The sequence to set as the second sequence. + */ + public function setSeq2($b) + { + if (!is_array($b)) { + $b = str_split($b); + } + if ($b == $this->b) { + return; + } + + $this->b = $b; + $this->matchingBlocks = null; + $this->opCodes = null; + $this->fullBCount = null; + $this->chainB(); + } + + /** + * Generate the internal arrays containing the list of junk and non-junk + * characters for the second ($b) sequence. + */ + private function chainB() + { + $length = count($this->b); + $this->b2j = array(); + $popularDict = array(); + + for ($i = 0; $i < $length; ++$i) { + $char = $this->b[$i]; + if (isset($this->b2j[$char])) { + if ($length >= 200 && count($this->b2j[$char]) * 100 > $length) { + $popularDict[$char] = 1; + unset($this->b2j[$char]); + } else { + $this->b2j[$char][] = $i; + } + } else { + $this->b2j[$char] = array( + $i + ); + } + } + + // Remove leftovers + foreach (array_keys($popularDict) as $char) { + unset($this->b2j[$char]); + } + + $this->junkDict = array(); + if (is_callable($this->junkCallback)) { + foreach (array_keys($popularDict) as $char) { + if (call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($popularDict[$char]); + } + } + + foreach (array_keys($this->b2j) as $char) { + if (call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($this->b2j[$char]); + } + } + } + } + + /** + * Checks if a particular character is in the junk dictionary + * for the list of junk characters. + * + * @param string $b + * @return bool $b True if the character is considered junk. False if not. + */ + private function isBJunk($b) : bool + { + if (isset($this->junkDict[$b])) { + return true; + } + + return false; + } + + /** + * Find the longest matching block in the two sequences, as defined by the + * lower and upper constraints for each sequence. (for the first sequence, + * $alo - $ahi and for the second sequence, $blo - $bhi) + * + * Essentially, of all of the maximal matching blocks, return the one that + * starts earliest in $a, and all of those maximal matching blocks that + * start earliest in $a, return the one that starts earliest in $b. + * + * If the junk callback is defined, do the above but with the restriction + * that the junk element appears in the block. Extend it as far as possible + * by matching only junk elements in both $a and $b. + * + * @param int $alo The lower constraint for the first sequence. + * @param int $ahi The upper constraint for the first sequence. + * @param int $blo The lower constraint for the second sequence. + * @param int $bhi The upper constraint for the second sequence. + * @return array Array containing the longest match that includes the starting position in $a, + * start in $b and the length/size. + */ + public function findLongestMatch($alo, $ahi, $blo, $bhi) : array + { + $a = $this->a; + $b = $this->b; + + $bestI = $alo; + $bestJ = $blo; + $bestSize = 0; + + $j2Len = array(); + $nothing = array(); + + for ($i = $alo; $i < $ahi; ++$i) { + $newJ2Len = array(); + $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing); + foreach ($jDict as $jKey => $j) { + if ($j < $blo) { + continue; + } elseif ($j >= $bhi) { + break; + } + + $k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1; + $newJ2Len[$j] = $k; + if ($k > $bestSize) { + $bestI = $i - $k + 1; + $bestJ = $j - $k + 1; + $bestSize = $k; + } + } + + $j2Len = $newJ2Len; + } + + while ($bestI > $alo && + $bestJ > $blo && + !$this->isBJunk($b[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1) + ) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while ($bestI + $bestSize < $ahi && + ($bestJ + $bestSize) < $bhi && + !$this->isBJunk($b[$bestJ + $bestSize]) && + !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + while ($bestI > $alo && + $bestJ > $blo && + $this->isBJunk($b[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1) + ) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while ($bestI + $bestSize < $ahi && + $bestJ + $bestSize < $bhi && + $this->isBJunk($b[$bestJ + $bestSize]) && + !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize) + ) { + ++$bestSize; + } + + return array( + $bestI, + $bestJ, + $bestSize + ); + } + + /** + * Check if the two lines at the given indexes are different or not. + * + * @param int $aIndex Line number to check against in a. + * @param int $bIndex Line number to check against in b. + * @return bool True if the lines are different and false if not. + */ + public function linesAreDifferent($aIndex, $bIndex) : bool + { + $lineA = $this->a[$aIndex]; + $lineB = $this->b[$bIndex]; + + if ($this->options['ignoreWhitespace']) { + $replace = array("\t", ' '); + $lineA = str_replace($replace, '', $lineA); + $lineB = str_replace($replace, '', $lineB); + } + + if ($this->options['ignoreCase']) { + $lineA = strtolower($lineA); + $lineB = strtolower($lineB); + } + + if ($lineA != $lineB) { + return true; + } + + return false; + } + + /** + * Return a nested set of arrays for all of the matching sub-sequences + * in the strings $a and $b. + * + * Each block contains the lower constraint of the block in $a, the lower + * constraint of the block in $b and finally the number of lines that the + * block continues for. + * + * @return array Nested array of the matching blocks, as described by the function. + */ + public function getMatchingBlocks() : array + { + if (!empty($this->matchingBlocks)) { + return $this->matchingBlocks; + } + + $aLength = count($this->a); + $bLength = count($this->b); + + $queue = array( + array( + 0, + $aLength, + 0, + $bLength + ) + ); + + $matchingBlocks = array(); + while (!empty($queue)) { + list($alo, $ahi, $blo, $bhi) = array_pop($queue); + $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi); + list($i, $j, $k) = $x; + if ($k) { + $matchingBlocks[] = $x; + if ($alo < $i && $blo < $j) { + $queue[] = array( + $alo, + $i, + $blo, + $j + ); + } + + if ($i + $k < $ahi && $j + $k < $bhi) { + $queue[] = array( + $i + $k, + $ahi, + $j + $k, + $bhi + ); + } + } + } + + usort($matchingBlocks, array($this, 'tupleSort')); + + $i1 = 0; + $j1 = 0; + $k1 = 0; + $nonAdjacent = array(); + foreach ($matchingBlocks as $block) { + list($i2, $j2, $k2) = $block; + if ($i1 + $k1 == $i2 && $j1 + $k1 == $j2) { + $k1 += $k2; + } else { + if ($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $i1 = $i2; + $j1 = $j2; + $k1 = $k2; + } + } + + if ($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $nonAdjacent[] = array( + $aLength, + $bLength, + 0 + ); + + $this->matchingBlocks = $nonAdjacent; + return $this->matchingBlocks; + } + + /** + * Return a list of all of the op codes for the differences between the + * two strings. + * + * The nested array returned contains an array describing the op code + * which includes: + * 0 - The type of tag (as described below) for the op code. + * 1 - The beginning line in the first sequence. + * 2 - The end line in the first sequence. + * 3 - The beginning line in the second sequence. + * 4 - The end line in the second sequence. + * + * The different types of tags include: + * replace - The string from $i1 to $i2 in $a should be replaced by + * the string in $b from $j1 to $j2. + * delete - The string in $a from $i1 to $j2 should be deleted. + * insert - The string in $b from $j1 to $j2 should be inserted at + * $i1 in $a. + * equal - The two strings with the specified ranges are equal. + * + * @return array Array of the opcodes describing the differences between the strings. + */ + public function getOpCodes() : array + { + if (!empty($this->opCodes)) { + return $this->opCodes; + } + + $i = 0; + $j = 0; + $this->opCodes = array(); + + $blocks = $this->getMatchingBlocks(); + foreach ($blocks as $block) { + list($ai, $bj, $size) = $block; + $tag = ''; + if ($i < $ai && $j < $bj) { + $tag = 'replace'; + } elseif ($i < $ai) { + $tag = 'delete'; + } elseif ($j < $bj) { + $tag = 'insert'; + } + + if ($tag) { + $this->opCodes[] = array( + $tag, + $i, + $ai, + $j, + $bj + ); + } + + $i = $ai + $size; + $j = $bj + $size; + + if ($size) { + $this->opCodes[] = array( + 'equal', + $ai, + $i, + $bj, + $j + ); + } + } + return $this->opCodes; + } + + /** + * Return a series of nested arrays containing different groups of generated + * op codes for the differences between the strings with up to $context lines + * of surrounding content. + * + * Essentially what happens here is any big equal blocks of strings are stripped + * out, the smaller subsets of changes are then arranged in to their groups. + * This means that the sequence matcher and diffs do not need to include the full + * content of the different files but can still provide context as to where the + * changes are. + * + * @param int $context The number of lines of context to provide around the groups. + * @return array Nested array of all of the grouped op codes. + */ + public function getGroupedOpcodes($context = 3) : array + { + $opCodes = $this->getOpCodes(); + if (empty($opCodes)) { + $opCodes = array( + array( + 'equal', + 0, + 1, + 0, + 1 + ) + ); + } + + if ($opCodes['0']['0'] == 'equal') { + $opCodes['0'] = array( + $opCodes['0']['0'], + max($opCodes['0']['1'], $opCodes['0']['2'] - $context), + $opCodes['0']['2'], + max($opCodes['0']['3'], $opCodes['0']['4'] - $context), + $opCodes['0']['4'] + ); + } + + $lastItem = count($opCodes) - 1; + if ($opCodes[$lastItem]['0'] == 'equal') { + list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem]; + $opCodes[$lastItem] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + } + + $maxRange = $context * 2; + $groups = array(); + $group = array(); + + foreach ($opCodes as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if ($tag == 'equal' && $i2 - $i1 > $maxRange) { + $group[] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + $groups[] = $group; + $group = array(); + $i1 = max($i1, $i2 - $context); + $j1 = max($j1, $j2 - $context); + } + $group[] = array( + $tag, + $i1, + $i2, + $j1, + $j2 + ); + } + + if (!empty($group) && !(count($group) == 1 && $group['0']['0'] == 'equal')) { + $groups[] = $group; + } + + return $groups; + } + + /** + * Return a measure of the similarity between the two sequences. + * This will be a float value between 0 and 1. + * + * Out of all of the ratio calculation functions, this is the most + * expensive to call if getMatchingBlocks or getOpCodes is yet to be + * called. + * + * The ratio is calculated as (2 * number of matches) / total number of + * elements in both sequences. + * + * @return float The calculated ratio. + */ + public function ratio() : float + { + $matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0); + return $this->calculateRatio($matches, count($this->a) + count($this->b)); + } + + /** + * Helper function to calculate the number of matches for Ratio(). + * + * @param int $sum The running total for the number of matches. + * @param array $triple Array containing the matching block triple to add to the running total. + * @return int The new running total for the number of matches. + */ + private function ratioReduce($sum, $triple) : int + { + return $sum + ($triple[count($triple) - 1]); + } + + /** + * Helper function for calculating the ratio to measure similarity for the strings. + * The ratio is defined as being 2 * (number of matches / total length) + * + * @param int $matches The number of matches in the two strings. + * @param int $length The length of the two strings. + * @return float The calculated ratio. + */ + private function calculateRatio($matches, $length = 0) : float + { + if ($length) { + return 2 * ($matches / $length); + } + return 1; + } + + /** + * Helper function that provides the ability to return the value for a key + * in an array of it exists, or if it doesn't then return a default value. + * Essentially cleaner than doing a series of if (isset()) {} else {} calls. + * + * @param array $array The array to search. + * @param string|int $key The key to check that exists. + * @param mixed $default The value to return as the default value if the key doesn't exist. + * @return mixed The value from the array if the key exists or otherwise the default. + */ + private function arrayGetDefault($array, $key, $default) + { + if (isset($array[$key])) { + return $array[$key]; + } + return $default; + } + + /** + * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks + * + * @param array $a First array to compare. + * @param array $b Second array to compare. + * @return int -1, 0 or 1, as expected by the usort function. + */ + private function tupleSort($a, $b) : int + { + $max = max(count($a), count($b)); + for ($i = 0; $i < $max; ++$i) { + if ($a[$i] < $b[$i]) { + return -1; + } elseif ($a[$i] > $b[$i]) { + return 1; + } + } + + if (count($a) == count($b)) { + return 0; + } + if (count($a) < count($b)) { + return -1; + } + return 1; + } } diff --git a/phpcs.xml b/phpcs.xml index 536e8906..3b6ba4eb 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,8 +1,5 @@ php-diff - - - - + diff --git a/tests/Diff/Renderer/ArrayTest.php b/tests/Diff/Renderer/ArrayTest.php index 424aaa6b..f83392a8 100644 --- a/tests/Diff/Renderer/ArrayTest.php +++ b/tests/Diff/Renderer/ArrayTest.php @@ -12,78 +12,78 @@ class ArrayTest extends TestCase { - /** - * ArrayTest constructor. - * @param null $name - * @param array $data - * @param string $dataName - */ - public function __construct($name = null, array $data = [], $dataName = '') - { - require "../../../lib/Autoloader.php"; - new \jblond\Autoloader(); - parent::__construct($name, $data, $dataName); - } + /** + * ArrayTest constructor. + * @param null $name + * @param array $data + * @param string $dataName + */ + public function __construct($name = null, array $data = [], $dataName = '') + { + require "../../../lib/Autoloader.php"; + new \jblond\Autoloader(); + parent::__construct($name, $data, $dataName); + } - /** - * - */ - public function testRenderSimpleDelete() - { - $htmlRenderer = new HtmlArray(); - $htmlRenderer->diff = new \jblond\Diff( - array('a'), - array() - ); - $result = $htmlRenderer->render(); - static::assertEquals(array( - array( - array( - 'tag' => 'delete', - 'base' => array( - 'offset' => 0, - 'lines' => array( - 'a' - ) - ), - 'changed' => array( - 'offset' => 0, - 'lines' => array() - ) - ) - ) - ), $result); - } + /** + * + */ + public function testRenderSimpleDelete() + { + $htmlRenderer = new HtmlArray(); + $htmlRenderer->diff = new \jblond\Diff( + array('a'), + array() + ); + $result = $htmlRenderer->render(); + static::assertEquals(array( + array( + array( + 'tag' => 'delete', + 'base' => array( + 'offset' => 0, + 'lines' => array( + 'a' + ) + ), + 'changed' => array( + 'offset' => 0, + 'lines' => array() + ) + ) + ) + ), $result); + } - /** - * - */ - public function testRenderFixesSpaces() - { - $htmlRenderer = new HtmlArray(); - $htmlRenderer->diff = new \jblond\Diff( - array(' a'), - array('a') - ); - $result = $htmlRenderer->render(); - static::assertEquals(array( - array( - array( - 'tag' => 'replace', - 'base' => array( - 'offset' => 0, - 'lines' => array( - '   a', - ) - ), - 'changed' => array( - 'offset' => 0, - 'lines' => array( - 'a' - ) - ) - ) - ) - ), $result); - } + /** + * + */ + public function testRenderFixesSpaces() + { + $htmlRenderer = new HtmlArray(); + $htmlRenderer->diff = new \jblond\Diff( + array(' a'), + array('a') + ); + $result = $htmlRenderer->render(); + static::assertEquals(array( + array( + array( + 'tag' => 'replace', + 'base' => array( + 'offset' => 0, + 'lines' => array( + '   a', + ) + ), + 'changed' => array( + 'offset' => 0, + 'lines' => array( + 'a' + ) + ) + ) + ) + ), $result); + } }
'.$this->options['title_a'].''.$this->options['title_b'].'
  
  
'.$fromLine.''.$line.' '.$toLine.''.$line.' 
'.$fromLine.''.$line.' '.$toLine.''.$line.' 
  '.$toLine.''.$line.' 
  '.$toLine.''.$line.' 
'.$fromLine.''.$line.'   
'.$fromLine.''.$line.'   
'.$fromLine.''.$line.' '.$toLine.''.$changedLine.'
'.$fromLine.''.$line.' '.$toLine.''.$changedLine.'
'.$fromLine.''.$line.' '.$toLine.''.$changedLine.'
'.$fromLine.''.$line.' '.$toLine.''.$changedLine.'