home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / class-wp-text-diff-renderer-table.php < prev    next >
Encoding:
PHP Script  |  2017-07-26  |  14.8 KB  |  511 lines

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