[ Index ]

PHP Cross Reference of WordPress

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 null            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          // Does the aforementioned additional processing
 253          // *_matches tell what rows are "the same" in orig and final. Those pairs will be diffed to get word changes
 254          //    match is numeric: an index in other column
 255          //    match is 'X': no match. It is a new row
 256          // *_rows are column vectors for the orig column and the final column.
 257          //    row >= 0: an indix of the $orig or $final array
 258          //    row  < 0: a blank row for that column
 259          list($orig_matches, $final_matches, $orig_rows, $final_rows) = $this->interleave_changed_lines( $orig, $final );
 260  
 261          // These will hold the word changes as determined by an inline diff
 262          $orig_diffs  = array();
 263          $final_diffs = array();
 264  
 265          // Compute word diffs for each matched pair using the inline diff
 266          foreach ( $orig_matches as $o => $f ) {
 267              if ( is_numeric( $o ) && is_numeric( $f ) ) {
 268                  $text_diff = new Text_Diff( 'auto', array( array( $orig[ $o ] ), array( $final[ $f ] ) ) );
 269                  $renderer  = new $this->inline_diff_renderer;
 270                  $diff      = $renderer->render( $text_diff );
 271  
 272                  // If they're too different, don't include any <ins> or <dels>
 273                  if ( preg_match_all( '!(<ins>.*?</ins>|<del>.*?</del>)!', $diff, $diff_matches ) ) {
 274                      // length of all text between <ins> or <del>
 275                      $stripped_matches = strlen( strip_tags( join( ' ', $diff_matches[0] ) ) );
 276                      // since we count lengith of text between <ins> or <del> (instead of picking just one),
 277                      //    we double the length of chars not in those tags.
 278                      $stripped_diff = strlen( strip_tags( $diff ) ) * 2 - $stripped_matches;
 279                      $diff_ratio    = $stripped_matches / $stripped_diff;
 280                      if ( $diff_ratio > $this->_diff_threshold ) {
 281                          continue; // Too different. Don't save diffs.
 282                      }
 283                  }
 284  
 285                  // Un-inline the diffs by removing del or ins
 286                  $orig_diffs[ $o ]  = preg_replace( '|<ins>.*?</ins>|', '', $diff );
 287                  $final_diffs[ $f ] = preg_replace( '|<del>.*?</del>|', '', $diff );
 288              }
 289          }
 290  
 291          foreach ( array_keys( $orig_rows ) as $row ) {
 292              // Both columns have blanks. Ignore them.
 293              if ( $orig_rows[ $row ] < 0 && $final_rows[ $row ] < 0 ) {
 294                  continue;
 295              }
 296  
 297              // If we have a word based diff, use it. Otherwise, use the normal line.
 298              if ( isset( $orig_diffs[ $orig_rows[ $row ] ] ) ) {
 299                  $orig_line = $orig_diffs[ $orig_rows[ $row ] ];
 300              } elseif ( isset( $orig[ $orig_rows[ $row ] ] ) ) {
 301                  $orig_line = htmlspecialchars( $orig[ $orig_rows[ $row ] ] );
 302              } else {
 303                  $orig_line = '';
 304              }
 305  
 306              if ( isset( $final_diffs[ $final_rows[ $row ] ] ) ) {
 307                  $final_line = $final_diffs[ $final_rows[ $row ] ];
 308              } elseif ( isset( $final[ $final_rows[ $row ] ] ) ) {
 309                  $final_line = htmlspecialchars( $final[ $final_rows[ $row ] ] );
 310              } else {
 311                  $final_line = '';
 312              }
 313  
 314              if ( $orig_rows[ $row ] < 0 ) { // Orig is blank. This is really an added row.
 315                  $r .= $this->_added( array( $final_line ), false );
 316              } elseif ( $final_rows[ $row ] < 0 ) { // Final is blank. This is really a deleted row.
 317                  $r .= $this->_deleted( array( $orig_line ), false );
 318              } else { // A true changed row.
 319                  if ( $this->_show_split_view ) {
 320                      $r .= '<tr>' . $this->deletedLine( $orig_line ) . $this->emptyLine() . $this->addedLine( $final_line ) . "</tr>\n";
 321                  } else {
 322                      $r .= '<tr>' . $this->deletedLine( $orig_line ) . '</tr><tr>' . $this->addedLine( $final_line ) . "</tr>\n";
 323                  }
 324              }
 325          }
 326  
 327          return $r;
 328      }
 329  
 330      /**
 331       * Takes changed blocks and matches which rows in orig turned into which rows in final.
 332       *
 333       * @since 2.6.0
 334       *
 335       * @param array $orig  Lines of the original version of the text.
 336       * @param array $final Lines of the final version of the text.
 337       * @return array {
 338       *    Array containing results of comparing the original text to the final text.
 339       *
 340       *    @type array $orig_matches  Associative array of original matches. Index == row
 341       *                               number of `$orig`, value == corresponding row number
 342       *                               of that same line in `$final` or 'x' if there is no
 343       *                               corresponding row (indicating it is a deleted line).
 344       *    @type array $final_matches Associative array of final matches. Index == row
 345       *                               number of `$final`, value == corresponding row number
 346       *                               of that same line in `$orig` or 'x' if there is no
 347       *                               corresponding row (indicating it is a new line).
 348       *    @type array $orig_rows     Associative array of interleaved rows of `$orig` with
 349       *                               blanks to keep matches aligned with side-by-side diff
 350       *                               of `$final`. A value >= 0 corresponds to index of `$orig`.
 351       *                               Value < 0 indicates a blank row.
 352       *    @type array $final_rows    Associative array of interleaved rows of `$final` with
 353       *                               blanks to keep matches aligned with side-by-side diff
 354       *                               of `$orig`. A value >= 0 corresponds to index of `$final`.
 355       *                               Value < 0 indicates a blank row.
 356       * }
 357       */
 358  	public function interleave_changed_lines( $orig, $final ) {
 359  
 360          // Contains all pairwise string comparisons. Keys are such that this need only be a one dimensional array.
 361          $matches = array();
 362          foreach ( array_keys( $orig ) as $o ) {
 363              foreach ( array_keys( $final ) as $f ) {
 364                  $matches[ "$o,$f" ] = $this->compute_string_distance( $orig[ $o ], $final[ $f ] );
 365              }
 366          }
 367          asort( $matches ); // Order by string distance.
 368  
 369          $orig_matches  = array();
 370          $final_matches = array();
 371  
 372          foreach ( $matches as $keys => $difference ) {
 373              list($o, $f) = explode( ',', $keys );
 374              $o           = (int) $o;
 375              $f           = (int) $f;
 376  
 377              // Already have better matches for these guys
 378              if ( isset( $orig_matches[ $o ] ) && isset( $final_matches[ $f ] ) ) {
 379                  continue;
 380              }
 381  
 382              // First match for these guys. Must be best match
 383              if ( ! isset( $orig_matches[ $o ] ) && ! isset( $final_matches[ $f ] ) ) {
 384                  $orig_matches[ $o ]  = $f;
 385                  $final_matches[ $f ] = $o;
 386                  continue;
 387              }
 388  
 389              // Best match of this final is already taken?  Must mean this final is a new row.
 390              if ( isset( $orig_matches[ $o ] ) ) {
 391                  $final_matches[ $f ] = 'x';
 392              } elseif ( isset( $final_matches[ $f ] ) ) {
 393                  // Best match of this orig is already taken?  Must mean this orig is a deleted row.
 394                  $orig_matches[ $o ] = 'x';
 395              }
 396          }
 397  
 398          // We read the text in this order
 399          ksort( $orig_matches );
 400          ksort( $final_matches );
 401  
 402          // Stores rows and blanks for each column.
 403          $orig_rows      = array_keys( $orig_matches );
 404          $orig_rows_copy = $orig_rows;
 405          $final_rows     = array_keys( $final_matches );
 406  
 407          // Interleaves rows with blanks to keep matches aligned.
 408          // We may end up with some extraneous blank rows, but we'll just ignore them later.
 409          foreach ( $orig_rows_copy as $orig_row ) {
 410              $final_pos = array_search( $orig_matches[ $orig_row ], $final_rows, true );
 411              $orig_pos  = (int) array_search( $orig_row, $orig_rows, true );
 412  
 413              if ( false === $final_pos ) { // This orig is paired with a blank final.
 414                  array_splice( $final_rows, $orig_pos, 0, -1 );
 415              } elseif ( $final_pos < $orig_pos ) { // This orig's match is up a ways. Pad final with blank rows.
 416                  $diff_array = range( -1, $final_pos - $orig_pos );
 417                  array_splice( $final_rows, $orig_pos, 0, $diff_array );
 418              } elseif ( $final_pos > $orig_pos ) { // This orig's match is down a ways. Pad orig with blank rows.
 419                  $diff_array = range( -1, $orig_pos - $final_pos );
 420                  array_splice( $orig_rows, $orig_pos, 0, $diff_array );
 421              }
 422          }
 423  
 424          // Pad the ends with blank rows if the columns aren't the same length
 425          $diff_count = count( $orig_rows ) - count( $final_rows );
 426          if ( $diff_count < 0 ) {
 427              while ( $diff_count < 0 ) {
 428                  array_push( $orig_rows, $diff_count++ );
 429              }
 430          } elseif ( $diff_count > 0 ) {
 431              $diff_count = -1 * $diff_count;
 432              while ( $diff_count < 0 ) {
 433                  array_push( $final_rows, $diff_count++ );
 434              }
 435          }
 436  
 437          return array( $orig_matches, $final_matches, $orig_rows, $final_rows );
 438      }
 439  
 440      /**
 441       * Computes a number that is intended to reflect the "distance" between two strings.
 442       *
 443       * @since 2.6.0
 444       *
 445       * @param string $string1
 446       * @param string $string2
 447       * @return int
 448       */
 449  	public function compute_string_distance( $string1, $string2 ) {
 450          // Use an md5 hash of the strings for a count cache, as it's fast to generate, and collisions aren't a concern.
 451          $count_key1 = md5( $string1 );
 452          $count_key2 = md5( $string2 );
 453  
 454          // Cache vectors containing character frequency for all chars in each string.
 455          if ( ! isset( $this->count_cache[ $count_key1 ] ) ) {
 456              $this->count_cache[ $count_key1 ] = count_chars( $string1 );
 457          }
 458          if ( ! isset( $this->count_cache[ $count_key2 ] ) ) {
 459              $this->count_cache[ $count_key2 ] = count_chars( $string2 );
 460          }
 461  
 462          $chars1 = $this->count_cache[ $count_key1 ];
 463          $chars2 = $this->count_cache[ $count_key2 ];
 464  
 465          $difference_key = md5( implode( ',', $chars1 ) . ':' . implode( ',', $chars2 ) );
 466          if ( ! isset( $this->difference_cache[ $difference_key ] ) ) {
 467              // L1-norm of difference vector.
 468              $this->difference_cache[ $difference_key ] = array_sum( array_map( array( $this, 'difference' ), $chars1, $chars2 ) );
 469          }
 470  
 471          $difference = $this->difference_cache[ $difference_key ];
 472  
 473          // $string1 has zero length? Odd. Give huge penalty by not dividing.
 474          if ( ! $string1 ) {
 475              return $difference;
 476          }
 477  
 478          // Return distance per character (of string1).
 479          return $difference / strlen( $string1 );
 480      }
 481  
 482      /**
 483       * @ignore
 484       * @since 2.6.0
 485       *
 486       * @param int $a
 487       * @param int $b
 488       * @return int
 489       */
 490  	public function difference( $a, $b ) {
 491          return abs( $a - $b );
 492      }
 493  
 494      /**
 495       * Make private properties readable for backward compatibility.
 496       *
 497       * @since 4.0.0
 498       *
 499       * @param string $name Property to get.
 500       * @return mixed Property.
 501       */
 502  	public function __get( $name ) {
 503          if ( in_array( $name, $this->compat_fields ) ) {
 504              return $this->$name;
 505          }
 506      }
 507  
 508      /**
 509       * Make private properties settable for backward compatibility.
 510       *
 511       * @since 4.0.0
 512       *
 513       * @param string $name  Property to check if set.
 514       * @param mixed  $value Property value.
 515       * @return mixed Newly-set property.
 516       */
 517  	public function __set( $name, $value ) {
 518          if ( in_array( $name, $this->compat_fields ) ) {
 519              return $this->$name = $value;
 520          }
 521      }
 522  
 523      /**
 524       * Make private properties checkable for backward compatibility.
 525       *
 526       * @since 4.0.0
 527       *
 528       * @param string $name Property to check if set.
 529       * @return bool Whether the property is set.
 530       */
 531  	public function __isset( $name ) {
 532          if ( in_array( $name, $this->compat_fields ) ) {
 533              return isset( $this->$name );
 534          }
 535      }
 536  
 537      /**
 538       * Make private properties un-settable for backward compatibility.
 539       *
 540       * @since 4.0.0
 541       *
 542       * @param string $name Property to unset.
 543       */
 544  	public function __unset( $name ) {
 545          if ( in_array( $name, $this->compat_fields ) ) {
 546              unset( $this->$name );
 547          }
 548      }
 549  }


Generated: Tue Sep 17 01:00:03 2019 Cross-referenced by PHPXref 0.7.1