├── Classes ├── Diff.php ├── Renderer │ ├── AbstractRenderer.php │ ├── Html │ │ ├── HtmlArrayRenderer.php │ │ ├── HtmlInlineRenderer.php │ │ └── HtmlSideBySideRenderer.php │ └── Text │ │ ├── TextContextRenderer.php │ │ └── TextUnifiedRenderer.php └── SequenceMatcher.php ├── LICENSE ├── README.md └── composer.json /Classes/Diff.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | /** 16 | * Class Diff 17 | */ 18 | class Diff 19 | { 20 | /** 21 | * @var array The "old" sequence to use as the basis for the comparison. 22 | */ 23 | private $a = null; 24 | 25 | /** 26 | * @var array The "new" sequence to generate the changes for. 27 | */ 28 | private $b = null; 29 | 30 | /** 31 | * @var array Array containing the generated opcodes for the differences between the two items. 32 | */ 33 | private $groupedCodes = null; 34 | 35 | /** 36 | * @var array Associative array of the default options available for the diff class and their default value. 37 | */ 38 | private $defaultOptions = [ 39 | 'context' => 3, 40 | 'ignoreNewLines' => false, 41 | 'ignoreWhitespace' => false, 42 | 'ignoreCase' => false 43 | ]; 44 | 45 | /** 46 | * @var array Array of the options that have been applied for generating the diff. 47 | */ 48 | private $options = []; 49 | 50 | /** 51 | * The constructor. 52 | * 53 | * @param array $a Array containing the lines of the first string to compare. 54 | * @param array $b Array containing the lines for the second string to compare. 55 | * @param array $options Options (see $defaultOptions in this class) 56 | */ 57 | public function __construct(array $a, array $b, array $options = []) 58 | { 59 | $this->a = $a; 60 | $this->b = $b; 61 | 62 | $this->options = array_merge($this->defaultOptions, $options); 63 | } 64 | 65 | /** 66 | * Render a diff using the supplied rendering class and return it. 67 | * 68 | * @param Renderer\AbstractRenderer $renderer An instance of the rendering object to use for generating the diff. 69 | * @return mixed The generated diff. Exact return value depends on the renderer used. 70 | */ 71 | public function render(Renderer\AbstractRenderer $renderer) 72 | { 73 | $renderer->diff = $this; 74 | return $renderer->render(); 75 | } 76 | 77 | /** 78 | * Get a range of lines from $start to $end from the first comparison string 79 | * and return them as an array. If no values are supplied, the entire string 80 | * is returned. It's also possible to specify just one line to return only 81 | * that line. 82 | * 83 | * @param int $start The starting number. 84 | * @param int $end The ending number. If not supplied, only the item in $start will be returned. 85 | * @return array Array of all of the lines between the specified range. 86 | */ 87 | public function getA($start = 0, $end = null) 88 | { 89 | if ($start == 0 && $end === null) { 90 | return $this->a; 91 | } 92 | 93 | if ($end === null) { 94 | $length = 1; 95 | } else { 96 | $length = $end - $start; 97 | } 98 | 99 | return array_slice($this->a, $start, $length); 100 | } 101 | 102 | /** 103 | * Get a range of lines from $start to $end from the second comparison string 104 | * and return them as an array. If no values are supplied, the entire string 105 | * is returned. It's also possible to specify just one line to return only 106 | * that line. 107 | * 108 | * @param int $start The starting number. 109 | * @param int $end The ending number. If not supplied, only the item in $start will be returned. 110 | * @return array Array of all of the lines between the specified range. 111 | */ 112 | public function getB($start = 0, $end = null) 113 | { 114 | if ($start == 0 && $end === null) { 115 | return $this->b; 116 | } 117 | 118 | if ($end === null) { 119 | $length = 1; 120 | } else { 121 | $length = $end - $start; 122 | } 123 | 124 | return array_slice($this->b, $start, $length); 125 | } 126 | 127 | /** 128 | * Generate a list of the compiled and grouped opcodes for the differences between the 129 | * two strings. Generally called by the renderer, this class instantiates the sequence 130 | * matcher and performs the actual diff generation and return an array of the opcodes 131 | * for it. Once generated, the results are cached in the diff class instance. 132 | * 133 | * @return array Array of the grouped opcodes for the generated diff. 134 | */ 135 | public function getGroupedOpcodes() 136 | { 137 | if (!is_null($this->groupedCodes)) { 138 | return $this->groupedCodes; 139 | } 140 | 141 | $sequenceMatcher = new SequenceMatcher($this->a, $this->b, null, $this->options); 142 | $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes(); 143 | return $this->groupedCodes; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Classes/Renderer/AbstractRenderer.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | /** 16 | * Abstract Diff Renderer 17 | */ 18 | abstract class AbstractRenderer 19 | { 20 | /** 21 | * @var object Instance of the diff class that this renderer is generating the rendered diff for. 22 | */ 23 | public $diff; 24 | 25 | /** 26 | * @var array Array of the default options that apply to this renderer. 27 | */ 28 | protected $defaultOptions = []; 29 | 30 | /** 31 | * @var array Array containing the user applied and merged default options for the renderer. 32 | */ 33 | protected $options = []; 34 | 35 | /** 36 | * The constructor. Instantiates the rendering engine and if options are passed, 37 | * sets the options for the renderer. 38 | * 39 | * @param array $options Optionally, an array of the options for the renderer. 40 | */ 41 | public function __construct(array $options = []) 42 | { 43 | $this->setOptions($options); 44 | } 45 | 46 | /** 47 | * Set the options of the renderer to those supplied in the passed in array. 48 | * Options are merged with the default to ensure that there aren't any missing 49 | * options. 50 | * 51 | * @param array $options Array of options to set. 52 | */ 53 | public function setOptions(array $options) 54 | { 55 | $this->options = array_merge($this->defaultOptions, $options); 56 | } 57 | 58 | /** 59 | * Render the diff. 60 | * 61 | * @return string The diff 62 | */ 63 | abstract public function render(); 64 | } 65 | -------------------------------------------------------------------------------- /Classes/Renderer/Html/HtmlArrayRenderer.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | use Neos\Diff\Renderer\AbstractRenderer; 16 | 17 | /** 18 | * Array renderer for HTML based diffs 19 | */ 20 | class HtmlArrayRenderer extends AbstractRenderer 21 | { 22 | /** 23 | * @var array Array of the default options that apply to this renderer. 24 | */ 25 | protected $defaultOptions = [ 26 | 'tabSize' => 4 27 | ]; 28 | 29 | /** 30 | * Render and return an array structure suitable for generating HTML 31 | * based differences. Generally called by subclasses that generate a 32 | * HTML based diff and return an array of the changes to show in the diff. 33 | * 34 | * @return array An array of the generated chances, suitable for presentation in HTML. 35 | */ 36 | public function render() 37 | { 38 | // As we'll be modifying a & b to include our change markers, 39 | // we need to get the contents and store them here. That way 40 | // we're not going to destroy the original data 41 | $a = $this->diff->getA(); 42 | $b = $this->diff->getB(); 43 | 44 | $changes = []; 45 | $opCodes = $this->diff->getGroupedOpcodes(); 46 | foreach ($opCodes as $group) { 47 | $blocks = []; 48 | $lastTag = null; 49 | $lastBlock = 0; 50 | foreach ($group as $code) { 51 | list($tag, $i1, $i2, $j1, $j2) = $code; 52 | 53 | if ($tag == 'replace' && $i2 - $i1 == $j2 - $j1) { 54 | for ($i = 0; $i < ($i2 - $i1); ++$i) { 55 | $fromLine = $a[$i1 + $i]; 56 | $toLine = $b[$j1 + $i]; 57 | 58 | list($start, $end) = $this->getChangeExtent($fromLine, $toLine); 59 | if ($start != 0 || $end != 0) { 60 | $last = $end + strlen($fromLine); 61 | $fromLine = substr_replace($fromLine, "\0", $start, 0); 62 | $fromLine = substr_replace($fromLine, "\1", $last + 1, 0); 63 | $last = $end + strlen($toLine); 64 | $toLine = substr_replace($toLine, "\0", $start, 0); 65 | $toLine = substr_replace($toLine, "\1", $last + 1, 0); 66 | $a[$i1 + $i] = $fromLine; 67 | $b[$j1 + $i] = $toLine; 68 | } 69 | } 70 | } 71 | 72 | if ($tag != $lastTag) { 73 | $blocks[] = [ 74 | 'tag' => $tag, 75 | 'base' => [ 76 | 'offset' => $i1, 77 | 'lines' => [] 78 | ], 79 | 'changed' => [ 80 | 'offset' => $j1, 81 | 'lines' => [] 82 | ] 83 | ]; 84 | $lastBlock = count($blocks) - 1; 85 | } 86 | 87 | $lastTag = $tag; 88 | 89 | if ($tag == 'equal') { 90 | $lines = array_slice($a, $i1, ($i2 - $i1)); 91 | $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); 92 | $lines = array_slice($b, $j1, ($j2 - $j1)); 93 | $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); 94 | } else { 95 | if ($tag == 'replace' || $tag == 'delete') { 96 | $lines = array_slice($a, $i1, ($i2 - $i1)); 97 | $lines = $this->formatLines($lines); 98 | $lines = str_replace(["\0", "\1"], ['', ''], $lines); 99 | $blocks[$lastBlock]['base']['lines'] += $lines; 100 | } 101 | 102 | if ($tag == 'replace' || $tag == 'insert') { 103 | $lines = array_slice($b, $j1, ($j2 - $j1)); 104 | $lines = $this->formatLines($lines); 105 | $lines = str_replace(["\0", "\1"], ['', ''], $lines); 106 | $blocks[$lastBlock]['changed']['lines'] += $lines; 107 | } 108 | } 109 | } 110 | $changes[] = $blocks; 111 | } 112 | return $changes; 113 | } 114 | 115 | /** 116 | * Given two strings, determine where the changes in the two strings 117 | * begin, and where the changes in the two strings end. 118 | * 119 | * @param string $fromLine The first string. 120 | * @param string $toLine The second string. 121 | * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) 122 | */ 123 | private function getChangeExtent($fromLine, $toLine) 124 | { 125 | $start = 0; 126 | $limit = min(strlen($fromLine), strlen($toLine)); 127 | while ($start < $limit && $fromLine[$start] == $toLine[$start]) { 128 | ++$start; 129 | } 130 | $end = -1; 131 | $limit = $limit - $start; 132 | while (-$end <= $limit && substr($fromLine, $end, 1) == substr($toLine, $end, 1)) { 133 | --$end; 134 | } 135 | return [ 136 | $start, 137 | $end + 1 138 | ]; 139 | } 140 | 141 | /** 142 | * Format a series of lines suitable for output in a HTML rendered diff. 143 | * This involves replacing tab characters with spaces, making the HTML safe 144 | * for output, ensuring that double spaces are replaced with   etc. 145 | * 146 | * @param array $lines Array of lines to format. 147 | * @return array Array of the formatted lines. 148 | */ 149 | private function formatLines(array $lines) 150 | { 151 | $lines = array_map([$this, 'ExpandTabs'], $lines); 152 | $lines = array_map([$this, 'HtmlSafe'], $lines); 153 | foreach ($lines as &$line) { 154 | $line = preg_replace_callback('# ( +)|^ #', function (array $matches) { 155 | return (isset($matches[1]) ? $matches[1] : ''); 156 | }, $line); 157 | } 158 | return $lines; 159 | } 160 | 161 | /** 162 | * Replace a string containing spaces with a HTML representation using  . 163 | * 164 | * @param string $spaces The string of spaces. 165 | * @return string The HTML representation of the string. 166 | */ 167 | public function fixSpaces($spaces = '') 168 | { 169 | $count = strlen($spaces); 170 | if ($count == 0) { 171 | return ''; 172 | } 173 | 174 | $div = floor($count / 2); 175 | $mod = $count % 2; 176 | return str_repeat('  ', $div) . str_repeat(' ', $mod); 177 | } 178 | 179 | /** 180 | * Replace tabs in a single line with a number of spaces as defined by the tabSize option. 181 | * 182 | * @param string $line The containing tabs to convert. 183 | * @return string The line with the tabs converted to spaces. 184 | */ 185 | private function expandTabs($line) 186 | { 187 | return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line); 188 | } 189 | 190 | /** 191 | * Make a string containing HTML safe for output on a page. 192 | * 193 | * @param string $string The string. 194 | * @return string The string with the HTML characters replaced by entities. 195 | */ 196 | private function htmlSafe($string) 197 | { 198 | return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Classes/Renderer/Html/HtmlInlineRenderer.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | /** 16 | * Inline HTML Diff Renderer 17 | */ 18 | class HtmlInlineRenderer extends HtmlArrayRenderer 19 | { 20 | /** 21 | * Render a and return diff with changes between the two sequences 22 | * displayed inline (under each other) 23 | * 24 | * @return string The generated inline diff. 25 | */ 26 | public function render() 27 | { 28 | $changes = parent::render(); 29 | $html = ''; 30 | if (empty($changes)) { 31 | return $html; 32 | } 33 | 34 | $html .= ''; 35 | $html .= ''; 36 | $html .= ''; 37 | $html .= ''; 38 | $html .= ''; 39 | $html .= ''; 40 | $html .= ''; 41 | $html .= ''; 42 | foreach ($changes as $i => $blocks) { 43 | // If this is a separate block, we're condensing code so output ..., 44 | // indicating a significant portion of the code has been collapsed as 45 | // it is the same 46 | if ($i > 0) { 47 | $html .= ''; 48 | $html .= ''; 49 | $html .= ''; 50 | $html .= ''; 51 | $html .= ''; 52 | } 53 | 54 | foreach ($blocks as $change) { 55 | $html .= ''; 56 | // Equal changes should be shown on both sides of the diff 57 | if ($change['tag'] == 'equal') { 58 | foreach ($change['base']['lines'] as $no => $line) { 59 | $fromLine = $change['base']['offset'] + $no + 1; 60 | $toLine = $change['changed']['offset'] + $no + 1; 61 | $html .= ''; 62 | $html .= ''; 63 | $html .= ''; 64 | $html .= ''; 65 | $html .= ''; 66 | } 67 | } // Added lines only on the right side 68 | else { 69 | if ($change['tag'] == 'insert') { 70 | foreach ($change['changed']['lines'] as $no => $line) { 71 | $toLine = $change['changed']['offset'] + $no + 1; 72 | $html .= ''; 73 | $html .= ''; 74 | $html .= ''; 75 | $html .= ''; 76 | $html .= ''; 77 | } 78 | } // Show deleted lines only on the left side 79 | else { 80 | if ($change['tag'] == 'delete') { 81 | foreach ($change['base']['lines'] as $no => $line) { 82 | $fromLine = $change['base']['offset'] + $no + 1; 83 | $html .= ''; 84 | $html .= ''; 85 | $html .= ''; 86 | $html .= ''; 87 | $html .= ''; 88 | } 89 | } // Show modified lines on both sides 90 | else { 91 | if ($change['tag'] == 'replace') { 92 | foreach ($change['base']['lines'] as $no => $line) { 93 | $fromLine = $change['base']['offset'] + $no + 1; 94 | $html .= ''; 95 | $html .= ''; 96 | $html .= ''; 97 | $html .= ''; 98 | $html .= ''; 99 | } 100 | 101 | foreach ($change['changed']['lines'] as $no => $line) { 102 | $toLine = $change['changed']['offset'] + $no + 1; 103 | $html .= ''; 104 | $html .= ''; 105 | $html .= ''; 106 | $html .= ''; 107 | $html .= ''; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | $html .= ''; 114 | } 115 | } 116 | $html .= '
OldNewDifferences
 
' . $fromLine . '' . $toLine . '' . $line . '
 ' . $toLine . '' . $line . ' 
' . $fromLine . ' ' . $line . ' 
' . $fromLine . ' ' . $line . '
' . $toLine . ' ' . $line . '
'; 117 | return $html; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Classes/Renderer/Html/HtmlSideBySideRenderer.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | /** 16 | * Inline HTML Diff Renderer 17 | */ 18 | class HtmlSideBySideRenderer extends HtmlArrayRenderer 19 | { 20 | /** 21 | * Render a and return diff with changes between the two sequences 22 | * displayed side by side. 23 | * 24 | * @return string The generated side by side diff. 25 | */ 26 | public function render() 27 | { 28 | $changes = parent::render(); 29 | 30 | $html = ''; 31 | if (empty($changes)) { 32 | return $html; 33 | } 34 | 35 | $html .= ''; 36 | $html .= ''; 37 | $html .= ''; 38 | $html .= ''; 39 | $html .= ''; 40 | $html .= ''; 41 | $html .= ''; 42 | foreach ($changes as $i => $blocks) { 43 | if ($i > 0) { 44 | $html .= ''; 45 | $html .= ''; 46 | $html .= ''; 47 | $html .= ''; 48 | } 49 | 50 | foreach ($blocks as $change) { 51 | $html .= ''; 52 | // Equal changes should be shown on both sides of the diff 53 | if ($change['tag'] == 'equal') { 54 | foreach ($change['base']['lines'] as $no => $line) { 55 | $fromLine = $change['base']['offset'] + $no + 1; 56 | $toLine = $change['changed']['offset'] + $no + 1; 57 | $html .= ''; 58 | $html .= ''; 59 | $html .= ''; 60 | $html .= ''; 61 | $html .= ''; 62 | $html .= ''; 63 | } 64 | } // Added lines only on the right side 65 | else { 66 | if ($change['tag'] == 'insert') { 67 | foreach ($change['changed']['lines'] as $no => $line) { 68 | $toLine = $change['changed']['offset'] + $no + 1; 69 | $html .= ''; 70 | $html .= ''; 71 | $html .= ''; 72 | $html .= ''; 73 | $html .= ''; 74 | $html .= ''; 75 | } 76 | } // Show deleted lines only on the left side 77 | else { 78 | if ($change['tag'] == 'delete') { 79 | foreach ($change['base']['lines'] as $no => $line) { 80 | $fromLine = $change['base']['offset'] + $no + 1; 81 | $html .= ''; 82 | $html .= ''; 83 | $html .= ''; 84 | $html .= ''; 85 | $html .= ''; 86 | $html .= ''; 87 | } 88 | } // Show modified lines on both sides 89 | else { 90 | if ($change['tag'] == 'replace') { 91 | if (count($change['base']['lines']) >= count($change['changed']['lines'])) { 92 | foreach ($change['base']['lines'] as $no => $line) { 93 | $fromLine = $change['base']['offset'] + $no + 1; 94 | $html .= ''; 95 | $html .= ''; 96 | $html .= ''; 97 | if (!isset($change['changed']['lines'][$no])) { 98 | $toLine = ' '; 99 | $changedLine = ' '; 100 | } else { 101 | $toLine = $change['base']['offset'] + $no + 1; 102 | $changedLine = '' . $change['changed']['lines'][$no] . ''; 103 | } 104 | $html .= ''; 105 | $html .= ''; 106 | $html .= ''; 107 | } 108 | } else { 109 | foreach ($change['changed']['lines'] as $no => $changedLine) { 110 | if (!isset($change['base']['lines'][$no])) { 111 | $fromLine = ' '; 112 | $line = ' '; 113 | } else { 114 | $fromLine = $change['base']['offset'] + $no + 1; 115 | $line = '' . $change['base']['lines'][$no] . ''; 116 | } 117 | $html .= ''; 118 | $html .= ''; 119 | $html .= ''; 120 | $toLine = $change['changed']['offset'] + $no + 1; 121 | $html .= ''; 122 | $html .= ''; 123 | $html .= ''; 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | $html .= ''; 131 | } 132 | } 133 | $html .= '
Old VersionNew Version
  
' . $fromLine . '' . $line . ' ' . $toLine . '' . $line . ' 
  ' . $toLine . '' . $line . ' 
' . $fromLine . '' . $line . '   
' . $fromLine . '' . $line . ' ' . $toLine . '' . $changedLine . '
' . $fromLine . '' . $line . ' ' . $toLine . '' . $changedLine . '
'; 134 | return $html; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Classes/Renderer/Text/TextContextRenderer.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | use Neos\Diff\Renderer\AbstractRenderer; 16 | 17 | /** 18 | * Text Context Diff Renderer 19 | */ 20 | class TextContextRenderer extends AbstractRenderer 21 | { 22 | /** 23 | * @var array Array of the different opcode tags and how they map to the context diff equivalent. 24 | */ 25 | private $tagMap = [ 26 | 'insert' => '+', 27 | 'delete' => '-', 28 | 'replace' => '!', 29 | 'equal' => ' ' 30 | ]; 31 | 32 | /** 33 | * Render and return a context formatted (old school!) diff file. 34 | * 35 | * @return string The generated context diff. 36 | */ 37 | public function render() 38 | { 39 | $diff = ''; 40 | $opCodes = $this->diff->getGroupedOpcodes(); 41 | foreach ($opCodes as $group) { 42 | $diff .= "***************\n"; 43 | $lastItem = count($group) - 1; 44 | $i1 = $group[0][1]; 45 | $i2 = $group[$lastItem][2]; 46 | $j1 = $group[0][3]; 47 | $j2 = $group[$lastItem][4]; 48 | 49 | if ($i2 - $i1 >= 2) { 50 | $diff .= '*** ' . ($group[0][1] + 1) . ',' . $i2 . " ****\n"; 51 | } else { 52 | $diff .= '*** ' . $i2 . " ****\n"; 53 | } 54 | 55 | if ($j2 - $j1 >= 2) { 56 | $separator = '--- ' . ($j1 + 1) . ',' . $j2 . " ----\n"; 57 | } else { 58 | $separator = '--- ' . $j2 . " ----\n"; 59 | } 60 | 61 | $hasVisible = false; 62 | foreach ($group as $code) { 63 | if ($code[0] == 'replace' || $code[0] == 'delete') { 64 | $hasVisible = true; 65 | break; 66 | } 67 | } 68 | 69 | if ($hasVisible) { 70 | foreach ($group as $code) { 71 | list($tag, $i1, $i2, $j1, $j2) = $code; 72 | if ($tag == 'insert') { 73 | continue; 74 | } 75 | $diff .= $this->tagMap[$tag] . ' ' . implode( 76 | "\n" . $this->tagMap[$tag] . ' ', 77 | $this->diff->GetA($i1, $i2) 78 | ) . "\n"; 79 | } 80 | } 81 | 82 | $hasVisible = false; 83 | foreach ($group as $code) { 84 | if ($code[0] == 'replace' || $code[0] == 'insert') { 85 | $hasVisible = true; 86 | break; 87 | } 88 | } 89 | 90 | $diff .= $separator; 91 | 92 | if ($hasVisible) { 93 | foreach ($group as $code) { 94 | list($tag, $i1, $i2, $j1, $j2) = $code; 95 | if ($tag == 'delete') { 96 | continue; 97 | } 98 | $diff .= $this->tagMap[$tag] . ' ' . implode( 99 | "\n" . $this->tagMap[$tag] . ' ', 100 | $this->diff->GetB($j1, $j2) 101 | ) . "\n"; 102 | } 103 | } 104 | } 105 | return $diff; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Classes/Renderer/Text/TextUnifiedRenderer.php: -------------------------------------------------------------------------------- 1 | 8 | * Portions (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | use Neos\Diff\Renderer\AbstractRenderer; 16 | 17 | /** 18 | * Unified Diff Renderer 19 | */ 20 | class TextUnifiedRenderer extends AbstractRenderer 21 | { 22 | /** 23 | * Render and return a unified diff. 24 | * 25 | * @return string The unified diff. 26 | */ 27 | public function render() 28 | { 29 | $diff = ''; 30 | $opCodes = $this->diff->getGroupedOpcodes(); 31 | foreach ($opCodes as $group) { 32 | $lastItem = count($group) - 1; 33 | $i1 = $group[0][1]; 34 | $i2 = $group[$lastItem][2]; 35 | $j1 = $group[0][3]; 36 | $j2 = $group[$lastItem][4]; 37 | 38 | if ($i1 == 0 && $i2 == 0) { 39 | $i1 = -1; 40 | $i2 = -1; 41 | } 42 | 43 | $diff .= '@@ -' . ($i1 + 1) . ',' . ($i2 - $i1) . ' +' . ($j1 + 1) . ',' . ($j2 - $j1) . " @@\n"; 44 | foreach ($group as $code) { 45 | list($tag, $i1, $i2, $j1, $j2) = $code; 46 | if ($tag == 'equal') { 47 | $diff .= ' ' . implode("\n ", $this->diff->GetA($i1, $i2)) . "\n"; 48 | } else { 49 | if ($tag == 'replace' || $tag == 'delete') { 50 | $diff .= '-' . implode("\n-", $this->diff->GetA($i1, $i2)) . "\n"; 51 | } 52 | 53 | if ($tag == 'replace' || $tag == 'insert') { 54 | $diff .= '+' . implode("\n+", $this->diff->GetB($j1, $j2)) . "\n"; 55 | } 56 | } 57 | } 58 | } 59 | return $diff; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/SequenceMatcher.php: -------------------------------------------------------------------------------- 1 | 8 | * (c) Contributors of the Neos Project - www.neos.io 9 | * 10 | * This package is Open Source Software. For the full copyright and license 11 | * information, please view the LICENSE file which was distributed with this 12 | * source code. 13 | */ 14 | 15 | /** 16 | * A Diff Sequence Matcher 17 | */ 18 | class SequenceMatcher 19 | { 20 | /** 21 | * @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not. 22 | */ 23 | private $junkCallback = null; 24 | 25 | /** 26 | * @var array The first sequence to compare against. 27 | */ 28 | private $a = null; 29 | 30 | /** 31 | * @var array The second sequence. 32 | */ 33 | private $b = null; 34 | 35 | /** 36 | * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. 37 | */ 38 | private $junkDict = []; 39 | 40 | /** 41 | * @var array Array of indices that do not contain junk elements. 42 | */ 43 | private $b2j = []; 44 | 45 | /** 46 | * @var array 47 | */ 48 | private $options = []; 49 | 50 | /** 51 | * @var array 52 | */ 53 | private $defaultOptions = [ 54 | 'ignoreNewLines' => false, 55 | 'ignoreWhitespace' => false, 56 | 'ignoreCase' => false 57 | ]; 58 | 59 | /** 60 | * @var array 61 | */ 62 | private $matchingBlocks; 63 | 64 | /** 65 | * @var array 66 | */ 67 | private $opCodes; 68 | 69 | /** 70 | * @var array 71 | */ 72 | private $fullBCount; 73 | 74 | /** 75 | * The constructor. With the sequences being passed, they'll be set for the 76 | * sequence matcher and it will perform a basic cleanup & calculate junk 77 | * elements. 78 | * 79 | * @param string|array $a A string or array containing the lines to compare against. 80 | * @param string|array $b A string or array containing the lines to compare. 81 | * @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters. 82 | * @param array $options An array of options for the matcher. 83 | */ 84 | public function __construct($a, $b, $junkCallback = null, array $options = []) 85 | { 86 | $this->a = null; 87 | $this->b = null; 88 | $this->junkCallback = $junkCallback; 89 | $this->setOptions($options); 90 | $this->setSequences($a, $b); 91 | } 92 | 93 | /** 94 | * Set options for the matcher. 95 | * 96 | * @param array $options 97 | * @return void 98 | */ 99 | public function setOptions(array $options) 100 | { 101 | $this->options = array_merge($this->defaultOptions, $options); 102 | } 103 | 104 | /** 105 | * Set the first and second sequences to use with the sequence matcher. 106 | * 107 | * @param string|array $a A string or array containing the lines to compare against. 108 | * @param string|array $b A string or array containing the lines to compare. 109 | * @return void 110 | */ 111 | public function setSequences($a, $b) 112 | { 113 | $this->setSeq1($a); 114 | $this->setSeq2($b); 115 | } 116 | 117 | /** 118 | * Set the first sequence ($a) and reset any internal caches to indicate that 119 | * when calling the calculation methods, we need to recalculate them. 120 | * 121 | * @param string|array $a The sequence to set as the first sequence. 122 | * @return void 123 | */ 124 | public function setSeq1($a) 125 | { 126 | if (!is_array($a)) { 127 | $a = str_split($a); 128 | } 129 | if ($a == $this->a) { 130 | return; 131 | } 132 | 133 | $this->a = $a; 134 | $this->matchingBlocks = null; 135 | $this->opCodes = null; 136 | } 137 | 138 | /** 139 | * Set the second sequence ($b) and reset any internal caches to indicate that 140 | * when calling the calculation methods, we need to recalculate them. 141 | * 142 | * @param string|array $b The sequence to set as the second sequence. 143 | * @return void 144 | */ 145 | public function setSeq2($b) 146 | { 147 | if (!is_array($b)) { 148 | $b = str_split($b); 149 | } 150 | if ($b == $this->b) { 151 | return; 152 | } 153 | 154 | $this->b = $b; 155 | $this->matchingBlocks = null; 156 | $this->opCodes = null; 157 | $this->fullBCount = null; 158 | $this->chainB(); 159 | } 160 | 161 | /** 162 | * Generate the internal arrays containing the list of junk and non-junk 163 | * characters for the second ($b) sequence. 164 | * 165 | * @return void 166 | */ 167 | private function chainB() 168 | { 169 | $length = count($this->b); 170 | $this->b2j = []; 171 | $popularDict = []; 172 | 173 | for ($i = 0; $i < $length; ++$i) { 174 | $char = $this->b[$i]; 175 | if (isset($this->b2j[$char])) { 176 | if ($length >= 200 && count($this->b2j[$char]) * 100 > $length) { 177 | $popularDict[$char] = 1; 178 | unset($this->b2j[$char]); 179 | } else { 180 | $this->b2j[$char][] = $i; 181 | } 182 | } else { 183 | $this->b2j[$char] = [ 184 | $i 185 | ]; 186 | } 187 | } 188 | 189 | // Remove leftovers 190 | foreach (array_keys($popularDict) as $char) { 191 | unset($this->b2j[$char]); 192 | } 193 | 194 | $this->junkDict = []; 195 | if (is_callable($this->junkCallback)) { 196 | foreach (array_keys($popularDict) as $char) { 197 | if (call_user_func($this->junkCallback, $char)) { 198 | $this->junkDict[$char] = 1; 199 | unset($popularDict[$char]); 200 | } 201 | } 202 | 203 | foreach (array_keys($this->b2j) as $char) { 204 | if (call_user_func($this->junkCallback, $char)) { 205 | $this->junkDict[$char] = 1; 206 | unset($this->b2j[$char]); 207 | } 208 | } 209 | } 210 | } 211 | 212 | /** 213 | * Checks if a particular character is in the junk dictionary 214 | * for the list of junk characters. 215 | * 216 | * @param string $b 217 | * @return boolean $b True if the character is considered junk. False if not. 218 | */ 219 | private function isBJunk($b) 220 | { 221 | if (isset($this->junkDict[$b])) { 222 | return true; 223 | } 224 | 225 | return false; 226 | } 227 | 228 | /** 229 | * Find the longest matching block in the two sequences, as defined by the 230 | * lower and upper constraints for each sequence. (for the first sequence, 231 | * $alo - $ahi and for the second sequence, $blo - $bhi) 232 | * 233 | * Essentially, of all of the maximal matching blocks, return the one that 234 | * starts earliest in $a, and all of those maximal matching blocks that 235 | * start earliest in $a, return the one that starts earliest in $b. 236 | * 237 | * If the junk callback is defined, do the above but with the restriction 238 | * that the junk element appears in the block. Extend it as far as possible 239 | * by matching only junk elements in both $a and $b. 240 | * 241 | * @param int $alo The lower constraint for the first sequence. 242 | * @param int $ahi The upper constraint for the first sequence. 243 | * @param int $blo The lower constraint for the second sequence. 244 | * @param int $bhi The upper constraint for the second sequence. 245 | * @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size. 246 | */ 247 | public function findLongestMatch($alo, $ahi, $blo, $bhi) 248 | { 249 | $a = $this->a; 250 | $b = $this->b; 251 | 252 | $bestI = $alo; 253 | $bestJ = $blo; 254 | $bestSize = 0; 255 | 256 | $j2Len = []; 257 | $nothing = []; 258 | 259 | for ($i = $alo; $i < $ahi; ++$i) { 260 | $newJ2Len = []; 261 | $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing); 262 | foreach ($jDict as $jKey => $j) { 263 | if ($j < $blo) { 264 | continue; 265 | } else { 266 | if ($j >= $bhi) { 267 | break; 268 | } 269 | } 270 | 271 | $k = $this->arrayGetDefault($j2Len, $j - 1, 0) + 1; 272 | $newJ2Len[$j] = $k; 273 | if ($k > $bestSize) { 274 | $bestI = $i - $k + 1; 275 | $bestJ = $j - $k + 1; 276 | $bestSize = $k; 277 | } 278 | } 279 | 280 | $j2Len = $newJ2Len; 281 | } 282 | 283 | while ($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) && 284 | !$this->linesAreDifferent($bestI - 1, $bestJ - 1)) { 285 | --$bestI; 286 | --$bestJ; 287 | ++$bestSize; 288 | } 289 | 290 | while ($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi && 291 | !$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent( 292 | $bestI + $bestSize, 293 | $bestJ + $bestSize 294 | )) { 295 | ++$bestSize; 296 | } 297 | 298 | while ($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) && 299 | !$this->linesAreDifferent($bestI - 1, $bestJ - 1)) { 300 | --$bestI; 301 | --$bestJ; 302 | ++$bestSize; 303 | } 304 | 305 | while ($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi && 306 | $this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent( 307 | $bestI + $bestSize, 308 | $bestJ + $bestSize 309 | )) { 310 | ++$bestSize; 311 | } 312 | 313 | return [ 314 | $bestI, 315 | $bestJ, 316 | $bestSize 317 | ]; 318 | } 319 | 320 | /** 321 | * Check if the two lines at the given indexes are different or not. 322 | * 323 | * @param int $aIndex Line number to check against in a. 324 | * @param int $bIndex Line number to check against in b. 325 | * @return boolean True if the lines are different and false if not. 326 | */ 327 | public function linesAreDifferent($aIndex, $bIndex) 328 | { 329 | $lineA = $this->a[$aIndex]; 330 | $lineB = $this->b[$bIndex]; 331 | 332 | if ($this->options['ignoreWhitespace']) { 333 | $replace = ["\t", ' ']; 334 | $lineA = str_replace($replace, '', $lineA); 335 | $lineB = str_replace($replace, '', $lineB); 336 | } 337 | 338 | if ($this->options['ignoreCase']) { 339 | $lineA = strtolower($lineA); 340 | $lineB = strtolower($lineB); 341 | } 342 | 343 | if ($lineA != $lineB) { 344 | return true; 345 | } 346 | 347 | return false; 348 | } 349 | 350 | /** 351 | * Return a nested set of arrays for all of the matching sub-sequences 352 | * in the strings $a and $b. 353 | * 354 | * Each block contains the lower constraint of the block in $a, the lower 355 | * constraint of the block in $b and finally the number of lines that the 356 | * block continues for. 357 | * 358 | * @return array Nested array of the matching blocks, as described by the function. 359 | */ 360 | public function getMatchingBlocks() 361 | { 362 | if (!empty($this->matchingBlocks)) { 363 | return $this->matchingBlocks; 364 | } 365 | 366 | $aLength = $this->a === null ? 0 : count($this->a); 367 | $bLength = $this->b === null ? 0 : count($this->b); 368 | 369 | $queue = [ 370 | [ 371 | 0, 372 | $aLength, 373 | 0, 374 | $bLength 375 | ] 376 | ]; 377 | 378 | $matchingBlocks = []; 379 | while (!empty($queue)) { 380 | list($alo, $ahi, $blo, $bhi) = array_pop($queue); 381 | $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi); 382 | list($i, $j, $k) = $x; 383 | if ($k) { 384 | $matchingBlocks[] = $x; 385 | if ($alo < $i && $blo < $j) { 386 | $queue[] = [ 387 | $alo, 388 | $i, 389 | $blo, 390 | $j 391 | ]; 392 | } 393 | 394 | if ($i + $k < $ahi && $j + $k < $bhi) { 395 | $queue[] = [ 396 | $i + $k, 397 | $ahi, 398 | $j + $k, 399 | $bhi 400 | ]; 401 | } 402 | } 403 | } 404 | 405 | usort($matchingBlocks, [$this, 'tupleSort']); 406 | 407 | $i1 = 0; 408 | $j1 = 0; 409 | $k1 = 0; 410 | $nonAdjacent = []; 411 | foreach ($matchingBlocks as $block) { 412 | list($i2, $j2, $k2) = $block; 413 | if ($i1 + $k1 == $i2 && $j1 + $k1 == $j2) { 414 | $k1 += $k2; 415 | } else { 416 | if ($k1) { 417 | $nonAdjacent[] = [ 418 | $i1, 419 | $j1, 420 | $k1 421 | ]; 422 | } 423 | 424 | $i1 = $i2; 425 | $j1 = $j2; 426 | $k1 = $k2; 427 | } 428 | } 429 | 430 | if ($k1) { 431 | $nonAdjacent[] = [ 432 | $i1, 433 | $j1, 434 | $k1 435 | ]; 436 | } 437 | 438 | $nonAdjacent[] = [ 439 | $aLength, 440 | $bLength, 441 | 0 442 | ]; 443 | 444 | $this->matchingBlocks = $nonAdjacent; 445 | return $this->matchingBlocks; 446 | } 447 | 448 | /** 449 | * Return a list of all of the opcodes for the differences between the 450 | * two strings. 451 | * 452 | * The nested array returned contains an array describing the opcode 453 | * which includes: 454 | * 0 - The type of tag (as described below) for the opcode. 455 | * 1 - The beginning line in the first sequence. 456 | * 2 - The end line in the first sequence. 457 | * 3 - The beginning line in the second sequence. 458 | * 4 - The end line in the second sequence. 459 | * 460 | * The different types of tags include: 461 | * replace - The string from $i1 to $i2 in $a should be replaced by 462 | * the string in $b from $j1 to $j2. 463 | * delete - The string in $a from $i1 to $j2 should be deleted. 464 | * insert - The string in $b from $j1 to $j2 should be inserted at 465 | * $i1 in $a. 466 | * equal - The two strings with the specified ranges are equal. 467 | * 468 | * @return array Array of the opcodes describing the differences between the strings. 469 | */ 470 | public function getOpCodes() 471 | { 472 | if (!empty($this->opCodes)) { 473 | return $this->opCodes; 474 | } 475 | 476 | $i = 0; 477 | $j = 0; 478 | $this->opCodes = []; 479 | 480 | $blocks = $this->getMatchingBlocks(); 481 | foreach ($blocks as $block) { 482 | list($ai, $bj, $size) = $block; 483 | $tag = ''; 484 | if ($i < $ai && $j < $bj) { 485 | $tag = 'replace'; 486 | } else { 487 | if ($i < $ai) { 488 | $tag = 'delete'; 489 | } else { 490 | if ($j < $bj) { 491 | $tag = 'insert'; 492 | } 493 | } 494 | } 495 | 496 | if ($tag) { 497 | $this->opCodes[] = [ 498 | $tag, 499 | $i, 500 | $ai, 501 | $j, 502 | $bj 503 | ]; 504 | } 505 | 506 | $i = $ai + $size; 507 | $j = $bj + $size; 508 | 509 | if ($size) { 510 | $this->opCodes[] = [ 511 | 'equal', 512 | $ai, 513 | $i, 514 | $bj, 515 | $j 516 | ]; 517 | } 518 | } 519 | return $this->opCodes; 520 | } 521 | 522 | /** 523 | * Return a series of nested arrays containing different groups of generated 524 | * opcodes for the differences between the strings with up to $context lines 525 | * of surrounding content. 526 | * 527 | * Essentially what happens here is any big equal blocks of strings are stripped 528 | * out, the smaller subsets of changes are then arranged in to their groups. 529 | * This means that the sequence matcher and diffs do not need to include the full 530 | * content of the different files but can still provide context as to where the 531 | * changes are. 532 | * 533 | * @param int $context The number of lines of context to provide around the groups. 534 | * @return array Nested array of all of the grouped opcodes. 535 | */ 536 | public function getGroupedOpcodes($context = 3) 537 | { 538 | $opCodes = $this->getOpCodes(); 539 | if (empty($opCodes)) { 540 | $opCodes = [ 541 | [ 542 | 'equal', 543 | 0, 544 | 1, 545 | 0, 546 | 1 547 | ] 548 | ]; 549 | } 550 | 551 | if ($opCodes[0][0] == 'equal') { 552 | $opCodes[0] = [ 553 | $opCodes[0][0], 554 | max($opCodes[0][1], $opCodes[0][2] - $context), 555 | $opCodes[0][2], 556 | max($opCodes[0][3], $opCodes[0][4] - $context), 557 | $opCodes[0][4] 558 | ]; 559 | } 560 | 561 | $lastItem = count($opCodes) - 1; 562 | if ($opCodes[$lastItem][0] == 'equal') { 563 | list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem]; 564 | $opCodes[$lastItem] = [ 565 | $tag, 566 | $i1, 567 | min($i2, $i1 + $context), 568 | $j1, 569 | min($j2, $j1 + $context) 570 | ]; 571 | } 572 | 573 | $maxRange = $context * 2; 574 | $groups = []; 575 | $group = []; 576 | foreach ($opCodes as $code) { 577 | list($tag, $i1, $i2, $j1, $j2) = $code; 578 | if ($tag == 'equal' && $i2 - $i1 > $maxRange) { 579 | $group[] = [ 580 | $tag, 581 | $i1, 582 | min($i2, $i1 + $context), 583 | $j1, 584 | min($j2, $j1 + $context) 585 | ]; 586 | $groups[] = $group; 587 | $group = []; 588 | $i1 = max($i1, $i2 - $context); 589 | $j1 = max($j1, $j2 - $context); 590 | } 591 | $group[] = [ 592 | $tag, 593 | $i1, 594 | $i2, 595 | $j1, 596 | $j2 597 | ]; 598 | } 599 | 600 | if (!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) { 601 | $groups[] = $group; 602 | } 603 | 604 | return $groups; 605 | } 606 | 607 | /** 608 | * Return a measure of the similarity between the two sequences. 609 | * This will be a float value between 0 and 1. 610 | * 611 | * Out of all of the ratio calculation functions, this is the most 612 | * expensive to call if getMatchingBlocks or getOpCodes is yet to be 613 | * called. The other calculation methods (quickRatio and realquickRatio) 614 | * can be used to perform quicker calculations but may be less accurate. 615 | * 616 | * The ratio is calculated as (2 * number of matches) / total number of 617 | * elements in both sequences. 618 | * 619 | * @return float The calculated ratio. 620 | */ 621 | public function ratio() 622 | { 623 | $matches = array_reduce($this->getMatchingBlocks(), [$this, 'ratioReduce'], 0); 624 | return $this->calculateRatio($matches, count($this->a) + count($this->b)); 625 | } 626 | 627 | /** 628 | * Helper function to calculate the number of matches for Ratio(). 629 | * 630 | * @param int $sum The running total for the number of matches. 631 | * @param array $triple Array containing the matching block triple to add to the running total. 632 | * @return int The new running total for the number of matches. 633 | */ 634 | private function ratioReduce($sum, array $triple) 635 | { 636 | return $sum + ($triple[count($triple) - 1]); 637 | } 638 | 639 | /** 640 | * Quickly return an upper bound ratio for the similarity of the strings. 641 | * This is quicker to compute than Ratio(). 642 | * 643 | * @return float The calculated ratio. 644 | * @todo throw away or make public 645 | */ 646 | private function quickRatio() 647 | { 648 | if ($this->fullBCount === null) { 649 | $this->fullBCount = []; 650 | $bLength = count($this->b); 651 | for ($i = 0; $i < $bLength; ++$i) { 652 | $char = $this->b[$i]; 653 | $this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1; 654 | } 655 | } 656 | 657 | $avail = []; 658 | $matches = 0; 659 | $aLength = count($this->a); 660 | for ($i = 0; $i < $aLength; ++$i) { 661 | $char = $this->a[$i]; 662 | if (isset($avail[$char])) { 663 | $numb = $avail[$char]; 664 | } else { 665 | $numb = $this->arrayGetDefault($this->fullBCount, $char, 0); 666 | } 667 | $avail[$char] = $numb - 1; 668 | if ($numb > 0) { 669 | ++$matches; 670 | } 671 | } 672 | 673 | $this->calculateRatio($matches, count($this->a) + count($this->b)); 674 | } 675 | 676 | /** 677 | * Return an upper bound ratio really quickly for the similarity of the strings. 678 | * This is quicker to compute than Ratio() and quickRatio(). 679 | * 680 | * @return float The calculated ratio. 681 | * @todo throw away or make public 682 | */ 683 | private function realquickRatio() 684 | { 685 | $aLength = count($this->a); 686 | $bLength = count($this->b); 687 | 688 | return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); 689 | } 690 | 691 | /** 692 | * Helper function for calculating the ratio to measure similarity for the strings. 693 | * The ratio is defined as being 2 * (number of matches / total length) 694 | * 695 | * @param int $matches The number of matches in the two strings. 696 | * @param int $length The length of the two strings. 697 | * @return float The calculated ratio. 698 | */ 699 | private function calculateRatio($matches, $length = 0) 700 | { 701 | if ($length) { 702 | return 2 * ($matches / $length); 703 | } else { 704 | return 1; 705 | } 706 | } 707 | 708 | /** 709 | * Helper function that provides the ability to return the value for a key 710 | * in an array of it exists, or if it doesn't then return a default value. 711 | * Essentially cleaner than doing a series of if(isset()) {} else {} calls. 712 | * 713 | * @param array $array The array to search. 714 | * @param string $key The key to check that exists. 715 | * @param mixed $default The value to return as the default value if the key doesn't exist. 716 | * @return mixed The value from the array if the key exists or otherwise the default. 717 | */ 718 | private function arrayGetDefault(array $array, $key, $default) 719 | { 720 | if (isset($array[$key])) { 721 | return $array[$key]; 722 | } else { 723 | return $default; 724 | } 725 | } 726 | 727 | /** 728 | * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks 729 | * 730 | * @param array $a First array to compare. 731 | * @param array $b Second array to compare. 732 | * @return int -1, 0 or 1, as expected by the usort function. 733 | */ 734 | private function tupleSort(array $a, array $b) 735 | { 736 | $max = max(count($a), count($b)); 737 | for ($i = 0; $i < $max; ++$i) { 738 | if ($a[$i] < $b[$i]) { 739 | return -1; 740 | } else { 741 | if ($a[$i] > $b[$i]) { 742 | return 1; 743 | } 744 | } 745 | } 746 | 747 | if (count($a) == count($b)) { 748 | return 0; 749 | } else { 750 | if (count($a) < count($b)) { 751 | return -1; 752 | } else { 753 | return 1; 754 | } 755 | } 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License (BSD License) 2 | 3 | Portions Copyright by Contributors of the Neos Project - www.neos.io 4 | 5 | Copyright (c) 2009 Chris Boulton 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | - Redistributions of source code must retain the above copyright notice, 12 | this list of conditions and the following disclaimer. 13 | - Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | - Neither the name of the Chris Boulton nor the names of its contributors 17 | may be used to endorse or promote products derived from this software 18 | without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 24 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![BSD License](https://img.shields.io/github/license/mashape/apistatus.svg)] 2 | [![Latest Stable Version](https://poser.pugx.org/neos/diff/version)](https://packagist.org/packages/neos/diff) 3 | 4 | # Diff Library 5 | 6 | This is a repackaged and modernized version of Chris Boulton's PHP Diff 7 | Library. It has been transformed to the Neos namespace and is working out 8 | of the box with Composer's and Flow's auto loading mechanism. This library 9 | is compatible with PHP 5 (tested with 5.5 and 5.6) and PHP 7. 10 | 11 | Note: Even though this library is rather stable and has not been modified 12 | by its original author for years, the Neos Team does not actively maintain 13 | all contained renderers. 14 | 15 | ## Features 16 | 17 | This is a comprehensive library for generating differences between 18 | two hashable objects (strings or arrays). Generated differences can be 19 | rendered in all of the standard formats including: 20 | 21 | * Unified 22 | * Context 23 | * Inline HTML 24 | * Side by Side HTML 25 | 26 | The logic behind the core of the diff engine (ie, the sequence matcher) 27 | is primarily based on the Python [difflib package](https://docs.python.org/2/library/difflib.html). The reason for doing 28 | so is primarily because of its high degree of accuracy. 29 | 30 | 31 | ## License (BSD License) 32 | 33 | Portions Copyright by Contributors of the Neos Project - www.neos.io 34 | 35 | Copyright (c) 2009 Chris Boulton 36 | All rights reserved. 37 | 38 | Redistribution and use in source and binary forms, with or without 39 | modification, are permitted provided that the following conditions are met: 40 | 41 | - Redistributions of source code must retain the above copyright notice, 42 | this list of conditions and the following disclaimer. 43 | - Redistributions in binary form must reproduce the above copyright notice, 44 | this list of conditions and the following disclaimer in the documentation 45 | and/or other materials provided with the distribution. 46 | - Neither the name of the Chris Boulton nor the names of its contributors 47 | may be used to endorse or promote products derived from this software 48 | without specific prior written permission. 49 | 50 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 51 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 52 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 53 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 54 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 55 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 56 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 57 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 58 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 59 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 60 | POSSIBILITY OF SUCH DAMAGE. 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neos/diff", 3 | "type": "neos-package", 4 | "license": "BSD-3-Clause", 5 | "description": "This is a comprehensive library for generating differences between two strings or arrays", 6 | "require": { 7 | "php": "^8.0" 8 | }, 9 | "autoload": { 10 | "psr-4": { 11 | "Neos\\Diff\\": "Classes" 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------