diff --git a/wire/core/WireTextTools.php b/wire/core/WireTextTools.php index d9c02a4d..2350a6cf 100644 --- a/wire/core/WireTextTools.php +++ b/wire/core/WireTextTools.php @@ -854,6 +854,117 @@ class WireTextTools extends Wire { return $str; } + /** + * Given two arrays, return array of the changes with 'ins' and 'del' keys + * + * Based upon Paul Butler’s Simple Diff Algorithm v0.1 © 2007 (zlib/libpng) https://paulbutler.org + * + * @param array $oldArray + * @param array $newArray + * @return array + * + */ + protected function diffArray(array $oldArray, array $newArray) { + + $matrix = array(); + $maxLen = 0; + $oldMax = 0; + $newMax = 0; + + foreach($oldArray as $oldKey => $oldValue){ + + $newKeys = array_keys($newArray, $oldValue); + + foreach($newKeys as $newKey) { + $len = 1; + if(isset($matrix[$oldKey - 1][$newKey - 1])) { + $len = $matrix[$oldKey - 1][$newKey - 1] + 1; + } + $matrix[$oldKey][$newKey] = $len; + + if($len > $maxLen) { + $maxLen = $len; + $oldMax = $oldKey + 1 - $maxLen; + $newMax = $newKey + 1 - $maxLen; + } + } + } + + if($maxLen == 0) { + $result = array( + array('del' => $oldArray, 'ins' => $newArray) + ); + + } else { + $result = array_merge( + $this->diffArray( + array_slice($oldArray, 0, $oldMax), + array_slice($newArray, 0, $newMax) + ), + array_slice($newArray, $newMax, $maxLen), + $this->diffArray( + array_slice($oldArray, $oldMax + $maxLen), + array_slice($newArray, $newMax + $maxLen) + ) + ); + } + + return $result; + } + + /** + * Given two strings ($old and $new) return a diff string in HTML markup + * + * @param string $old Old string value + * @param string $new New string value + * @param array $options Options to modify behavior: + * - `ins` (string) Markup to use for diff insertions (default: `{out}`) + * - `del` (string) Markup to use for diff deletions (default: `{out}`) + * - `entityEncode` (bool): Entity encode values, other than added ins/del tags? (default=true) + * - `split` (string): Regex used to split strings for parts to diff (default=`\s+`) + * @return string + * + */ + public function diffMarkup($old, $new, array $options = array()) { + + $defaults = array( + 'ins' => "{out}", + 'del' => "{out}", + 'entityEncode' => true, + 'split' => '\s+', + ); + + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); + list($old, $new) = array("$old", "$new"); // enforce as string + $options = array_merge($defaults, $options); + $oldArray = preg_split("!($options[split])!", $old, 0, PREG_SPLIT_DELIM_CAPTURE); + $newArray = preg_split("!($options[split])!", $new, 0, PREG_SPLIT_DELIM_CAPTURE); + $diffArray = $this->diffArray($oldArray, $newArray); + list(,$delClose) = explode('{out}', $options['del'], 2); + list($insOpen,) = explode('{out}', $options['ins'], 2); + $out = ''; + + foreach($diffArray as $diff) { + if(is_array($diff)) { + foreach(array('del', 'ins') as $key) { + if(empty($diff[$key])) continue; + $diffStr = implode('', $diff[$key]); + if($options['entityEncode']) $diffStr = $sanitizer->entities1($diffStr); + $out .= str_replace('{out}', $diffStr, $options[$key]); + } + } else { + $out .= ($options['entityEncode'] ? $sanitizer->entities1($diff) : $diff); + } + } + + if(strpos($out, "$delClose$insOpen")) { + // put a space between '' so that it is ' ' + $out = str_replace("$delClose$insOpen", "$delClose $insOpen", $out); + } + + return $out; + } /*********************************************************************************************************** * MULTIBYTE PHP STRING FUNCTIONS THAT FALLBACK WHEN MBSTRING NOT AVAILABLE