Plugin to allow visitor contributions to WordPress posts, wiki style.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

281 lines
10 KiB

  1. <?php
  2. namespace Caxy\HtmlDiff;
  3. use Caxy\HtmlDiff\ListDiff\DiffList;
  4. use Caxy\HtmlDiff\ListDiff\DiffListItem;
  5. class ListDiffNew extends AbstractDiff
  6. {
  7. protected static $listTypes = array('ul', 'ol', 'dl');
  8. /**
  9. * @param string $oldText
  10. * @param string $newText
  11. * @param HtmlDiffConfig|null $config
  12. *
  13. * @return self
  14. */
  15. public static function create($oldText, $newText, HtmlDiffConfig $config = null)
  16. {
  17. $diff = new self($oldText, $newText);
  18. if (null !== $config) {
  19. $diff->setConfig($config);
  20. }
  21. return $diff;
  22. }
  23. public function build()
  24. {
  25. if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
  26. $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
  27. return $this->content;
  28. }
  29. $this->splitInputsToWords();
  30. $this->content = $this->diffLists(
  31. $this->buildDiffList($this->oldWords),
  32. $this->buildDiffList($this->newWords)
  33. );
  34. if ($this->hasDiffCache()) {
  35. $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
  36. }
  37. return $this->content;
  38. }
  39. protected function diffLists(DiffList $oldList, DiffList $newList)
  40. {
  41. $oldMatchData = array();
  42. $newMatchData = array();
  43. $oldListIndices = array();
  44. $newListIndices = array();
  45. $oldListItems = array();
  46. $newListItems = array();
  47. foreach ($oldList->getListItems() as $oldIndex => $oldListItem) {
  48. if ($oldListItem instanceof DiffListItem) {
  49. $oldListItems[$oldIndex] = $oldListItem;
  50. $oldListIndices[] = $oldIndex;
  51. $oldMatchData[$oldIndex] = array();
  52. // Get match percentages
  53. foreach ($newList->getListItems() as $newIndex => $newListItem) {
  54. if ($newListItem instanceof DiffListItem) {
  55. if (!in_array($newListItem, $newListItems)) {
  56. $newListItems[$newIndex] = $newListItem;
  57. }
  58. if (!in_array($newIndex, $newListIndices)) {
  59. $newListIndices[] = $newIndex;
  60. }
  61. if (!array_key_exists($newIndex, $newMatchData)) {
  62. $newMatchData[$newIndex] = array();
  63. }
  64. $oldText = implode('', $oldListItem->getText());
  65. $newText = implode('', $newListItem->getText());
  66. // similar_text
  67. $percentage = null;
  68. similar_text($oldText, $newText, $percentage);
  69. $oldMatchData[$oldIndex][$newIndex] = $percentage;
  70. $newMatchData[$newIndex][$oldIndex] = $percentage;
  71. }
  72. }
  73. }
  74. }
  75. $currentIndexInOld = 0;
  76. $currentIndexInNew = 0;
  77. $oldCount = count($oldListIndices);
  78. $newCount = count($newListIndices);
  79. $difference = max($oldCount, $newCount) - min($oldCount, $newCount);
  80. $diffOutput = '';
  81. foreach ($newList->getListItems() as $newIndex => $newListItem) {
  82. if ($newListItem instanceof DiffListItem) {
  83. $operation = null;
  84. $oldListIndex = array_key_exists($currentIndexInOld, $oldListIndices) ? $oldListIndices[$currentIndexInOld] : null;
  85. $class = 'normal';
  86. if (null !== $oldListIndex && array_key_exists($oldListIndex, $oldMatchData)) {
  87. // Check percentage matches of upcoming list items in old.
  88. $matchPercentage = $oldMatchData[$oldListIndex][$newIndex];
  89. // does the old list item match better?
  90. $otherMatchBetter = false;
  91. foreach ($oldMatchData[$oldListIndex] as $index => $percentage) {
  92. if ($index > $newIndex && $percentage > $matchPercentage) {
  93. $otherMatchBetter = $index;
  94. }
  95. }
  96. if (false !== $otherMatchBetter && $newCount > $oldCount && $difference > 0) {
  97. $diffOutput .= sprintf('%s', $newListItem->getHtml('normal new', 'ins'));
  98. $currentIndexInNew++;
  99. $difference--;
  100. continue;
  101. }
  102. $nextOldListIndex = array_key_exists($currentIndexInOld + 1, $oldListIndices) ? $oldListIndices[$currentIndexInOld + 1] : null;
  103. $replacement = false;
  104. if ($nextOldListIndex !== null && $oldMatchData[$nextOldListIndex][$newIndex] > $matchPercentage && $oldMatchData[$nextOldListIndex][$newIndex] > $this->config->getMatchThreshold()) {
  105. // Following list item in old is better match, use that.
  106. $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del'));
  107. $currentIndexInOld++;
  108. $oldListIndex = $nextOldListIndex;
  109. $matchPercentage = $oldMatchData[$oldListIndex];
  110. $replacement = true;
  111. }
  112. if ($matchPercentage > $this->config->getMatchThreshold() || $currentIndexInNew === $currentIndexInOld) {
  113. // Diff the two lists.
  114. $htmlDiff = HtmlDiff::create(
  115. $oldListItems[$oldListIndex]->getInnerHtml(),
  116. $newListItem->getInnerHtml(),
  117. $this->config
  118. );
  119. $diffContent = $htmlDiff->build();
  120. $diffOutput .= sprintf('%s%s%s', $newListItem->getStartTagWithDiffClass($replacement ? 'replacement' : 'normal'), $diffContent, $newListItem->getEndTag());
  121. } else {
  122. $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del'));
  123. $diffOutput .= sprintf('%s', $newListItem->getHtml('replacement', 'ins'));
  124. }
  125. $currentIndexInOld++;
  126. } else {
  127. $diffOutput .= sprintf('%s', $newListItem->getHtml('normal new', 'ins'));
  128. }
  129. $currentIndexInNew++;
  130. }
  131. }
  132. // Output any additional list items
  133. while (array_key_exists($currentIndexInOld, $oldListIndices)) {
  134. $oldListIndex = $oldListIndices[$currentIndexInOld];
  135. $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del'));
  136. $currentIndexInOld++;
  137. }
  138. return sprintf('%s%s%s', $newList->getStartTagWithDiffClass(), $diffOutput, $newList->getEndTag());
  139. }
  140. protected function buildDiffList($words)
  141. {
  142. $listType = null;
  143. $listStartTag = null;
  144. $listEndTag = null;
  145. $attributes = array();
  146. $openLists = 0;
  147. $openListItems = 0;
  148. $list = array();
  149. $currentListItem = null;
  150. $listItemType = null;
  151. $listItemStart = null;
  152. $listItemEnd = null;
  153. foreach ($words as $i => $word) {
  154. if ($this->isOpeningListTag($word, $listType)) {
  155. if ($openLists > 0) {
  156. if ($openListItems > 0) {
  157. $currentListItem[] = $word;
  158. } else {
  159. $list[] = $word;
  160. }
  161. } else {
  162. $listType = substr($word, 1, 2);
  163. $listStartTag = $word;
  164. }
  165. $openLists++;
  166. } elseif ($this->isClosingListTag($word, $listType)) {
  167. if ($openLists > 1) {
  168. if ($openListItems > 0) {
  169. $currentListItem[] = $word;
  170. } else {
  171. $list[] = $word;
  172. }
  173. } else {
  174. $listEndTag = $word;
  175. }
  176. $openLists--;
  177. } elseif ($this->isOpeningListItemTag($word, $listItemType)) {
  178. if ($openListItems === 0) {
  179. // New top-level list item
  180. $currentListItem = array();
  181. $listItemType = substr($word, 1, 2);
  182. $listItemStart = $word;
  183. } else {
  184. $currentListItem[] = $word;
  185. }
  186. $openListItems++;
  187. } elseif ($this->isClosingListItemTag($word, $listItemType)) {
  188. if ($openListItems === 1) {
  189. $listItemEnd = $word;
  190. $listItem = new DiffListItem($currentListItem, array(), $listItemStart, $listItemEnd);
  191. $list[] = $listItem;
  192. $currentListItem = null;
  193. } else {
  194. $currentListItem[] = $word;
  195. }
  196. $openListItems--;
  197. } else {
  198. if ($openListItems > 0) {
  199. $currentListItem[] = $word;
  200. } else {
  201. $list[] = $word;
  202. }
  203. }
  204. }
  205. $diffList = new DiffList($listType, $listStartTag, $listEndTag, $list, $attributes);
  206. return $diffList;
  207. }
  208. protected function isOpeningListTag($word, $type = null)
  209. {
  210. $filter = $type !== null ? array('<' . $type) : array('<ul', '<ol', '<dl');
  211. return in_array(substr($word, 0, 3), $filter);
  212. }
  213. protected function isClosingListTag($word, $type = null)
  214. {
  215. $filter = $type !== null ? array('</' . $type) : array('</ul', '</ol', '</dl');
  216. return in_array(substr($word, 0, 4), $filter);
  217. }
  218. protected function isOpeningListItemTag($word, $type = null)
  219. {
  220. $filter = $type !== null ? array('<' . $type) : array('<li', '<dd', '<dt');
  221. return in_array(substr($word, 0, 3), $filter);
  222. }
  223. protected function isClosingListItemTag($word, $type = null)
  224. {
  225. $filter = $type !== null ? array('</' . $type) : array('</li', '</dd', '</dt');
  226. return in_array(substr($word, 0, 4), $filter);
  227. }
  228. }