PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/blog/wp-includes/wp-diff.php

https://github.com/GCheung55/mootools-website
PHP | 477 lines | 182 code | 55 blank | 240 comment | 26 complexity | 286e1b0de35504c54f753cc4a57751f0 MD5 | raw file
  1. <?php
  2. /**
  3. * WordPress Diff bastard child of old MediaWiki Diff Formatter.
  4. *
  5. * Basically all that remains is the table structure and some method names.
  6. *
  7. * @package WordPress
  8. * @subpackage Diff
  9. */
  10. if ( !class_exists( 'Text_Diff' ) ) {
  11. /** Text_Diff class */
  12. require( dirname(__FILE__).'/Text/Diff.php' );
  13. /** Text_Diff_Renderer class */
  14. require( dirname(__FILE__).'/Text/Diff/Renderer.php' );
  15. /** Text_Diff_Renderer_inline class */
  16. require( dirname(__FILE__).'/Text/Diff/Renderer/inline.php' );
  17. }
  18. /**
  19. * Table renderer to display the diff lines.
  20. *
  21. * @since 2.6.0
  22. * @uses Text_Diff_Renderer Extends
  23. */
  24. class WP_Text_Diff_Renderer_Table extends Text_Diff_Renderer {
  25. /**
  26. * @see Text_Diff_Renderer::_leading_context_lines
  27. * @var int
  28. * @access protected
  29. * @since 2.6.0
  30. */
  31. var $_leading_context_lines = 10000;
  32. /**
  33. * @see Text_Diff_Renderer::_trailing_context_lines
  34. * @var int
  35. * @access protected
  36. * @since 2.6.0
  37. */
  38. var $_trailing_context_lines = 10000;
  39. /**
  40. * {@internal Missing Description}}
  41. *
  42. * @var float
  43. * @access protected
  44. * @since 2.6.0
  45. */
  46. var $_diff_threshold = 0.6;
  47. /**
  48. * Inline display helper object name.
  49. *
  50. * @var string
  51. * @access protected
  52. * @since 2.6.0
  53. */
  54. var $inline_diff_renderer = 'WP_Text_Diff_Renderer_inline';
  55. /**
  56. * Constructor - Call parent constructor with params array.
  57. *
  58. * This will set class properties based on the key value pairs in the array.
  59. *
  60. * @since 2.6.0
  61. *
  62. * @param array $params
  63. */
  64. function __construct( $params = array() ) {
  65. parent::__construct( $params );
  66. }
  67. /**
  68. * @ignore
  69. *
  70. * @param string $header
  71. * @return string
  72. */
  73. function _startBlock( $header ) {
  74. return '';
  75. }
  76. /**
  77. * @ignore
  78. *
  79. * @param array $lines
  80. * @param string $prefix
  81. */
  82. function _lines( $lines, $prefix=' ' ) {
  83. }
  84. /**
  85. * @ignore
  86. *
  87. * @param string $line HTML-escape the value.
  88. * @return string
  89. */
  90. function addedLine( $line ) {
  91. return "<td>+</td><td class='diff-addedline'>{$line}</td>";
  92. }
  93. /**
  94. * @ignore
  95. *
  96. * @param string $line HTML-escape the value.
  97. * @return string
  98. */
  99. function deletedLine( $line ) {
  100. return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
  101. }
  102. /**
  103. * @ignore
  104. *
  105. * @param string $line HTML-escape the value.
  106. * @return string
  107. */
  108. function contextLine( $line ) {
  109. return "<td> </td><td class='diff-context'>{$line}</td>";
  110. }
  111. /**
  112. * @ignore
  113. *
  114. * @return string
  115. */
  116. function emptyLine() {
  117. return '<td colspan="2">&nbsp;</td>';
  118. }
  119. /**
  120. * @ignore
  121. * @access private
  122. *
  123. * @param array $lines
  124. * @param bool $encode
  125. * @return string
  126. */
  127. function _added( $lines, $encode = true ) {
  128. $r = '';
  129. foreach ($lines as $line) {
  130. if ( $encode )
  131. $line = htmlspecialchars( $line );
  132. $r .= '<tr>' . $this->emptyLine() . $this->addedLine( $line ) . "</tr>\n";
  133. }
  134. return $r;
  135. }
  136. /**
  137. * @ignore
  138. * @access private
  139. *
  140. * @param array $lines
  141. * @param bool $encode
  142. * @return string
  143. */
  144. function _deleted( $lines, $encode = true ) {
  145. $r = '';
  146. foreach ($lines as $line) {
  147. if ( $encode )
  148. $line = htmlspecialchars( $line );
  149. $r .= '<tr>' . $this->deletedLine( $line ) . $this->emptyLine() . "</tr>\n";
  150. }
  151. return $r;
  152. }
  153. /**
  154. * @ignore
  155. * @access private
  156. *
  157. * @param array $lines
  158. * @param bool $encode
  159. * @return string
  160. */
  161. function _context( $lines, $encode = true ) {
  162. $r = '';
  163. foreach ($lines as $line) {
  164. if ( $encode )
  165. $line = htmlspecialchars( $line );
  166. $r .= '<tr>' .
  167. $this->contextLine( $line ) . $this->contextLine( $line ) . "</tr>\n";
  168. }
  169. return $r;
  170. }
  171. /**
  172. * Process changed lines to do word-by-word diffs for extra highlighting.
  173. *
  174. * (TRAC style) sometimes these lines can actually be deleted or added rows.
  175. * We do additional processing to figure that out
  176. *
  177. * @access private
  178. * @since 2.6.0
  179. *
  180. * @param array $orig
  181. * @param array $final
  182. * @return string
  183. */
  184. function _changed( $orig, $final ) {
  185. $r = '';
  186. // Does the aforementioned additional processing
  187. // *_matches tell what rows are "the same" in orig and final. Those pairs will be diffed to get word changes
  188. // match is numeric: an index in other column
  189. // match is 'X': no match. It is a new row
  190. // *_rows are column vectors for the orig column and the final column.
  191. // row >= 0: an indix of the $orig or $final array
  192. // row < 0: a blank row for that column
  193. list($orig_matches, $final_matches, $orig_rows, $final_rows) = $this->interleave_changed_lines( $orig, $final );
  194. // These will hold the word changes as determined by an inline diff
  195. $orig_diffs = array();
  196. $final_diffs = array();
  197. // Compute word diffs for each matched pair using the inline diff
  198. foreach ( $orig_matches as $o => $f ) {
  199. if ( is_numeric($o) && is_numeric($f) ) {
  200. $text_diff = new Text_Diff( 'auto', array( array($orig[$o]), array($final[$f]) ) );
  201. $renderer = new $this->inline_diff_renderer;
  202. $diff = $renderer->render( $text_diff );
  203. // If they're too different, don't include any <ins> or <dels>
  204. if ( $diff_count = preg_match_all( '!(<ins>.*?</ins>|<del>.*?</del>)!', $diff, $diff_matches ) ) {
  205. // length of all text between <ins> or <del>
  206. $stripped_matches = strlen(strip_tags( join(' ', $diff_matches[0]) ));
  207. // since we count lengith of text between <ins> or <del> (instead of picking just one),
  208. // we double the length of chars not in those tags.
  209. $stripped_diff = strlen(strip_tags( $diff )) * 2 - $stripped_matches;
  210. $diff_ratio = $stripped_matches / $stripped_diff;
  211. if ( $diff_ratio > $this->_diff_threshold )
  212. continue; // Too different. Don't save diffs.
  213. }
  214. // Un-inline the diffs by removing del or ins
  215. $orig_diffs[$o] = preg_replace( '|<ins>.*?</ins>|', '', $diff );
  216. $final_diffs[$f] = preg_replace( '|<del>.*?</del>|', '', $diff );
  217. }
  218. }
  219. foreach ( array_keys($orig_rows) as $row ) {
  220. // Both columns have blanks. Ignore them.
  221. if ( $orig_rows[$row] < 0 && $final_rows[$row] < 0 )
  222. continue;
  223. // If we have a word based diff, use it. Otherwise, use the normal line.
  224. if ( isset( $orig_diffs[$orig_rows[$row]] ) )
  225. $orig_line = $orig_diffs[$orig_rows[$row]];
  226. elseif ( isset( $orig[$orig_rows[$row]] ) )
  227. $orig_line = htmlspecialchars($orig[$orig_rows[$row]]);
  228. else
  229. $orig_line = '';
  230. if ( isset( $final_diffs[$final_rows[$row]] ) )
  231. $final_line = $final_diffs[$final_rows[$row]];
  232. elseif ( isset( $final[$final_rows[$row]] ) )
  233. $final_line = htmlspecialchars($final[$final_rows[$row]]);
  234. else
  235. $final_line = '';
  236. if ( $orig_rows[$row] < 0 ) { // Orig is blank. This is really an added row.
  237. $r .= $this->_added( array($final_line), false );
  238. } elseif ( $final_rows[$row] < 0 ) { // Final is blank. This is really a deleted row.
  239. $r .= $this->_deleted( array($orig_line), false );
  240. } else { // A true changed row.
  241. $r .= '<tr>' . $this->deletedLine( $orig_line ) . $this->addedLine( $final_line ) . "</tr>\n";
  242. }
  243. }
  244. return $r;
  245. }
  246. /**
  247. * Takes changed blocks and matches which rows in orig turned into which rows in final.
  248. *
  249. * Returns
  250. * *_matches ( which rows match with which )
  251. * *_rows ( order of rows in each column interleaved with blank rows as
  252. * necessary )
  253. *
  254. * @since 2.6.0
  255. *
  256. * @param unknown_type $orig
  257. * @param unknown_type $final
  258. * @return unknown
  259. */
  260. function interleave_changed_lines( $orig, $final ) {
  261. // Contains all pairwise string comparisons. Keys are such that this need only be a one dimensional array.
  262. $matches = array();
  263. foreach ( array_keys($orig) as $o ) {
  264. foreach ( array_keys($final) as $f ) {
  265. $matches["$o,$f"] = $this->compute_string_distance( $orig[$o], $final[$f] );
  266. }
  267. }
  268. asort($matches); // Order by string distance.
  269. $orig_matches = array();
  270. $final_matches = array();
  271. foreach ( $matches as $keys => $difference ) {
  272. list($o, $f) = explode(',', $keys);
  273. $o = (int) $o;
  274. $f = (int) $f;
  275. // Already have better matches for these guys
  276. if ( isset($orig_matches[$o]) && isset($final_matches[$f]) )
  277. continue;
  278. // First match for these guys. Must be best match
  279. if ( !isset($orig_matches[$o]) && !isset($final_matches[$f]) ) {
  280. $orig_matches[$o] = $f;
  281. $final_matches[$f] = $o;
  282. continue;
  283. }
  284. // Best match of this final is already taken? Must mean this final is a new row.
  285. if ( isset($orig_matches[$o]) )
  286. $final_matches[$f] = 'x';
  287. // Best match of this orig is already taken? Must mean this orig is a deleted row.
  288. elseif ( isset($final_matches[$f]) )
  289. $orig_matches[$o] = 'x';
  290. }
  291. // We read the text in this order
  292. ksort($orig_matches);
  293. ksort($final_matches);
  294. // Stores rows and blanks for each column.
  295. $orig_rows = $orig_rows_copy = array_keys($orig_matches);
  296. $final_rows = array_keys($final_matches);
  297. // Interleaves rows with blanks to keep matches aligned.
  298. // We may end up with some extraneous blank rows, but we'll just ignore them later.
  299. foreach ( $orig_rows_copy as $orig_row ) {
  300. $final_pos = array_search($orig_matches[$orig_row], $final_rows, true);
  301. $orig_pos = (int) array_search($orig_row, $orig_rows, true);
  302. if ( false === $final_pos ) { // This orig is paired with a blank final.
  303. array_splice( $final_rows, $orig_pos, 0, -1 );
  304. } elseif ( $final_pos < $orig_pos ) { // This orig's match is up a ways. Pad final with blank rows.
  305. $diff_pos = $final_pos - $orig_pos;
  306. while ( $diff_pos < 0 )
  307. array_splice( $final_rows, $orig_pos, 0, $diff_pos++ );
  308. } elseif ( $final_pos > $orig_pos ) { // This orig's match is down a ways. Pad orig with blank rows.
  309. $diff_pos = $orig_pos - $final_pos;
  310. while ( $diff_pos < 0 )
  311. array_splice( $orig_rows, $orig_pos, 0, $diff_pos++ );
  312. }
  313. }
  314. // Pad the ends with blank rows if the columns aren't the same length
  315. $diff_count = count($orig_rows) - count($final_rows);
  316. if ( $diff_count < 0 ) {
  317. while ( $diff_count < 0 )
  318. array_push($orig_rows, $diff_count++);
  319. } elseif ( $diff_count > 0 ) {
  320. $diff_count = -1 * $diff_count;
  321. while ( $diff_count < 0 )
  322. array_push($final_rows, $diff_count++);
  323. }
  324. return array($orig_matches, $final_matches, $orig_rows, $final_rows);
  325. /*
  326. // Debug
  327. echo "\n\n\n\n\n";
  328. echo "-- DEBUG Matches: Orig -> Final --";
  329. foreach ( $orig_matches as $o => $f ) {
  330. echo "\n\n\n\n\n";
  331. echo "ORIG: $o, FINAL: $f\n";
  332. var_dump($orig[$o],$final[$f]);
  333. }
  334. echo "\n\n\n\n\n";
  335. echo "-- DEBUG Matches: Final -> Orig --";
  336. foreach ( $final_matches as $f => $o ) {
  337. echo "\n\n\n\n\n";
  338. echo "FINAL: $f, ORIG: $o\n";
  339. var_dump($final[$f],$orig[$o]);
  340. }
  341. echo "\n\n\n\n\n";
  342. echo "-- DEBUG Rows: Orig -- Final --";
  343. echo "\n\n\n\n\n";
  344. foreach ( $orig_rows as $row => $o ) {
  345. if ( $o < 0 )
  346. $o = 'X';
  347. $f = $final_rows[$row];
  348. if ( $f < 0 )
  349. $f = 'X';
  350. echo "$o -- $f\n";
  351. }
  352. echo "\n\n\n\n\n";
  353. echo "-- END DEBUG --";
  354. echo "\n\n\n\n\n";
  355. return array($orig_matches, $final_matches, $orig_rows, $final_rows);
  356. */
  357. }
  358. /**
  359. * Computes a number that is intended to reflect the "distance" between two strings.
  360. *
  361. * @since 2.6.0
  362. *
  363. * @param string $string1
  364. * @param string $string2
  365. * @return int
  366. */
  367. function compute_string_distance( $string1, $string2 ) {
  368. // Vectors containing character frequency for all chars in each string
  369. $chars1 = count_chars($string1);
  370. $chars2 = count_chars($string2);
  371. // L1-norm of difference vector.
  372. $difference = array_sum( array_map( array(&$this, 'difference'), $chars1, $chars2 ) );
  373. // $string1 has zero length? Odd. Give huge penalty by not dividing.
  374. if ( !$string1 )
  375. return $difference;
  376. // Return distance per charcter (of string1)
  377. return $difference / strlen($string1);
  378. }
  379. /**
  380. * @ignore
  381. * @since 2.6.0
  382. *
  383. * @param int $a
  384. * @param int $b
  385. * @return int
  386. */
  387. function difference( $a, $b ) {
  388. return abs( $a - $b );
  389. }
  390. }
  391. /**
  392. * Better word splitting than the PEAR package provides.
  393. *
  394. * @since 2.6.0
  395. * @uses Text_Diff_Renderer_inline Extends
  396. */
  397. class WP_Text_Diff_Renderer_inline extends Text_Diff_Renderer_inline {
  398. /**
  399. * @ignore
  400. * @since 2.6.0
  401. *
  402. * @param string $string
  403. * @param string $newlineEscape
  404. * @return string
  405. */
  406. function _splitOnWords($string, $newlineEscape = "\n") {
  407. $string = str_replace("\0", '', $string);
  408. $words = preg_split( '/([^\w])/u', $string, -1, PREG_SPLIT_DELIM_CAPTURE );
  409. $words = str_replace( "\n", $newlineEscape, $words );
  410. return $words;
  411. }
  412. }
  413. ?>