├── LICENSE ├── README.md ├── composer.json └── src └── ViKon └── Diff ├── Diff.php ├── DiffException.php ├── DiffGroup.php ├── Entry ├── AbstractEntry.php ├── DeletedEntry.php ├── InsertedEntry.php └── UnmodifiedEntry.php └── Text.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 ViKon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff tool 2 | 3 | This package is for comparison strings and show changes. 4 | 5 | ## Table of content 6 | 7 | * [Features](#features) 8 | * [Installation](#installation) 9 | * [Configuration](#configuration) 10 | * [Usage](#usage) 11 | 12 | --- 13 | [Back to top](#diff-tool) 14 | 15 | ## Features 16 | 17 | * compare **strings** 18 | * compare **files** 19 | * group string differences into **hunk groups** 20 | 21 | ## Installation 22 | 23 | Via `composer`: 24 | 25 | ```bash 26 | composer require vi-kon/laravel-diff 27 | ``` 28 | 29 | --- 30 | [Back to top](#diff-tool) 31 | 32 | ## Usage 33 | 34 | Simple usage: 35 | 36 | ```php 37 | // Compare string line by line 38 | $diff = Diff::compare("hello\na", "hello\nasd\na"); 39 | // Outputs span, ins, del HTML tags, depend if entry 40 | // is unmodified, inserted or deleted 41 | echo $diff->toHTML(); 42 | ``` 43 | 44 | Compare two file: 45 | 46 | ```php 47 | // Compare files line by line 48 | $diff = Diff::compareFiles("a.txt", "b.txt"); 49 | echo $diff->toHTML(); 50 | ``` 51 | 52 | You can customize output by getting raw data: 53 | 54 | ```php 55 | 56 | $options = [ 57 | // Compare by line or by characters 58 | 'compareCharacters' => false, 59 | // Offset size in hunk groups 60 | 'offset' => 2, 61 | ]; 62 | 63 | $diff = Diff::compare("hello\na", "hello\nasd\na", $options); 64 | $groups = $diff->getGroups(); 65 | 66 | foreach($groups as $i => $group) 67 | { 68 | // Output: Hunk 1 : Lines 2 - 6 69 | echo 'Hunk ' . $i . ' : Lines ' 70 | . $group->getFirstPosition() . ' - ' . $group->getLastPosition(); 71 | 72 | // Output changed lines (entries) 73 | foreach($group->getEntries() as $entry) 74 | { 75 | // Output old position of line 76 | echo $entry instanceof \ViKon\Diff\Entry\InsertedEntry 77 | ? '-' 78 | : $entry->getOldPosition() + 1; 79 | 80 | echo ' | '; 81 | 82 | // Output new position of line 83 | echo $entry instanceof \ViKon\Diff\Entry\DeletedEntry 84 | ? '-' 85 | : $entry->getNewPosition() + 1; 86 | 87 | echo ' - '; 88 | 89 | // Output line (entry) 90 | echo $entry; 91 | } 92 | } 93 | 94 | ``` 95 | 96 | --- 97 | [Back to top](#diff-tool) 98 | 99 | ## License 100 | 101 | This package is licensed under the MIT License 102 | 103 | --- 104 | [Back to top](#diff-tool) 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "vi-kon/laravel-diff", 3 | "description" : "Diff tool for Laravel 5", 4 | "keywords" : [ 5 | "laravel", 6 | "diff", 7 | "compare" 8 | ], 9 | "license" : "MIT", 10 | "homepage" : "https://github.com/vi-kon/laravel-diff", 11 | "authors" : [ 12 | { 13 | "name" : "Kovács Vince", 14 | "email": "vincekovacs@hotmail.com" 15 | } 16 | ], 17 | "require" : { 18 | "php": ">=5.5.9" 19 | }, 20 | "autoload" : { 21 | "psr-4": { 22 | "ViKon\\Diff\\": "src/ViKon/Diff" 23 | } 24 | }, 25 | "minimum-stability": "stable" 26 | } 27 | -------------------------------------------------------------------------------- /src/ViKon/Diff/Diff.php: -------------------------------------------------------------------------------- 1 | old = new Text($old, $options); 62 | $this->new = new Text($new, $options); 63 | 64 | $diffTable = $this->buildDiffTable($this->old, $this->new); 65 | list($this->diff, $this->inserted, $this->deleted) = $this->buildDiff($this->old, $this->new, $diffTable); 66 | $this->groups = $this->buildGroups($this->diff, isset($options['offset']) && $options['offset'] ? $options['offset'] : 2); 67 | } 68 | 69 | /** 70 | * Get inserted changes count 71 | * 72 | * @return int 73 | */ 74 | public function getInsertedCount() { 75 | return $this->inserted; 76 | } 77 | 78 | /** 79 | * Get deleted changes count 80 | * 81 | * @return int 82 | */ 83 | public function getDeletedCount() { 84 | return $this->deleted; 85 | } 86 | 87 | /** 88 | * Get hunk groups 89 | * 90 | * @return \ViKon\Diff\DiffGroup[] 91 | */ 92 | public function getGroups() { 93 | return $this->groups; 94 | } 95 | 96 | /** 97 | * Render diff to HTML 98 | * 99 | * * span tag - no changes 100 | * * del tag - deleted entry 101 | * * ins tag - inserted entry 102 | * 103 | * @param string $separator 104 | * 105 | * @return string 106 | */ 107 | public function toHTML($separator = '
') { 108 | $output = ''; 109 | 110 | foreach ($this->diff as $entry) { 111 | $element = ''; 112 | if ($entry instanceof UnmodifiedEntry) { 113 | $element = 'span'; 114 | } elseif ($entry instanceof DeletedEntry) { 115 | $element = 'del'; 116 | } elseif ($entry instanceof InsertedEntry) { 117 | $element = 'ins'; 118 | } 119 | $output .= '<' . $element . '>' . $entry . '' . $separator; 120 | } 121 | 122 | return $output; 123 | } 124 | 125 | /** 126 | * @param \ViKon\Diff\Text $old 127 | * @param \ViKon\Diff\Text $new 128 | * 129 | * @return array 130 | */ 131 | private function buildDiffTable(Text $old, Text $new) { 132 | $diffTable = [array_fill(0, count($new) + 1, 0)]; 133 | for ($row = 1; $row <= count($old); $row++) { 134 | $diffTable[$row] = [0]; 135 | for ($col = 1; $col <= count($new); $col++) { 136 | $diffTable[$row][$col] = $old[$row - 1] === $new[$col - 1] 137 | ? $diffTable[$row - 1][$col - 1] + 1 138 | : max($diffTable[$row - 1][$col], $diffTable[$row][$col - 1]); 139 | } 140 | } 141 | 142 | return $diffTable; 143 | } 144 | 145 | /** 146 | * @param \ViKon\Diff\Text $old 147 | * @param \ViKon\Diff\Text $new 148 | * @param $diffTable 149 | * 150 | * @return array 151 | */ 152 | private function buildDiff(Text $old, Text $new, $diffTable) { 153 | $inserted = 0; 154 | $deleted = 0; 155 | $diff = []; 156 | 157 | $row = count($this->old); 158 | $col = count($this->new); 159 | while ($row > 0 || $col > 0) { 160 | if ($row > 0 && $col > 0 && $this->old[$row - 1] === $this->new[$col - 1]) { 161 | $diff[] = new UnmodifiedEntry($old, $row - 1, $col - 1); 162 | $row--; 163 | $col--; 164 | } elseif ($col > 0 && $diffTable[$row][$col] === $diffTable[$row][$col - 1]) { 165 | $diff[] = new InsertedEntry($new, $col - 1); 166 | $inserted++; 167 | $col--; 168 | } else { 169 | $diff[] = new DeletedEntry($old, $row - 1); 170 | $deleted++; 171 | $row--; 172 | } 173 | } 174 | 175 | return [array_reverse($diff), $inserted, $deleted]; 176 | } 177 | 178 | /** 179 | * @param $diff 180 | * @param int $offset 181 | * 182 | * @return \ViKon\Diff\DiffGroup[] 183 | */ 184 | private function buildGroups($diff, $offset = 2) { 185 | $groups = []; 186 | 187 | for ($i = 0; $i < count($diff); $i++) { 188 | if (!$diff[$i] instanceof UnmodifiedEntry) { 189 | $group = new DiffGroup(); 190 | 191 | // Beginning offset 192 | for ($j = $i - 1; $j >= $i - $offset && $j >= 0; $j--) { 193 | if ($diff[$j] instanceof UnmodifiedEntry) { 194 | $group->addEntry($diff[$j], true); 195 | } 196 | } 197 | 198 | for (; $i < count($diff) && !$diff[$i] instanceof UnmodifiedEntry; $i++) { 199 | $group->addEntry($diff[$i]); 200 | } 201 | 202 | // Ending offset 203 | for ($j = $i; $j <= $i + $offset - 1 && $j < count($diff); $j++) { 204 | if ($diff[$j] instanceof UnmodifiedEntry) { 205 | $group->addEntry($diff[$j]); 206 | } 207 | } 208 | 209 | $groups[] = $group; 210 | } 211 | } 212 | 213 | return $groups; 214 | } 215 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/DiffException.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * @package ViKon\Diff 17 | */ 18 | class DiffException extends \Exception { 19 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/DiffGroup.php: -------------------------------------------------------------------------------- 1 | entries, $entry); 30 | } else { 31 | $this->entries[] = $entry; 32 | } 33 | } 34 | 35 | /** 36 | * Get hunk group first entry position 37 | * 38 | * @return int 39 | */ 40 | public function getFirstPosition() { 41 | return reset($this->entries)->getNewPosition(); 42 | } 43 | 44 | /** 45 | * Get hunk group last entry position 46 | * 47 | * @return int 48 | */ 49 | public function getLastPosition() { 50 | return end($this->entries)->getNewPosition(); 51 | } 52 | 53 | public function getEntries() { 54 | return $this->entries; 55 | } 56 | 57 | public function offsetExists($offset) { 58 | return isset($this->entries[$offset]); 59 | } 60 | 61 | public function offsetGet($offset) { 62 | return isset($this->entries[$offset]) ? $this->entries[$offset] : null; 63 | } 64 | 65 | public function offsetSet($offset, $value) { 66 | throw new DiffException('Set is not supported'); 67 | } 68 | 69 | public function offsetUnset($offset) { 70 | throw new DiffException('Unset is not supported'); 71 | } 72 | 73 | /** 74 | * Get entries count 75 | * 76 | * @return int 77 | */ 78 | public function count() { 79 | return count($this->entries); 80 | } 81 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/Entry/AbstractEntry.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @package ViKon\Diff\Entry 11 | */ 12 | abstract class AbstractEntry { 13 | 14 | /** @var string */ 15 | protected $content; 16 | 17 | /** @var int|null */ 18 | protected $oldPosition = null; 19 | 20 | /** @var int|null */ 21 | protected $newPosition = null; 22 | 23 | /** 24 | * @return int|null 25 | */ 26 | public function getOldPosition() { 27 | return $this->oldPosition; 28 | } 29 | 30 | /** 31 | * @return int|null 32 | */ 33 | public function getNewPosition() { 34 | return $this->newPosition; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getContent() { 41 | return $this->content; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function __toString() { 48 | return htmlspecialchars($this->content); 49 | } 50 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/Entry/DeletedEntry.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @package ViKon\Diff\Entry 12 | */ 13 | class DeletedEntry extends AbstractEntry { 14 | /** 15 | * @param \ViKon\Diff\Text $content 16 | * @param int $oldPosition 17 | */ 18 | public function __construct(Text $content, $oldPosition) { 19 | $this->content = $content[$oldPosition]; 20 | $this->oldPosition = $oldPosition; 21 | } 22 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/Entry/InsertedEntry.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @package ViKon\Diff\Entry 12 | */ 13 | class InsertedEntry extends AbstractEntry { 14 | /** 15 | * @param \ViKon\Diff\Text $content 16 | * @param int $newPosition 17 | */ 18 | public function __construct(Text $content, $newPosition) { 19 | $this->content = $content[$newPosition]; 20 | $this->newPosition = $newPosition; 21 | } 22 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/Entry/UnmodifiedEntry.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @package ViKon\Diff\Entry 13 | */ 14 | class UnmodifiedEntry extends AbstractEntry { 15 | 16 | /** 17 | * @param \ViKon\Diff\Text $content 18 | * @param int $newPosition 19 | * @param int $oldPosition 20 | */ 21 | public function __construct(Text $content, $oldPosition, $newPosition) { 22 | $this->content = $content[$newPosition]; 23 | $this->newPosition = $newPosition; 24 | $this->oldPosition = $oldPosition; 25 | } 26 | } -------------------------------------------------------------------------------- /src/ViKon/Diff/Text.php: -------------------------------------------------------------------------------- 1 | options = $options; 22 | 23 | if(isset($this->options['compareWords']) && $this->options['compareWords']) { 24 | $this->source = preg_split('/[\s]+/', $source); 25 | } else if (isset($this->options['compareCharacters']) && $this->options['compareCharacters']) { 26 | $this->source = str_split($source); 27 | } else { 28 | $this->source = preg_split('/(\R)/', $source); 29 | } 30 | } 31 | 32 | public function offsetExists($offset) { 33 | return isset($this->source[$offset]); 34 | } 35 | 36 | public function offsetGet($offset) { 37 | return isset($this->source[$offset]) ? $this->source[$offset] : null; 38 | } 39 | 40 | public function offsetSet($offset, $value) { 41 | throw new DiffException('Set not allowed'); 42 | } 43 | 44 | public function offsetUnset($offset) { 45 | throw new DiffException('Unset not allowed'); 46 | } 47 | 48 | public function count() { 49 | return count($this->source); 50 | }} --------------------------------------------------------------------------------