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


Generated : Fri Feb 21 08:20:01 2025 Cross-referenced by PHPXref