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