├── 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 .= 'Old | ';
38 | $html .= 'New | ';
39 | $html .= 'Differences | ';
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 .= '' . $fromLine . ' | ';
63 | $html .= '' . $toLine . ' | ';
64 | $html .= '' . $line . ' | ';
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 .= '' . $toLine . ' | ';
75 | $html .= '' . $line . ' | ';
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 .= '' . $fromLine . ' | ';
85 | $html .= ' | ';
86 | $html .= '' . $line . ' | ';
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 .= '' . $fromLine . ' | ';
96 | $html .= ' | ';
97 | $html .= '' . $line . ' | ';
98 | $html .= '
';
99 | }
100 |
101 | foreach ($change['changed']['lines'] as $no => $line) {
102 | $toLine = $change['changed']['offset'] + $no + 1;
103 | $html .= '';
104 | $html .= '' . $toLine . ' | ';
105 | $html .= ' | ';
106 | $html .= '' . $line . ' | ';
107 | $html .= '
';
108 | }
109 | }
110 | }
111 | }
112 | }
113 | $html .= '';
114 | }
115 | }
116 | $html .= '
';
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 .= 'Old Version | ';
39 | $html .= 'New Version | ';
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 .= '' . $fromLine . ' | ';
59 | $html .= '' . $line . ' | ';
60 | $html .= '' . $toLine . ' | ';
61 | $html .= '' . $line . ' | ';
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 .= '' . $toLine . ' | ';
73 | $html .= '' . $line . ' | ';
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 .= '' . $fromLine . ' | ';
83 | $html .= '' . $line . ' | ';
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 .= '' . $fromLine . ' | ';
96 | $html .= '' . $line . ' | ';
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 .= '' . $toLine . ' | ';
105 | $html .= '' . $changedLine . ' | ';
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 .= '' . $fromLine . ' | ';
119 | $html .= '' . $line . ' | ';
120 | $toLine = $change['changed']['offset'] + $no + 1;
121 | $html .= '' . $toLine . ' | ';
122 | $html .= '' . $changedLine . ' | ';
123 | $html .= '
';
124 | }
125 | }
126 | }
127 | }
128 | }
129 | }
130 | $html .= '';
131 | }
132 | }
133 | $html .= '
';
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 | []
2 | [](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 |
--------------------------------------------------------------------------------