Changeset View
Changeset View
Standalone View
Standalone View
src/infrastructure/diff/prose/PhutilProseDiff.php
- This file was added.
| <?php | |||||
| final class PhutilProseDiff extends Phobject { | |||||
| private $parts = array(); | |||||
| public function addPart($type, $text) { | |||||
| $this->parts[] = array( | |||||
| 'type' => $type, | |||||
| 'text' => $text, | |||||
| ); | |||||
| return $this; | |||||
| } | |||||
| public function getParts() { | |||||
| return $this->parts; | |||||
| } | |||||
| /** | |||||
| * Get diff parts, but replace large blocks of unchanged text with "." | |||||
| * parts representing missing context. | |||||
| */ | |||||
| public function getSummaryParts() { | |||||
| $parts = $this->getParts(); | |||||
| $head_key = head_key($parts); | |||||
| $last_key = last_key($parts); | |||||
| $results = array(); | |||||
| foreach ($parts as $key => $part) { | |||||
| $is_head = ($key == $head_key); | |||||
| $is_last = ($key == $last_key); | |||||
| switch ($part['type']) { | |||||
| case '=': | |||||
| $pieces = $this->splitTextForSummary($part['text']); | |||||
| if ($is_head || $is_last) { | |||||
| $need = 2; | |||||
| } else { | |||||
| $need = 3; | |||||
| } | |||||
| // We don't have enough pieces to omit anything, so just continue. | |||||
| if (count($pieces) < $need) { | |||||
| $results[] = $part; | |||||
| break; | |||||
| } | |||||
| if (!$is_head) { | |||||
| $results[] = array( | |||||
| 'type' => '=', | |||||
| 'text' => head($pieces), | |||||
| ); | |||||
| } | |||||
| $results[] = array( | |||||
| 'type' => '.', | |||||
| 'text' => null, | |||||
| ); | |||||
| if (!$is_last) { | |||||
| $results[] = array( | |||||
| 'type' => '=', | |||||
| 'text' => last($pieces), | |||||
| ); | |||||
| } | |||||
| break; | |||||
| default: | |||||
| $results[] = $part; | |||||
| break; | |||||
| } | |||||
| } | |||||
| return $results; | |||||
| } | |||||
| public function reorderParts() { | |||||
| // Reorder sequences of removed and added sections to put all the "-" | |||||
| // parts together first, then all the "+" parts together. This produces | |||||
| // a more human-readable result than intermingling them. | |||||
| $o_run = array(); | |||||
| $n_run = array(); | |||||
| $result = array(); | |||||
| foreach ($this->parts as $part) { | |||||
| $type = $part['type']; | |||||
| switch ($type) { | |||||
| case '-': | |||||
| $o_run[] = $part; | |||||
| break; | |||||
| case '+': | |||||
| $n_run[] = $part; | |||||
| break; | |||||
| default: | |||||
| if ($o_run || $n_run) { | |||||
| foreach ($this->combineRuns($o_run, $n_run) as $merged_part) { | |||||
| $result[] = $merged_part; | |||||
| } | |||||
| $o_run = array(); | |||||
| $n_run = array(); | |||||
| } | |||||
| $result[] = $part; | |||||
| break; | |||||
| } | |||||
| } | |||||
| if ($o_run || $n_run) { | |||||
| foreach ($this->combineRuns($o_run, $n_run) as $part) { | |||||
| $result[] = $part; | |||||
| } | |||||
| } | |||||
| // Now, combine consecuitive runs of the same type of change (like a | |||||
| // series of "-" parts) into a single run. | |||||
| $combined = array(); | |||||
| $last = null; | |||||
| $last_text = null; | |||||
| foreach ($result as $part) { | |||||
| $type = $part['type']; | |||||
| if ($last !== $type) { | |||||
| if ($last !== null) { | |||||
| $combined[] = array( | |||||
| 'type' => $last, | |||||
| 'text' => $last_text, | |||||
| ); | |||||
| } | |||||
| $last_text = null; | |||||
| $last = $type; | |||||
| } | |||||
| $last_text .= $part['text']; | |||||
| } | |||||
| if ($last_text !== null) { | |||||
| $combined[] = array( | |||||
| 'type' => $last, | |||||
| 'text' => $last_text, | |||||
| ); | |||||
| } | |||||
| $this->parts = $combined; | |||||
| return $this; | |||||
| } | |||||
| private function combineRuns($o_run, $n_run) { | |||||
| $o_merge = $this->mergeParts($o_run); | |||||
| $n_merge = $this->mergeParts($n_run); | |||||
| // When removed and added blocks share a prefix or suffix, we sometimes | |||||
| // want to count it as unchanged (for example, if it is whitespace) but | |||||
| // sometimes want to count it as changed (for example, if it is a word | |||||
| // suffix like "ing"). Find common prefixes and suffixes of these layout | |||||
| // characters and emit them as "=" (unchanged) blocks. | |||||
| $layout_characters = array( | |||||
| ' ' => true, | |||||
| "\n" => true, | |||||
| '.' => true, | |||||
| '!' => true, | |||||
| ',' => true, | |||||
| '?' => true, | |||||
| ']' => true, | |||||
| '[' => true, | |||||
| '(' => true, | |||||
| ')' => true, | |||||
| '<' => true, | |||||
| '>' => true, | |||||
| ); | |||||
| $o_text = $o_merge['text']; | |||||
| $n_text = $n_merge['text']; | |||||
| $o_len = strlen($o_text); | |||||
| $n_len = strlen($n_text); | |||||
| $min_len = min($o_len, $n_len); | |||||
| $prefix_len = 0; | |||||
| for ($pos = 0; $pos < $min_len; $pos++) { | |||||
| $o = $o_text[$pos]; | |||||
| $n = $n_text[$pos]; | |||||
| if ($o !== $n) { | |||||
| break; | |||||
| } | |||||
| if (empty($layout_characters[$o])) { | |||||
| break; | |||||
| } | |||||
| $prefix_len++; | |||||
| } | |||||
| $suffix_len = 0; | |||||
| for ($pos = 0; $pos < ($min_len - $prefix_len); $pos++) { | |||||
| $o = $o_text[$o_len - ($pos + 1)]; | |||||
| $n = $n_text[$n_len - ($pos + 1)]; | |||||
| if ($o !== $n) { | |||||
| break; | |||||
| } | |||||
| if (empty($layout_characters[$o])) { | |||||
| break; | |||||
| } | |||||
| $suffix_len++; | |||||
| } | |||||
| $results = array(); | |||||
| if ($prefix_len) { | |||||
| $results[] = array( | |||||
| 'type' => '=', | |||||
| 'text' => substr($o_text, 0, $prefix_len), | |||||
| ); | |||||
| } | |||||
| if ($prefix_len < $o_len) { | |||||
| $results[] = array( | |||||
| 'type' => '-', | |||||
| 'text' => substr( | |||||
| $o_text, | |||||
| $prefix_len, | |||||
| $o_len - $prefix_len - $suffix_len), | |||||
| ); | |||||
| } | |||||
| if ($prefix_len < $n_len) { | |||||
| $results[] = array( | |||||
| 'type' => '+', | |||||
| 'text' => substr( | |||||
| $n_text, | |||||
| $prefix_len, | |||||
| $n_len - $prefix_len - $suffix_len), | |||||
| ); | |||||
| } | |||||
| if ($suffix_len) { | |||||
| $results[] = array( | |||||
| 'type' => '=', | |||||
| 'text' => substr($o_text, -$suffix_len), | |||||
| ); | |||||
| } | |||||
| return $results; | |||||
| } | |||||
| private function mergeParts(array $parts) { | |||||
| $text = ''; | |||||
| $type = null; | |||||
| foreach ($parts as $part) { | |||||
| $part_type = $part['type']; | |||||
| if ($type === null) { | |||||
| $type = $part_type; | |||||
| } | |||||
| if ($type !== $part_type) { | |||||
| throw new Exception(pht('Can not merge parts of dissimilar types!')); | |||||
| } | |||||
| $text .= $part['text']; | |||||
| } | |||||
| return array( | |||||
| 'type' => $type, | |||||
| 'text' => $text, | |||||
| ); | |||||
| } | |||||
| private function splitTextForSummary($text) { | |||||
| $matches = null; | |||||
| $ok = preg_match('/^(\n*[^\n]+)\n/', $text, $matches); | |||||
| if (!$ok) { | |||||
| return array($text); | |||||
| } | |||||
| $head = $matches[1]; | |||||
| $text = substr($text, strlen($head)); | |||||
| $ok = preg_match('/\n([^\n]+\n*)\z/', $text, $matches); | |||||
| if (!$ok) { | |||||
| return array($text); | |||||
| } | |||||
| $last = $matches[1]; | |||||
| $text = substr($text, 0, -strlen($last)); | |||||
| if (!strlen(trim($text))) { | |||||
| return array($head, $last); | |||||
| } else { | |||||
| return array($head, $text, $last); | |||||
| } | |||||
| } | |||||
| } | |||||