[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-text-diff-renderer-table.php (source)

   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->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() . $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->emptyLine() . $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->emptyLine() . $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  }


Generated : Sat Oct 24 08:20:01 2020 Cross-referenced by PHPXref