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


Generated : Tue Apr 16 08:20:01 2024 Cross-referenced by PHPXref