* @license BSD */ //namespace Domframework; /** Create a diff from two strings, array or files * The output is compatible with "patch" command. */ class xdiff { /** The name of file1 */ private $filename1 = "Original"; /** The name of file2 */ private $filename2 = "New"; /** The timestamp for file1 */ private $file1Time; /** The timestamp for file2 */ private $file2Time; /** The output requested */ private $output = null; /** Define the size of one column in side by side mode * 62 chars by default * The maximum width used on screen is 2*$s2sWidth+2 (134 by default) * The value must validate $s2sWidth = 8 * X - 2 (with X is integer), like * 6, 14, 22, 30, 38, 46, 54, 62, 70, 78, 86, 94, 102 */ private $s2sWidth = 62; /** The constructor allow to choose the output. * @param string $output The output mode [Normal|Unified|SideBySide] */ public function __construct ($output = "normal") // {{{ { if (! method_exists ($this, "display".ucfirst ($output))) throw new \Exception ("Invalid output requested to xdiff", 406); $this->output = "display".ucfirst ($output); $this->file1Time = date ("Y-m-d H:i:s.u000 O"); $this->file2Time = date ("Y-m-d H:i:s.u001 O"); } // }}} /** Allow to set the side by side width to the maximum allowed by screenWidth * @param integer $screenWidth The maximum width of the screen */ public function setScreenWidth ($screenWidth) // {{{ { for ($x = 20 ; $x > 0 ; $x--) { $s2sWidth = 8 * $x - 2; if (2*$s2sWidth+2 <= $screenWidth) { $this->s2sWidth = $s2sWidth; break; } } } // }}} /** Compute the differences between two strings $string1 and $string2 * @param string $string1 The first string to compare * @param string $string2 The second string to compare */ public function diff ($string1, $string2) // {{{ { if (! is_string ($string1)) throw new \Exception ( "Invalid string1 provided to diff method : not a string", 406); if (! is_string ($string2)) throw new \Exception ( "Invalid string2 provided to diff method : not a string", 406); return $this->diffArray ( preg_split ("#(.*\\R)#", $string1, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY), preg_split ("#(.*\\R)#", $string2, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)); } // }}} /** Compute the differences between two files $file1 and $file2 * @param string $file1 The first file to use to compare * @param string $file2 The second file to use to compare */ public function diffFile ($file1, $file2) // {{{ { if (! is_string ($file1)) throw new \Exception ( "Invalid file1 provided to diff method : not a string", 406); if (! is_string ($file2)) throw new \Exception ( "Invalid file2 provided to diff method : not a string", 406); if (! file_exists ($file1)) throw new \Exception ( "Invalid file1 provided to diff method : file don't exists", 406); if (! file_exists ($file2)) throw new \Exception ( "Invalid file2 provided to diff method : file don't exists", 406); if (! is_readable ($file1)) throw new \Exception ( "Invalid file1 provided to diff method : file is not readable", 406); if (! is_readable ($file2)) throw new \Exception ( "Invalid file2 provided to diff method : file is not readable", 406); $this->filename1 = $file1; $this->filename2 = $file2; $this->file1Time = date ("Y-m-d H:i:s.u000 O", filemtime ($this->filename1)); $this->file2Time = date ("Y-m-d H:i:s.u001 O", filemtime ($this->filename2)); return $this->diff (file_get_contents ($file1), file_get_contents ($file2)); } // }}} /** Compute the differences between two arrays $array1 and $array2 * @param array $array1 The first array to compare * @param array $array2 The second array to compare */ public function diffArray ($array1, $array2) // {{{ { $diff = $this->computeArray ($array1, $array2); $method = $this->output; return $this->$method ($diff); } // }}} /** Compute the differences between two arrays $array1 and $array2 * @param array $array1 The first array to compare * @param array $array2 The second array to compare * @return array The data in internal format */ final public function computeArray ($array1, $array2) // {{{ { if (! is_array ($array1)) throw new \Exception ( "Invalid array1 provided to diffArray method : not a array", 406); if (! is_array ($array2)) throw new \Exception ( "Invalid array2 provided to diffArray method : not a array", 406); $array1 = array_values ($array1); $array2 = array_values ($array2); $diff = array (); $i = 0; // $i is the index for $array1 $j = 0; // $j is the index for $array2 while ($i < count ($array1) || $j < count ($array2)) { $chunk1 = array (); $chunk2 = array (); if (key_exists ($i, $array1) && is_array ($array1[$i])) throw new \Exception ("Can not diff a multidimensional array"); if (key_exists ($j, $array2) && is_array ($array2[$j])) throw new \Exception ("Can not diff a multidimensional array"); if (key_exists ($i, $array1) && key_exists ($j, $array2) && $array1[$i] === $array2[$j]) { // EQUAL while (key_exists ($i, $array1) && key_exists ($j, $array2) && $array1[$i] === $array2[$j]) { $chunk1[] = $array1[$i]; $i++; $j++; } $diff[] = array ("type" => "Equal", "startLine1" => 1 + $i - count ($chunk1), "endLine1" => $i, "startLine2" => 1 + $j - count ($chunk1), "endLine2" => $j, "length" => count ($chunk1), "chunk" => $chunk1); continue; } // Generate the chunks $lcs = $this->lcs ($array1, $array2, $i, $j); if ($lcs === "") { while ($i < count ($array1)) { if ($array1[$i] === $lcs) break; $chunk1[] = $array1[$i]; $i++; $lcs = $this->lcs ($array1, $array2, $i, $j); } while ($j < count ($array2)) { if ($array2[$j] === $lcs) break; $chunk2[] = $array2[$j]; $j++; $lcs = $this->lcs ($array1, $array2, $i, $j); } } else { while ($i < count ($array1)) { if ($array1[$i] === $lcs) break; $chunk1[] = $array1[$i]; $i++; } while ($j < count ($array2)) { if ($array2[$j] === $lcs) break; $chunk2[] = $array2[$j]; $j++; } } // Add the diffs by the chunks availability if (empty ($chunk1) && ! empty ($chunk2)) { // APPEND $diff[] = array ("type" => "Append", "startLine1" => $i, "endLine1" => $i, "startLine2" => 1 + $j - count ($chunk2), "endLine2" => $j, "length" => count ($chunk2), "chunk" => $chunk2); } elseif (! empty ($chunk1) && empty ($chunk2)) { // DELETE $diff[] = array ("type" => "Delete", "startLine1" => 1 + $i - count ($chunk1), "endLine1" => $i, "startLine2" => $j, "endLine2" => $j, "length" => count ($chunk1), "chunk" => $chunk1); } else { // CHANGE ON BOTH ARRAY $diff[] = array ("type" => "Change", "startLine1" => 1 + $i - count ($chunk1), "endLine1" => $i, "startLine2" => 1 + $j - count ($chunk2), "endLine2" => $j, "length1" => count ($chunk1), "length2" => count ($chunk2), "chunk1" => $chunk1, "chunk2" => $chunk2); } } return $diff; } // }}} /** Return a string like "diff -u" * @param array $diffArray The diff array analyzed by diffArray method * @return string */ private function displayUnified ($diffArray) // {{{ { $d = ""; $i = 0 ; while ($i < count ($diffArray)) { $diff = $diffArray[$i]; $i++; if ($diff["type"] === "Equal") continue; if ($diff["type"] === "Append") { $info = "@@ -"; $info .= $diff["startLine1"]; $info .= ",0"; $info .= " +"; $info .= $diff["startLine2"]; if ($diff["length"] !== 1) $info .= ",".$diff["length"]; $info .= " @@\n"; $d .= $info; $d .= "+".implode ("+", $diff["chunk"]); } elseif ($diff["type"] === "Delete") { $info = "@@ -"; $info .= $diff["startLine1"]; if ($diff["length"] !== 1) $info .= ",".$diff["length"]; $info .= " +"; $info .= $diff["startLine2"]; $info .= ",0"; $info .= " @@\n"; $d .= $info; $d .= "-".implode ("-", $diff["chunk"]); } elseif ($diff["type"] === "Change") { $info = "@@ -"; $info .= $diff["startLine1"]; if ($diff["length1"] !== 1) $info .= ",".$diff["length1"]; $info .= " +"; $info .= $diff["startLine2"]; if ($diff["length2"] !== 1) $info .= ",".$diff["length2"]; $info .= " @@\n"; $d .= $info; $d .= "-".implode ("-", $diff["chunk1"]); $d .= "+".implode ("+", $diff["chunk2"]); } else throw new \Exception ("Invalid Chunk Type : ".$diff["type"]); } if ($d === "") return $d; $e = "--- $this->filename1 $this->file1Time\n"; $e .= "+++ $this->filename2 $this->file2Time\n"; return $e.$d; } // }}} /** Return a string like "diff" without parameter * @param array $diffArray The diff array analyzed by diffArray method * @return string */ private function displayNormal ($diffArray) // {{{ { $d = ""; $i = 0 ; while ($i < count ($diffArray)) { $diff = $diffArray[$i]; $i++; if ($diff["type"] === "Equal") continue; $info = $diff["startLine1"]; if ($diff["startLine1"] !== $diff["endLine1"]) $info .= ",".$diff["endLine1"]; $info .= "%s"; $info .= $diff["startLine2"]; if ($diff["startLine2"] !== $diff["endLine2"]) $info .= ",".$diff["endLine2"]; $info .= "\n"; if ($diff["type"] === "Append") { $d .= sprintf ($info, "a"); $d .= "> ".implode ("> ", $diff["chunk"]); } elseif ($diff["type"] === "Delete") { $d .= sprintf ($info, "d"); $d .= "< ".implode ("< ", $diff["chunk"]); } elseif ($diff["type"] === "Change") { $d .= sprintf ($info, "c"); $d .= "< ".implode ("< ", $diff["chunk1"]); $d .= "---\n"; $d .= "> ".implode ("> ", $diff["chunk2"]); } else throw new \Exception ("Invalid Chunk Type : ".$diff["type"]); } return $d; } // }}} /** Return a string like "diff -y" (Side by Side) * @param array $diffArray The diff array analyzed by diffArray method * @return string */ private function displaySideBySide ($diffArray) // {{{ { $d = ""; $i = 0 ; while ($i < count ($diffArray)) { $diff = $diffArray[$i]; $i++; if ($diff["type"] === "Equal") { foreach ($diff["chunk"] as $line) $d .= $this->side ($line, $line); } elseif ($diff["type"] === "Append") { foreach ($diff["chunk"] as $line) $d .= $this->side ("", $line, ">"); } elseif ($diff["type"] === "Delete") { foreach ($diff["chunk"] as $line) $d .= $this->side ($line, "", "<"); } elseif ($diff["type"] === "Change") { $x = 0; $y = 0; while (key_exists ($x, $diff["chunk1"]) || key_exists ($y, $diff["chunk2"])) { $side1 = (key_exists ($x, $diff["chunk1"])) ? $diff["chunk1"][$x] : ""; $side2 = (key_exists ($y, $diff["chunk2"])) ? $diff["chunk2"][$y] : ""; $d .= $this->side ($side1, $side2, "|"); if ($side1 !== "") $x++; if ($side2 !== "") $y++; } } } return $d; } // }}} /** Return a string sideBySide. Used by displaySideBySide for each line * @param string $side1 The string to be displayed on side1 * @param string $side2 The string to be displayed on side2 * @param string|null $symbol The symbol used to present the diff (<>|) */ private function side ($side1, $side2, $symbol = null) // {{{ { $d = ""; if (! is_string ($side1)) throw new \Exception ("XDiff : side1 parameter not a string"); if (! is_string ($side2)) throw new \Exception ("XDiff : side2 parameter not a string"); if (trim ($symbol) === "") $symbol = null; $side1 = mb_substr (rtrim ($side1), 0, $this->s2sWidth - 1); $side2 = mb_substr (rtrim ($side2), 0, $this->s2sWidth - 1); $side1 = str_replace ("\t", " ", $side1); $side2 = str_replace ("\t", " ", $side2); $d .= $side1; $nbtabs = floor ((1 + $this->s2sWidth - mb_strlen ($side1)) / 8); if ($symbol !== null || $side2 !== "") $d .= str_repeat ("\t", $nbtabs); if ($side1 !== "" && $symbol === null) $d .= "\t"; elseif ($symbol !== null) { if (mb_strlen ($side1) < $this->s2sWidth - 1) { if ($nbtabs > 1) $d .= str_repeat (" ", 5); else $d .= str_repeat (" ", $this->s2sWidth - mb_strlen ($side1) - 1); } $d .= " ".$symbol; } if ($symbol !== null && $side2 !== "") $d .= "\t"; $d .= $side2; $d .= "\n"; return $d; } // }}} /** This function return the next common part between both arrays starting at * position $i for $array1 and $j for array2 * Return empty string if no common lines was found * @param array $array1 The first array to compare * @param array $array2 The second array to compare * @param integer $i The position pointer on first array * @param integer $j The position pointer on second array * @return string */ private function lcs ($array1, $array2, $i, $j) // {{{ { $found1 = false; $found2 = false; while ($i < count ($array1)) { $tmp2 = $j; while ($tmp2 < count ($array2)) { if ($array1[$i] === $array2[$tmp2] && trim ($array1[$i]) !== "") { $found1 = true; break 2; } $tmp2++; } $i++; } while ($j < count ($array2)) { $tmp1 = $i; while ($tmp1 < count ($array1)) { if ($array2[$j] === $array1[$tmp1] && trim ($array2[$j]) !== "") { $found2 = true; break 2; } $tmp1++; } $j++; } if (! $found1 || ! $found2) return ""; if ($tmp1 - $i < $tmp2 - $j) { return ""; } return $array1[$i]; } // }}} }