balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-text-diff-renderer-table.php (16795B)


      1 <?php
      2 /**
      3  * Diff API: WP_Text_Diff_Renderer_Table class
      4  *
      5  * @package WordPress
      6  * @subpackage Diff
      7  * @since 4.7.0
      8  */
      9 
     10 /**
     11  * Table renderer to display the diff lines.
     12  *
     13  * @since 2.6.0
     14  * @uses Text_Diff_Renderer Extends
     15  */
     16 class WP_Text_Diff_Renderer_Table extends Text_Diff_Renderer {
     17 
     18 	/**
     19 	 * @see Text_Diff_Renderer::_leading_context_lines
     20 	 * @var int
     21 	 * @since 2.6.0
     22 	 */
     23 	public $_leading_context_lines = 10000;
     24 
     25 	/**
     26 	 * @see Text_Diff_Renderer::_trailing_context_lines
     27 	 * @var int
     28 	 * @since 2.6.0
     29 	 */
     30 	public $_trailing_context_lines = 10000;
     31 
     32 	/**
     33 	 * Threshold for when a diff should be saved or omitted.
     34 	 *
     35 	 * @var float
     36 	 * @since 2.6.0
     37 	 */
     38 	protected $_diff_threshold = 0.6;
     39 
     40 	/**
     41 	 * Inline display helper object name.
     42 	 *
     43 	 * @var string
     44 	 * @since 2.6.0
     45 	 */
     46 	protected $inline_diff_renderer = 'WP_Text_Diff_Renderer_inline';
     47 
     48 	/**
     49 	 * Should we show the split view or not
     50 	 *
     51 	 * @var string
     52 	 * @since 3.6.0
     53 	 */
     54 	protected $_show_split_view = true;
     55 
     56 	protected $compat_fields = array( '_show_split_view', 'inline_diff_renderer', '_diff_threshold' );
     57 
     58 	/**
     59 	 * Caches the output of count_chars() in compute_string_distance()
     60 	 *
     61 	 * @var array
     62 	 * @since 5.0.0
     63 	 */
     64 	protected $count_cache = array();
     65 
     66 	/**
     67 	 * Caches the difference calculation in compute_string_distance()
     68 	 *
     69 	 * @var array
     70 	 * @since 5.0.0
     71 	 */
     72 	protected $difference_cache = array();
     73 
     74 	/**
     75 	 * Constructor - Call parent constructor with params array.
     76 	 *
     77 	 * This will set class properties based on the key value pairs in the array.
     78 	 *
     79 	 * @since 2.6.0
     80 	 *
     81 	 * @param array $params
     82 	 */
     83 	public function __construct( $params = array() ) {
     84 		parent::__construct( $params );
     85 		if ( isset( $params['show_split_view'] ) ) {
     86 			$this->_show_split_view = $params['show_split_view'];
     87 		}
     88 	}
     89 
     90 	/**
     91 	 * @ignore
     92 	 *
     93 	 * @param string $header
     94 	 * @return string
     95 	 */
     96 	public function _startBlock( $header ) {
     97 		return '';
     98 	}
     99 
    100 	/**
    101 	 * @ignore
    102 	 *
    103 	 * @param array  $lines
    104 	 * @param string $prefix
    105 	 */
    106 	public function _lines( $lines, $prefix = ' ' ) {
    107 	}
    108 
    109 	/**
    110 	 * @ignore
    111 	 *
    112 	 * @param string $line HTML-escape the value.
    113 	 * @return string
    114 	 */
    115 	public function addedLine( $line ) {
    116 		return "<td class='diff-addedline'><span aria-hidden='true' class='dashicons dashicons-plus'></span><span class='screen-reader-text'>" . __( 'Added:' ) . " </span>{$line}</td>";
    117 
    118 	}
    119 
    120 	/**
    121 	 * @ignore
    122 	 *
    123 	 * @param string $line HTML-escape the value.
    124 	 * @return string
    125 	 */
    126 	public function deletedLine( $line ) {
    127 		return "<td class='diff-deletedline'><span aria-hidden='true' class='dashicons dashicons-minus'></span><span class='screen-reader-text'>" . __( 'Deleted:' ) . " </span>{$line}</td>";
    128 	}
    129 
    130 	/**
    131 	 * @ignore
    132 	 *
    133 	 * @param string $line HTML-escape the value.
    134 	 * @return string
    135 	 */
    136 	public function contextLine( $line ) {
    137 		return "<td class='diff-context'><span class='screen-reader-text'>" . __( 'Unchanged:' ) . " </span>{$line}</td>";
    138 	}
    139 
    140 	/**
    141 	 * @ignore
    142 	 *
    143 	 * @return string
    144 	 */
    145 	public function emptyLine() {
    146 		return '<td>&nbsp;</td>';
    147 	}
    148 
    149 	/**
    150 	 * @ignore
    151 	 *
    152 	 * @param array $lines
    153 	 * @param bool  $encode
    154 	 * @return string
    155 	 */
    156 	public function _added( $lines, $encode = true ) {
    157 		$r = '';
    158 		foreach ( $lines as $line ) {
    159 			if ( $encode ) {
    160 				$processed_line = htmlspecialchars( $line );
    161 
    162 				/**
    163 				 * Contextually filters a diffed line.
    164 				 *
    165 				 * Filters TextDiff processing of diffed line. By default, diffs are processed with
    166 				 * htmlspecialchars. Use this filter to remove or change the processing. Passes a context
    167 				 * indicating if the line is added, deleted or unchanged.
    168 				 *
    169 				 * @since 4.1.0
    170 				 *
    171 				 * @param string $processed_line The processed diffed line.
    172 				 * @param string $line           The unprocessed diffed line.
    173 				 * @param string $context        The line context. Values are 'added', 'deleted' or 'unchanged'.
    174 				 */
    175 				$line = apply_filters( 'process_text_diff_html', $processed_line, $line, 'added' );
    176 			}
    177 
    178 			if ( $this->_show_split_view ) {
    179 				$r .= '<tr>' . $this->emptyLine() . $this->addedLine( $line ) . "</tr>\n";
    180 			} else {
    181 				$r .= '<tr>' . $this->addedLine( $line ) . "</tr>\n";
    182 			}
    183 		}
    184 		return $r;
    185 	}
    186 
    187 	/**
    188 	 * @ignore
    189 	 *
    190 	 * @param array $lines
    191 	 * @param bool  $encode
    192 	 * @return string
    193 	 */
    194 	public function _deleted( $lines, $encode = true ) {
    195 		$r = '';
    196 		foreach ( $lines as $line ) {
    197 			if ( $encode ) {
    198 				$processed_line = htmlspecialchars( $line );
    199 
    200 				/** This filter is documented in wp-includes/wp-diff.php */
    201 				$line = apply_filters( 'process_text_diff_html', $processed_line, $line, 'deleted' );
    202 			}
    203 			if ( $this->_show_split_view ) {
    204 				$r .= '<tr>' . $this->deletedLine( $line ) . $this->emptyLine() . "</tr>\n";
    205 			} else {
    206 				$r .= '<tr>' . $this->deletedLine( $line ) . "</tr>\n";
    207 			}
    208 		}
    209 		return $r;
    210 	}
    211 
    212 	/**
    213 	 * @ignore
    214 	 *
    215 	 * @param array $lines
    216 	 * @param bool  $encode
    217 	 * @return string
    218 	 */
    219 	public function _context( $lines, $encode = true ) {
    220 		$r = '';
    221 		foreach ( $lines as $line ) {
    222 			if ( $encode ) {
    223 				$processed_line = htmlspecialchars( $line );
    224 
    225 				/** This filter is documented in wp-includes/wp-diff.php */
    226 				$line = apply_filters( 'process_text_diff_html', $processed_line, $line, 'unchanged' );
    227 			}
    228 			if ( $this->_show_split_view ) {
    229 				$r .= '<tr>' . $this->contextLine( $line ) . $this->contextLine( $line ) . "</tr>\n";
    230 			} else {
    231 				$r .= '<tr>' . $this->contextLine( $line ) . "</tr>\n";
    232 			}
    233 		}
    234 		return $r;
    235 	}
    236 
    237 	/**
    238 	 * Process changed lines to do word-by-word diffs for extra highlighting.
    239 	 *
    240 	 * (TRAC style) sometimes these lines can actually be deleted or added rows.
    241 	 * We do additional processing to figure that out
    242 	 *
    243 	 * @since 2.6.0
    244 	 *
    245 	 * @param array $orig
    246 	 * @param array $final
    247 	 * @return string
    248 	 */
    249 	public function _changed( $orig, $final ) {
    250 		$r = '';
    251 
    252 		/*
    253 		 * Does the aforementioned additional processing:
    254 		 * *_matches tell what rows are "the same" in orig and final. Those pairs will be diffed to get word changes.
    255 		 * - match is numeric: an index in other column.
    256 		 * - match is 'X': no match. It is a new row.
    257 		 * *_rows are column vectors for the orig column and the final column.
    258 		 * - row >= 0: an indix of the $orig or $final array.
    259 		 * - row < 0: a blank row for that column.
    260 		 */
    261 		list($orig_matches, $final_matches, $orig_rows, $final_rows) = $this->interleave_changed_lines( $orig, $final );
    262 
    263 		// These will hold the word changes as determined by an inline diff.
    264 		$orig_diffs  = array();
    265 		$final_diffs = array();
    266 
    267 		// Compute word diffs for each matched pair using the inline diff.
    268 		foreach ( $orig_matches as $o => $f ) {
    269 			if ( is_numeric( $o ) && is_numeric( $f ) ) {
    270 				$text_diff = new Text_Diff( 'auto', array( array( $orig[ $o ] ), array( $final[ $f ] ) ) );
    271 				$renderer  = new $this->inline_diff_renderer;
    272 				$diff      = $renderer->render( $text_diff );
    273 
    274 				// If they're too different, don't include any <ins> or <del>'s.
    275 				if ( preg_match_all( '!(<ins>.*?</ins>|<del>.*?</del>)!', $diff, $diff_matches ) ) {
    276 					// Length of all text between <ins> or <del>.
    277 					$stripped_matches = strlen( strip_tags( implode( ' ', $diff_matches[0] ) ) );
    278 					// Since we count length of text between <ins> or <del> (instead of picking just one),
    279 					// we double the length of chars not in those tags.
    280 					$stripped_diff = strlen( strip_tags( $diff ) ) * 2 - $stripped_matches;
    281 					$diff_ratio    = $stripped_matches / $stripped_diff;
    282 					if ( $diff_ratio > $this->_diff_threshold ) {
    283 						continue; // Too different. Don't save diffs.
    284 					}
    285 				}
    286 
    287 				// Un-inline the diffs by removing <del> or <ins>.
    288 				$orig_diffs[ $o ]  = preg_replace( '|<ins>.*?</ins>|', '', $diff );
    289 				$final_diffs[ $f ] = preg_replace( '|<del>.*?</del>|', '', $diff );
    290 			}
    291 		}
    292 
    293 		foreach ( array_keys( $orig_rows ) as $row ) {
    294 			// Both columns have blanks. Ignore them.
    295 			if ( $orig_rows[ $row ] < 0 && $final_rows[ $row ] < 0 ) {
    296 				continue;
    297 			}
    298 
    299 			// If we have a word based diff, use it. Otherwise, use the normal line.
    300 			if ( isset( $orig_diffs[ $orig_rows[ $row ] ] ) ) {
    301 				$orig_line = $orig_diffs[ $orig_rows[ $row ] ];
    302 			} elseif ( isset( $orig[ $orig_rows[ $row ] ] ) ) {
    303 				$orig_line = htmlspecialchars( $orig[ $orig_rows[ $row ] ] );
    304 			} else {
    305 				$orig_line = '';
    306 			}
    307 
    308 			if ( isset( $final_diffs[ $final_rows[ $row ] ] ) ) {
    309 				$final_line = $final_diffs[ $final_rows[ $row ] ];
    310 			} elseif ( isset( $final[ $final_rows[ $row ] ] ) ) {
    311 				$final_line = htmlspecialchars( $final[ $final_rows[ $row ] ] );
    312 			} else {
    313 				$final_line = '';
    314 			}
    315 
    316 			if ( $orig_rows[ $row ] < 0 ) { // Orig is blank. This is really an added row.
    317 				$r .= $this->_added( array( $final_line ), false );
    318 			} elseif ( $final_rows[ $row ] < 0 ) { // Final is blank. This is really a deleted row.
    319 				$r .= $this->_deleted( array( $orig_line ), false );
    320 			} else { // A true changed row.
    321 				if ( $this->_show_split_view ) {
    322 					$r .= '<tr>' . $this->deletedLine( $orig_line ) . $this->addedLine( $final_line ) . "</tr>\n";
    323 				} else {
    324 					$r .= '<tr>' . $this->deletedLine( $orig_line ) . '</tr><tr>' . $this->addedLine( $final_line ) . "</tr>\n";
    325 				}
    326 			}
    327 		}
    328 
    329 		return $r;
    330 	}
    331 
    332 	/**
    333 	 * Takes changed blocks and matches which rows in orig turned into which rows in final.
    334 	 *
    335 	 * @since 2.6.0
    336 	 *
    337 	 * @param array $orig  Lines of the original version of the text.
    338 	 * @param array $final Lines of the final version of the text.
    339 	 * @return array {
    340 	 *     Array containing results of comparing the original text to the final text.
    341 	 *
    342 	 *     @type array $orig_matches  Associative array of original matches. Index == row
    343 	 *                                number of `$orig`, value == corresponding row number
    344 	 *                                of that same line in `$final` or 'x' if there is no
    345 	 *                                corresponding row (indicating it is a deleted line).
    346 	 *     @type array $final_matches Associative array of final matches. Index == row
    347 	 *                                number of `$final`, value == corresponding row number
    348 	 *                                of that same line in `$orig` or 'x' if there is no
    349 	 *                                corresponding row (indicating it is a new line).
    350 	 *     @type array $orig_rows     Associative array of interleaved rows of `$orig` with
    351 	 *                                blanks to keep matches aligned with side-by-side diff
    352 	 *                                of `$final`. A value >= 0 corresponds to index of `$orig`.
    353 	 *                                Value < 0 indicates a blank row.
    354 	 *     @type array $final_rows    Associative array of interleaved rows of `$final` with
    355 	 *                                blanks to keep matches aligned with side-by-side diff
    356 	 *                                of `$orig`. A value >= 0 corresponds to index of `$final`.
    357 	 *                                Value < 0 indicates a blank row.
    358 	 * }
    359 	 */
    360 	public function interleave_changed_lines( $orig, $final ) {
    361 
    362 		// Contains all pairwise string comparisons. Keys are such that this need only be a one dimensional array.
    363 		$matches = array();
    364 		foreach ( array_keys( $orig ) as $o ) {
    365 			foreach ( array_keys( $final ) as $f ) {
    366 				$matches[ "$o,$f" ] = $this->compute_string_distance( $orig[ $o ], $final[ $f ] );
    367 			}
    368 		}
    369 		asort( $matches ); // Order by string distance.
    370 
    371 		$orig_matches  = array();
    372 		$final_matches = array();
    373 
    374 		foreach ( $matches as $keys => $difference ) {
    375 			list($o, $f) = explode( ',', $keys );
    376 			$o           = (int) $o;
    377 			$f           = (int) $f;
    378 
    379 			// Already have better matches for these guys.
    380 			if ( isset( $orig_matches[ $o ] ) && isset( $final_matches[ $f ] ) ) {
    381 				continue;
    382 			}
    383 
    384 			// First match for these guys. Must be best match.
    385 			if ( ! isset( $orig_matches[ $o ] ) && ! isset( $final_matches[ $f ] ) ) {
    386 				$orig_matches[ $o ]  = $f;
    387 				$final_matches[ $f ] = $o;
    388 				continue;
    389 			}
    390 
    391 			// Best match of this final is already taken? Must mean this final is a new row.
    392 			if ( isset( $orig_matches[ $o ] ) ) {
    393 				$final_matches[ $f ] = 'x';
    394 			} elseif ( isset( $final_matches[ $f ] ) ) {
    395 				// Best match of this orig is already taken? Must mean this orig is a deleted row.
    396 				$orig_matches[ $o ] = 'x';
    397 			}
    398 		}
    399 
    400 		// We read the text in this order.
    401 		ksort( $orig_matches );
    402 		ksort( $final_matches );
    403 
    404 		// Stores rows and blanks for each column.
    405 		$orig_rows      = array_keys( $orig_matches );
    406 		$orig_rows_copy = $orig_rows;
    407 		$final_rows     = array_keys( $final_matches );
    408 
    409 		// Interleaves rows with blanks to keep matches aligned.
    410 		// We may end up with some extraneous blank rows, but we'll just ignore them later.
    411 		foreach ( $orig_rows_copy as $orig_row ) {
    412 			$final_pos = array_search( $orig_matches[ $orig_row ], $final_rows, true );
    413 			$orig_pos  = (int) array_search( $orig_row, $orig_rows, true );
    414 
    415 			if ( false === $final_pos ) { // This orig is paired with a blank final.
    416 				array_splice( $final_rows, $orig_pos, 0, -1 );
    417 			} elseif ( $final_pos < $orig_pos ) { // This orig's match is up a ways. Pad final with blank rows.
    418 				$diff_array = range( -1, $final_pos - $orig_pos );
    419 				array_splice( $final_rows, $orig_pos, 0, $diff_array );
    420 			} elseif ( $final_pos > $orig_pos ) { // This orig's match is down a ways. Pad orig with blank rows.
    421 				$diff_array = range( -1, $orig_pos - $final_pos );
    422 				array_splice( $orig_rows, $orig_pos, 0, $diff_array );
    423 			}
    424 		}
    425 
    426 		// Pad the ends with blank rows if the columns aren't the same length.
    427 		$diff_count = count( $orig_rows ) - count( $final_rows );
    428 		if ( $diff_count < 0 ) {
    429 			while ( $diff_count < 0 ) {
    430 				array_push( $orig_rows, $diff_count++ );
    431 			}
    432 		} elseif ( $diff_count > 0 ) {
    433 			$diff_count = -1 * $diff_count;
    434 			while ( $diff_count < 0 ) {
    435 				array_push( $final_rows, $diff_count++ );
    436 			}
    437 		}
    438 
    439 		return array( $orig_matches, $final_matches, $orig_rows, $final_rows );
    440 	}
    441 
    442 	/**
    443 	 * Computes a number that is intended to reflect the "distance" between two strings.
    444 	 *
    445 	 * @since 2.6.0
    446 	 *
    447 	 * @param string $string1
    448 	 * @param string $string2
    449 	 * @return int
    450 	 */
    451 	public function compute_string_distance( $string1, $string2 ) {
    452 		// Use an md5 hash of the strings for a count cache, as it's fast to generate, and collisions aren't a concern.
    453 		$count_key1 = md5( $string1 );
    454 		$count_key2 = md5( $string2 );
    455 
    456 		// Cache vectors containing character frequency for all chars in each string.
    457 		if ( ! isset( $this->count_cache[ $count_key1 ] ) ) {
    458 			$this->count_cache[ $count_key1 ] = count_chars( $string1 );
    459 		}
    460 		if ( ! isset( $this->count_cache[ $count_key2 ] ) ) {
    461 			$this->count_cache[ $count_key2 ] = count_chars( $string2 );
    462 		}
    463 
    464 		$chars1 = $this->count_cache[ $count_key1 ];
    465 		$chars2 = $this->count_cache[ $count_key2 ];
    466 
    467 		$difference_key = md5( implode( ',', $chars1 ) . ':' . implode( ',', $chars2 ) );
    468 		if ( ! isset( $this->difference_cache[ $difference_key ] ) ) {
    469 			// L1-norm of difference vector.
    470 			$this->difference_cache[ $difference_key ] = array_sum( array_map( array( $this, 'difference' ), $chars1, $chars2 ) );
    471 		}
    472 
    473 		$difference = $this->difference_cache[ $difference_key ];
    474 
    475 		// $string1 has zero length? Odd. Give huge penalty by not dividing.
    476 		if ( ! $string1 ) {
    477 			return $difference;
    478 		}
    479 
    480 		// Return distance per character (of string1).
    481 		return $difference / strlen( $string1 );
    482 	}
    483 
    484 	/**
    485 	 * @ignore
    486 	 * @since 2.6.0
    487 	 *
    488 	 * @param int $a
    489 	 * @param int $b
    490 	 * @return int
    491 	 */
    492 	public function difference( $a, $b ) {
    493 		return abs( $a - $b );
    494 	}
    495 
    496 	/**
    497 	 * Make private properties readable for backward compatibility.
    498 	 *
    499 	 * @since 4.0.0
    500 	 *
    501 	 * @param string $name Property to get.
    502 	 * @return mixed Property.
    503 	 */
    504 	public function __get( $name ) {
    505 		if ( in_array( $name, $this->compat_fields, true ) ) {
    506 			return $this->$name;
    507 		}
    508 	}
    509 
    510 	/**
    511 	 * Make private properties settable for backward compatibility.
    512 	 *
    513 	 * @since 4.0.0
    514 	 *
    515 	 * @param string $name  Property to check if set.
    516 	 * @param mixed  $value Property value.
    517 	 * @return mixed Newly-set property.
    518 	 */
    519 	public function __set( $name, $value ) {
    520 		if ( in_array( $name, $this->compat_fields, true ) ) {
    521 			return $this->$name = $value;
    522 		}
    523 	}
    524 
    525 	/**
    526 	 * Make private properties checkable for backward compatibility.
    527 	 *
    528 	 * @since 4.0.0
    529 	 *
    530 	 * @param string $name Property to check if set.
    531 	 * @return bool Whether the property is set.
    532 	 */
    533 	public function __isset( $name ) {
    534 		if ( in_array( $name, $this->compat_fields, true ) ) {
    535 			return isset( $this->$name );
    536 		}
    537 	}
    538 
    539 	/**
    540 	 * Make private properties un-settable for backward compatibility.
    541 	 *
    542 	 * @since 4.0.0
    543 	 *
    544 	 * @param string $name Property to unset.
    545 	 */
    546 	public function __unset( $name ) {
    547 		if ( in_array( $name, $this->compat_fields, true ) ) {
    548 			unset( $this->$name );
    549 		}
    550 	}
    551 }