Path: blob/master/src/infrastructure/diff/prose/PhutilProseDiff.php
12242 views
<?php12final class PhutilProseDiff extends Phobject {34private $parts = array();56public function addPart($type, $text) {7$this->parts[] = array(8'type' => $type,9'text' => $text,10);11return $this;12}1314public function getParts() {15return $this->parts;16}1718/**19* Get diff parts, but replace large blocks of unchanged text with "."20* parts representing missing context.21*/22public function getSummaryParts() {23$parts = $this->getParts();2425$head_key = head_key($parts);26$last_key = last_key($parts);2728$results = array();29foreach ($parts as $key => $part) {30$is_head = ($key == $head_key);31$is_last = ($key == $last_key);3233switch ($part['type']) {34case '=':35$pieces = $this->splitTextForSummary($part['text']);3637if ($is_head || $is_last) {38$need = 2;39} else {40$need = 3;41}4243// We don't have enough pieces to omit anything, so just continue.44if (count($pieces) < $need) {45$results[] = $part;46break;47}4849if (!$is_head) {50$results[] = array(51'type' => '=',52'text' => head($pieces),53);54}5556$results[] = array(57'type' => '.',58'text' => null,59);6061if (!$is_last) {62$results[] = array(63'type' => '=',64'text' => last($pieces),65);66}67break;68default:69$results[] = $part;70break;71}72}7374return $results;75}767778public function reorderParts() {79// Reorder sequences of removed and added sections to put all the "-"80// parts together first, then all the "+" parts together. This produces81// a more human-readable result than intermingling them.8283$o_run = array();84$n_run = array();85$result = array();86foreach ($this->parts as $part) {87$type = $part['type'];88switch ($type) {89case '-':90$o_run[] = $part;91break;92case '+':93$n_run[] = $part;94break;95default:96if ($o_run || $n_run) {97foreach ($this->combineRuns($o_run, $n_run) as $merged_part) {98$result[] = $merged_part;99}100$o_run = array();101$n_run = array();102}103$result[] = $part;104break;105}106}107108if ($o_run || $n_run) {109foreach ($this->combineRuns($o_run, $n_run) as $part) {110$result[] = $part;111}112}113114// Now, combine consecuitive runs of the same type of change (like a115// series of "-" parts) into a single run.116$combined = array();117118$last = null;119$last_text = null;120foreach ($result as $part) {121$type = $part['type'];122123if ($last !== $type) {124if ($last !== null) {125$combined[] = array(126'type' => $last,127'text' => $last_text,128);129}130$last_text = null;131$last = $type;132}133134$last_text .= $part['text'];135}136137if ($last_text !== null) {138$combined[] = array(139'type' => $last,140'text' => $last_text,141);142}143144$this->parts = $combined;145146return $this;147}148149private function combineRuns($o_run, $n_run) {150$o_merge = $this->mergeParts($o_run);151$n_merge = $this->mergeParts($n_run);152153// When removed and added blocks share a prefix or suffix, we sometimes154// want to count it as unchanged (for example, if it is whitespace) but155// sometimes want to count it as changed (for example, if it is a word156// suffix like "ing"). Find common prefixes and suffixes of these layout157// characters and emit them as "=" (unchanged) blocks.158159$layout_characters = array(160' ' => true,161"\n" => true,162'.' => true,163'!' => true,164',' => true,165'?' => true,166']' => true,167'[' => true,168'(' => true,169')' => true,170'<' => true,171'>' => true,172);173174$o_text = $o_merge['text'];175$n_text = $n_merge['text'];176$o_len = strlen($o_text);177$n_len = strlen($n_text);178$min_len = min($o_len, $n_len);179180$prefix_len = 0;181for ($pos = 0; $pos < $min_len; $pos++) {182$o = $o_text[$pos];183$n = $n_text[$pos];184if ($o !== $n) {185break;186}187if (empty($layout_characters[$o])) {188break;189}190$prefix_len++;191}192193$suffix_len = 0;194for ($pos = 0; $pos < ($min_len - $prefix_len); $pos++) {195$o = $o_text[$o_len - ($pos + 1)];196$n = $n_text[$n_len - ($pos + 1)];197if ($o !== $n) {198break;199}200if (empty($layout_characters[$o])) {201break;202}203$suffix_len++;204}205206$results = array();207208if ($prefix_len) {209$results[] = array(210'type' => '=',211'text' => substr($o_text, 0, $prefix_len),212);213}214215if ($prefix_len < $o_len) {216$results[] = array(217'type' => '-',218'text' => substr(219$o_text,220$prefix_len,221$o_len - $prefix_len - $suffix_len),222);223}224225if ($prefix_len < $n_len) {226$results[] = array(227'type' => '+',228'text' => substr(229$n_text,230$prefix_len,231$n_len - $prefix_len - $suffix_len),232);233}234235if ($suffix_len) {236$results[] = array(237'type' => '=',238'text' => substr($o_text, -$suffix_len),239);240}241242return $results;243}244245private function mergeParts(array $parts) {246$text = '';247$type = null;248foreach ($parts as $part) {249$part_type = $part['type'];250if ($type === null) {251$type = $part_type;252}253if ($type !== $part_type) {254throw new Exception(pht('Can not merge parts of dissimilar types!'));255}256$text .= $part['text'];257}258259return array(260'type' => $type,261'text' => $text,262);263}264265private function splitTextForSummary($text) {266$matches = null;267268$ok = preg_match('/^(\n*[^\n]+)\n/', $text, $matches);269if (!$ok) {270return array($text);271}272273$head = $matches[1];274$text = substr($text, strlen($head));275276$ok = preg_match('/\n([^\n]+\n*)\z/', $text, $matches);277if (!$ok) {278return array($text);279}280281$last = $matches[1];282$text = substr($text, 0, -strlen($last));283284if (!strlen(trim($text))) {285return array($head, $last);286} else {287return array($head, $text, $last);288}289}290291}292293294