945 Zeilen
30 KiB
PHP
945 Zeilen
30 KiB
PHP
<?php
|
|
|
|
namespace Caxy\HtmlDiff;
|
|
|
|
class ListDiff extends HtmlDiff
|
|
{
|
|
/**
|
|
* This is the minimum percentage a list item can match its counterpart in order to be considered a match.
|
|
* @var integer
|
|
*/
|
|
protected static $listMatchThreshold = 35;
|
|
|
|
/** @var array */
|
|
protected $listWords = array();
|
|
|
|
/** @var array */
|
|
protected $listTags = array();
|
|
|
|
/** @var array */
|
|
protected $listIsolatedDiffTags = array();
|
|
|
|
/** @var array */
|
|
protected $isolatedDiffTags = array (
|
|
'ol' => '[[REPLACE_ORDERED_LIST]]',
|
|
'ul' => '[[REPLACE_UNORDERED_LIST]]',
|
|
'dl' => '[[REPLACE_DEFINITION_LIST]]',
|
|
);
|
|
|
|
/**
|
|
* List (li) placeholder.
|
|
* @var string
|
|
*/
|
|
protected static $listPlaceHolder = "[[REPLACE_LIST_ITEM]]";
|
|
|
|
/**
|
|
* Holds the type of list this is ol, ul, dl.
|
|
* @var string
|
|
*/
|
|
protected $listType;
|
|
|
|
/**
|
|
* Used to hold what type of list the old list is.
|
|
* @var string
|
|
*/
|
|
protected $oldListType;
|
|
|
|
/**
|
|
* Used to hold what type of list the new list is.
|
|
* @var string
|
|
*/
|
|
protected $newListType;
|
|
|
|
/**
|
|
* Hold the old/new content of the content of the list.
|
|
* @var array
|
|
*/
|
|
protected $list;
|
|
|
|
/**
|
|
* Contains the old/new child lists content within this list.
|
|
* @var array
|
|
*/
|
|
protected $childLists;
|
|
|
|
/**
|
|
* Contains the old/new text strings that match
|
|
* @var array
|
|
*/
|
|
protected $textMatches;
|
|
|
|
/**
|
|
* Contains the indexed start positions of each list within word string.
|
|
* @var array
|
|
*/
|
|
protected $listsIndex;
|
|
|
|
/**
|
|
* Array that holds the index of all content outside of the array. Format is array(index => content).
|
|
* @var array
|
|
*/
|
|
protected $contentIndex = array();
|
|
|
|
/**
|
|
* Holds the order and data on each list/content block within this list.
|
|
* @var array
|
|
*/
|
|
protected $diffOrderIndex = array();
|
|
|
|
/**
|
|
* This is the opening ol,ul,dl ist tag.
|
|
* @var string
|
|
*/
|
|
protected $oldParentTag;
|
|
|
|
/**
|
|
* This is the opening ol,ul,dl ist tag.
|
|
* @var string
|
|
*/
|
|
protected $newParentTag;
|
|
|
|
/**
|
|
* We're using the same functions as the parent in build() to get us to the point of
|
|
* manipulating the data within this class.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function build()
|
|
{
|
|
// Use the parent functions to get the data we need organized.
|
|
$this->splitInputsToWords();
|
|
$this->replaceIsolatedDiffTags();
|
|
$this->indexNewWords();
|
|
// Now use the custom functions in this class to use the data and generate our diff.
|
|
$this->diffListContent();
|
|
|
|
return $this->content;
|
|
}
|
|
|
|
/**
|
|
* Calls to the actual custom functions of this class, to diff list content.
|
|
*/
|
|
protected function diffListContent()
|
|
{
|
|
/* Format the list we're focusing on.
|
|
* There will always be one list, though passed as an array with one item.
|
|
* Format this to only have the list contents, outside of the array.
|
|
*/
|
|
$this->formatThisListContent();
|
|
|
|
/* Build an index of content outside of list tags.
|
|
*/
|
|
$this->indexContent();
|
|
|
|
/* In cases where we're dealing with nested lists,
|
|
* make sure we use placeholders to replace the nested lists
|
|
*/
|
|
$this->replaceListIsolatedDiffTags();
|
|
|
|
/* Build a list of matches we can reference when we diff the contents of the lists.
|
|
* This is needed so that we each NEW list node is matched against the best possible OLD list node/
|
|
* It helps us determine whether the list was added, removed, or changed.
|
|
*/
|
|
$this->matchAndCompareLists();
|
|
|
|
/* Go through the list of matches, content, and diff each.
|
|
* Any nested lists would be sent to parent's diffList function, which creates a new listDiff class.
|
|
*/
|
|
$this->diff();
|
|
}
|
|
|
|
/**
|
|
* This function is used to populate both contentIndex and diffOrderIndex arrays for use in the diff function.
|
|
*/
|
|
protected function indexContent()
|
|
{
|
|
$this->contentIndex = array();
|
|
$this->diffOrderIndex = array('new' => array(), 'old' => array());
|
|
foreach ($this->list as $type => $list) {
|
|
|
|
$this->contentIndex[$type] = array();
|
|
$depth = 0;
|
|
$parentList = 0;
|
|
$position = 0;
|
|
$newBlock = true;
|
|
$listCount = 0;
|
|
$contentCount = 0;
|
|
foreach ($list as $key => $word) {
|
|
if (!$parentList && $this->isOpeningListTag($word)) {
|
|
$depth++;
|
|
|
|
$this->diffOrderIndex[$type][] = array('type' => 'list', 'position' => $listCount, 'index' => $key);
|
|
$listCount++;
|
|
continue;
|
|
}
|
|
|
|
if (!$parentList && $this->isClosingListTag($word)) {
|
|
$depth--;
|
|
|
|
if ($depth == 0) {
|
|
$newBlock = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($this->isOpeningIsolatedDiffTag($word)) {
|
|
$parentList++;
|
|
}
|
|
|
|
if ($this->isClosingIsolatedDiffTag($word)) {
|
|
$parentList--;
|
|
}
|
|
|
|
if ($depth == 0) {
|
|
if ($newBlock && !array_key_exists($contentCount, $this->contentIndex[$type])) {
|
|
$this->diffOrderIndex[$type][] = array('type' => 'content', 'position' => $contentCount, 'index' => $key);
|
|
|
|
$position = $contentCount;
|
|
$this->contentIndex[$type][$position] = '';
|
|
$contentCount++;
|
|
}
|
|
|
|
$this->contentIndex[$type][$position] .= $word;
|
|
}
|
|
|
|
$newBlock = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This function is used to remove the wrapped ul, ol, or dl characters from this list
|
|
* and sets the listType as ul, ol, or dl, so that we can use it later.
|
|
* $list is being set here as well, as an array with the old and new version of this list content.
|
|
*/
|
|
protected function formatThisListContent()
|
|
{
|
|
$formatArray = array(
|
|
array('type' => 'old', 'array' => $this->oldIsolatedDiffTags),
|
|
array('type' => 'new', 'array' => $this->newIsolatedDiffTags)
|
|
);
|
|
|
|
foreach ($formatArray as $item) {
|
|
$values = array_values($item['array']);
|
|
$this->list[$item['type']] = count($values)
|
|
? $this->formatList($values[0], $item['type'])
|
|
: array();
|
|
}
|
|
|
|
$this->listType = $this->newListType ?: $this->oldListType;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param array $arrayData
|
|
* @param string $index
|
|
* @return array
|
|
*/
|
|
protected function formatList(array $arrayData, $index = 'old')
|
|
{
|
|
$openingTag = $this->getAndStripTag($arrayData[0]);
|
|
$closingTag = $this->getAndStripTag($arrayData[count($arrayData) - 1]);
|
|
|
|
if (array_key_exists($openingTag, $this->isolatedDiffTags) &&
|
|
array_key_exists($closingTag, $this->isolatedDiffTags)
|
|
) {
|
|
if ($index == 'new' && $this->isOpeningTag($arrayData[0])) {
|
|
$this->newParentTag = $arrayData[0];
|
|
$this->newListType = $this->getAndStripTag($arrayData[0]);
|
|
}
|
|
|
|
if ($index == 'old' && $this->isOpeningTag($arrayData[0])) {
|
|
$this->oldParentTag = $arrayData[0];
|
|
$this->oldListType = $this->getAndStripTag($arrayData[0]);
|
|
}
|
|
|
|
array_shift($arrayData);
|
|
array_pop($arrayData);
|
|
}
|
|
|
|
return $arrayData;
|
|
}
|
|
|
|
/**
|
|
* @param string $tag
|
|
* @return string
|
|
*/
|
|
protected function getAndStripTag($tag)
|
|
{
|
|
$content = explode(' ', preg_replace("/[^A-Za-z0-9 ]/", '', $tag));
|
|
return $content[0];
|
|
}
|
|
|
|
protected function matchAndCompareLists()
|
|
{
|
|
/**
|
|
* Build the an array (childLists) to hold the contents of the list nodes within this list.
|
|
* This only holds the content of each list node.
|
|
*/
|
|
$this->buildChildLists();
|
|
|
|
/**
|
|
* Index the list, starting positions, so that we can refer back to it later.
|
|
* This is used to see where one list node starts and another ends.
|
|
*/
|
|
$this->indexLists();
|
|
|
|
/**
|
|
* Compare the lists and build $textMatches array with the matches.
|
|
* Each match is an array of "new" and "old" keys, with the id of the list it matches to.
|
|
* Whenever there is no match (in cases where a new list item was added or removed), null is used instead of the id.
|
|
*/
|
|
$this->compareChildLists();
|
|
}
|
|
|
|
/**
|
|
* Creates matches for lists.
|
|
*/
|
|
protected function compareChildLists()
|
|
{
|
|
$this->createNewOldMatches($this->childLists, $this->textMatches, 'content');
|
|
}
|
|
|
|
/**
|
|
* Abstracted function used to match items in an array.
|
|
* This is used primarily for populating lists matches.
|
|
*
|
|
* @param array $listArray
|
|
* @param array $resultArray
|
|
* @param string|null $column
|
|
*/
|
|
protected function createNewOldMatches(&$listArray, &$resultArray, $column = null)
|
|
{
|
|
// Always compare the new against the old.
|
|
// Compare each new string against each old string.
|
|
$bestMatchPercentages = array();
|
|
|
|
foreach ($listArray['new'] as $thisKey => $thisList) {
|
|
$bestMatchPercentages[$thisKey] = array();
|
|
foreach ($listArray['old'] as $thatKey => $thatList) {
|
|
// Save the percent amount each new list content compares against the old list content.
|
|
similar_text(
|
|
$column ? $thisList[$column] : $thisList,
|
|
$column ? $thatList[$column] : $thatList,
|
|
$percentage
|
|
);
|
|
|
|
$bestMatchPercentages[$thisKey][] = $percentage;
|
|
}
|
|
}
|
|
|
|
// Sort each array by value, highest percent to lowest percent.
|
|
foreach ($bestMatchPercentages as &$thisMatch) {
|
|
arsort($thisMatch);
|
|
}
|
|
|
|
// Build matches.
|
|
$matches = array();
|
|
$taken = array();
|
|
$takenItems = array();
|
|
$absoluteMatch = 100;
|
|
foreach ($bestMatchPercentages as $item => $percentages) {
|
|
$highestMatch = -1;
|
|
$highestMatchKey = -1;
|
|
$takeItemKey = -1;
|
|
|
|
foreach ($percentages as $key => $percent) {
|
|
// Check that the key for the percentage is not already taken and the new percentage is higher.
|
|
if (!in_array($key, $taken) && $percent > $highestMatch) {
|
|
// If an absolute match, choose this one.
|
|
if ($percent == $absoluteMatch) {
|
|
$highestMatch = $percent;
|
|
$highestMatchKey = $key;
|
|
$takenItemKey = $item;
|
|
break;
|
|
} else {
|
|
// Get all the other matces for the same $key
|
|
$columns = $this->getArrayColumn($bestMatchPercentages, $key);
|
|
$thisBestMatches = array_filter(
|
|
$columns,
|
|
function ($v) use ($percent) {
|
|
return $v > $percent;
|
|
}
|
|
);
|
|
|
|
arsort($thisBestMatches);
|
|
|
|
/**
|
|
* If the list item does not meet the threshold, it will not be considered a match.
|
|
*/
|
|
if ($percent >= self::$listMatchThreshold) {
|
|
// If no greater amounts, use this one.
|
|
if (!count($thisBestMatches)) {
|
|
$highestMatch = $percent;
|
|
$highestMatchKey = $key;
|
|
$takenItemKey = $item;
|
|
break;
|
|
}
|
|
|
|
// Loop through, comparing only the items that have not already been added.
|
|
foreach ($thisBestMatches as $k => $v) {
|
|
if (in_array($k, $takenItems)) {
|
|
$highestMatch = $percent;
|
|
$highestMatchKey = $key;
|
|
$takenItemKey = $item;
|
|
break(2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$matches[] = array('new' => $item, 'old' => $highestMatchKey > -1 ? $highestMatchKey : null);
|
|
if ($highestMatchKey > -1) {
|
|
$taken[] = $highestMatchKey;
|
|
$takenItems[] = $takenItemKey;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Checking for removed items. Basically, if a list item from the old lists is removed
|
|
* it will not be accounted for, and will disappear in the results altogether.
|
|
* Loop through all the old lists, any that has not been added, will be added as:
|
|
* array( new => null, old => oldItemId )
|
|
*/
|
|
$matchColumns = $this->getArrayColumn($matches, 'old');
|
|
foreach ($listArray['old'] as $thisKey => $thisList) {
|
|
if (!in_array($thisKey, $matchColumns)) {
|
|
$matches[] = array('new' => null, 'old' => $thisKey);
|
|
}
|
|
}
|
|
|
|
// Save the matches.
|
|
$resultArray = $matches;
|
|
}
|
|
|
|
/**
|
|
* This fuction is exactly like array_column. This is added for PHP versions that do not support array_column.
|
|
* @param array $targetArray
|
|
* @param mixed $key
|
|
* @return array
|
|
*/
|
|
protected function getArrayColumn(array $targetArray, $key)
|
|
{
|
|
$data = array();
|
|
foreach ($targetArray as $item) {
|
|
if (array_key_exists($key, $item)) {
|
|
$data[] = $item[$key];
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Build multidimensional array holding the contents of each list node, old and new.
|
|
*/
|
|
protected function buildChildLists()
|
|
{
|
|
$this->childLists['old'] = $this->getListsContent($this->list['old']);
|
|
$this->childLists['new'] = $this->getListsContent($this->list['new']);
|
|
}
|
|
|
|
/**
|
|
* Diff the actual contents of the lists against their matched counterpart.
|
|
* Build the content of the class.
|
|
*/
|
|
protected function diff()
|
|
{
|
|
// Add the opening parent node from listType. So if ol, <ol>, etc.
|
|
$this->content = $this->addListTypeWrapper();
|
|
|
|
$oldIndexCount = 0;
|
|
$diffOrderNewKeys = array_keys($this->diffOrderIndex['new']);
|
|
foreach ($this->diffOrderIndex['new'] as $key => $index) {
|
|
|
|
if ($index['type'] == "list") {
|
|
|
|
// Check to see if an old list was deleted.
|
|
$oldMatch = $this->getArrayByColumnValue($this->textMatches, 'old', $index['position']);
|
|
if ($oldMatch && $oldMatch['new'] === null) {
|
|
$newList = '';
|
|
$oldList = $this->getListByMatch($oldMatch, 'old');
|
|
$this->content .= $this->addListElementToContent($newList, $oldList, $oldMatch, $index, 'old');
|
|
}
|
|
|
|
$match = $this->getArrayByColumnValue($this->textMatches, 'new', $index['position']);
|
|
$newList = $this->childLists['new'][$match['new']];
|
|
$oldList = $this->getListByMatch($match, 'old');
|
|
$this->content .= $this->addListElementToContent($newList, $oldList, $match, $index, 'new');
|
|
}
|
|
|
|
if ($index['type'] == 'content') {
|
|
$this->content .= $this->addContentElementsToContent($oldIndexCount, $index['position']);
|
|
}
|
|
|
|
$oldIndexCount++;
|
|
|
|
if ($key == $diffOrderNewKeys[count($diffOrderNewKeys) - 1]) {
|
|
foreach ($this->diffOrderIndex['old'] as $oldKey => $oldIndex) {
|
|
if ($oldKey > $key) {
|
|
if ($oldIndex['type'] == 'list') {
|
|
$oldMatch = $this->getArrayByColumnValue($this->textMatches, 'old', $oldIndex['position']);
|
|
if ($oldMatch && $oldMatch['new'] === null) {
|
|
$newList = '';
|
|
$oldList = $this->getListByMatch($oldMatch, 'old');
|
|
$this->content .= $this->addListElementToContent($newList, $oldList, $oldMatch, $oldIndex, 'old');
|
|
}
|
|
} else {
|
|
$this->content .= $this->addContentElementsToContent($oldKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the closing parent node from listType. So if ol, </ol>, etc.
|
|
$this->content .= $this->addListTypeWrapper(false);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $newList
|
|
* @param string $oldList
|
|
* @param array $match
|
|
* @param array $index
|
|
* @return string
|
|
*/
|
|
protected function addListElementToContent($newList, $oldList, array $match, array $index, $type)
|
|
{
|
|
$content = $this->list[$type][$index['index']];
|
|
$content .= $this->processPlaceholders(
|
|
$this->diffElements(
|
|
$this->convertListContentArrayToString($oldList),
|
|
$this->convertListContentArrayToString($newList),
|
|
false
|
|
),
|
|
$match
|
|
);
|
|
$content .= "</li>";
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param integer $oldIndexCount
|
|
* @param null|integer $newPosition
|
|
* @return string
|
|
*/
|
|
protected function addContentElementsToContent($oldIndexCount, $newPosition = null)
|
|
{
|
|
$newContent = $newPosition && array_key_exists($newPosition, $this->contentIndex['new'])
|
|
? $this->contentIndex['new'][$newPosition]
|
|
: '';
|
|
|
|
$oldDiffOrderIndexMatch = array_key_exists($oldIndexCount, $this->diffOrderIndex['old'])
|
|
? $this->diffOrderIndex['old'][$oldIndexCount]
|
|
: '';
|
|
|
|
$oldContent = $oldDiffOrderIndexMatch && array_key_exists($oldDiffOrderIndexMatch['position'], $this->contentIndex['old'])
|
|
? $this->contentIndex['old'][$oldDiffOrderIndexMatch['position']]
|
|
: '';
|
|
|
|
$diffObject = new HtmlDiff($oldContent, $newContent);
|
|
$content = $diffObject->build();
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param array $match
|
|
* @param string $type
|
|
* @return array|string
|
|
*/
|
|
protected function getListByMatch(array $match, $type = 'new')
|
|
{
|
|
return array_key_exists($match[$type], $this->childLists[$type])
|
|
? $this->childLists[$type][$match[$type]]
|
|
: '';
|
|
}
|
|
|
|
/**
|
|
* This function replaces array_column function in PHP for older versions of php.
|
|
*
|
|
* @param array $parentArray
|
|
* @param string $column
|
|
* @param mixed $value
|
|
* @param boolean $allMatches
|
|
* @return array|boolean
|
|
*/
|
|
protected function getArrayByColumnValue($parentArray, $column, $value, $allMatches = false)
|
|
{
|
|
$returnArray = array();
|
|
foreach ($parentArray as $array) {
|
|
if (array_key_exists($column, $array) && $array[$column] == $value) {
|
|
if ($allMatches) {
|
|
$returnArray[] = $array;
|
|
} else {
|
|
return $array;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $allMatches ? $returnArray : false;
|
|
}
|
|
|
|
/**
|
|
* Converts the list (li) content arrays to string.
|
|
*
|
|
* @param array $listContentArray
|
|
* @return string
|
|
*/
|
|
protected function convertListContentArrayToString($listContentArray)
|
|
{
|
|
if (!is_array($listContentArray)) {
|
|
return $listContentArray;
|
|
}
|
|
|
|
$content = array();
|
|
|
|
$words = explode(" ", $listContentArray['content']);
|
|
$nestedListCount = 0;
|
|
foreach ($words as $word) {
|
|
$match = $word == self::$listPlaceHolder;
|
|
|
|
$content[] = $match
|
|
? "<li>" . $this->convertListContentArrayToString($listContentArray['kids'][$nestedListCount]) . "</li>"
|
|
: $word;
|
|
|
|
if ($match) {
|
|
$nestedListCount++;
|
|
}
|
|
}
|
|
|
|
return implode(" ", $content);
|
|
}
|
|
|
|
/**
|
|
* Return the contents of each list node.
|
|
* Process any placeholders for nested lists.
|
|
*
|
|
* @param string $text
|
|
* @param array $matches
|
|
* @return string
|
|
*/
|
|
protected function processPlaceholders($text, array $matches)
|
|
{
|
|
// Prepare return
|
|
$returnText = array();
|
|
// Save the contents of all list nodes, new and old.
|
|
$contentVault = array(
|
|
'old' => $this->getListContent('old', $matches),
|
|
'new' => $this->getListContent('new', $matches)
|
|
);
|
|
|
|
$count = 0;
|
|
// Loop through the text checking for placeholders. If a nested list is found, create a new ListDiff object for it.
|
|
foreach (explode(' ', $text) as $word) {
|
|
$preContent = $this->checkWordForDiffTag($this->stripNewLine($word));
|
|
|
|
if (in_array(
|
|
is_array($preContent) ? $preContent[1] : $preContent,
|
|
$this->isolatedDiffTags
|
|
)
|
|
) {
|
|
$oldText = array_key_exists($count, $contentVault['old']) ? implode('', $contentVault['old'][$count]) : '';
|
|
$newText = array_key_exists($count, $contentVault['new']) ? implode('', $contentVault['new'][$count]) : '';
|
|
$content = $this->diffList($oldText, $newText);
|
|
$count++;
|
|
} else {
|
|
$content = $preContent;
|
|
}
|
|
|
|
$returnText[] = is_array($preContent) ? $preContent[0] . $content . $preContent[2] : $content;
|
|
}
|
|
// Return the result.
|
|
return implode(' ', $returnText);
|
|
}
|
|
|
|
/**
|
|
* Checks to see if a diff tag is in string.
|
|
*
|
|
* @param string $word
|
|
* @return string
|
|
*/
|
|
protected function checkWordForDiffTag($word)
|
|
{
|
|
foreach ($this->isolatedDiffTags as $diffTag) {
|
|
if (strpos($word, $diffTag) > -1) {
|
|
$position = strpos($word, $diffTag);
|
|
$length = strlen($diffTag);
|
|
$result = array(
|
|
substr($word, 0, $position),
|
|
$diffTag,
|
|
substr($word, ($position + $length))
|
|
);
|
|
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
return $word;
|
|
}
|
|
|
|
/**
|
|
* Used to remove new lines.
|
|
*
|
|
* @param string $text
|
|
* @return string
|
|
*/
|
|
protected function stripNewLine($text)
|
|
{
|
|
return trim(preg_replace('/\s\s+/', ' ', $text));
|
|
}
|
|
|
|
/**
|
|
* Grab the list content using the listsIndex array.
|
|
*
|
|
* @param string $indexKey
|
|
* @param array $matches
|
|
* @return array
|
|
*/
|
|
protected function getListContent($indexKey = 'new', array $matches)
|
|
{
|
|
$bucket = array();
|
|
|
|
if (isset($matches[$indexKey]) && $matches[$indexKey] !== null) {
|
|
$start = $this->listsIndex[$indexKey][$matches[$indexKey]];
|
|
$stop = $this->findEndForIndex($this->list[$indexKey], $start);
|
|
|
|
for ($x = $start; $x <= $stop; $x++) {
|
|
|
|
if (in_array($this->list[$indexKey][$x], $this->isolatedDiffTags)) {
|
|
$bucket[] = $this->listIsolatedDiffTags[$indexKey][$x];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $bucket;
|
|
}
|
|
|
|
/**
|
|
* Finds the end of list within its index.
|
|
*
|
|
* @param array $index
|
|
* @param integer $start
|
|
* @return integer
|
|
*/
|
|
protected function findEndForIndex(array $index, $start)
|
|
{
|
|
$array = array_splice($index, $start);
|
|
$count = 0;
|
|
foreach ($array as $key => $item) {
|
|
if ($this->isOpeningListTag($item)) {
|
|
$count++;
|
|
}
|
|
|
|
if ($this->isClosingListTag($item)) {
|
|
$count--;
|
|
if ($count === 0) {
|
|
return $start + $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $start + count($array);
|
|
}
|
|
|
|
/**
|
|
* indexLists
|
|
*
|
|
* Index the list, starting positions, so that we can refer back to it later.
|
|
* This is used to see where one list node starts and another ends.
|
|
*/
|
|
protected function indexLists()
|
|
{
|
|
$this->listsIndex = array();
|
|
$count = 0;
|
|
foreach ($this->list as $type => $list) {
|
|
$this->listsIndex[$type] = array();
|
|
|
|
foreach ($list as $key => $listItem) {
|
|
if ($this->isOpeningListTag($listItem)) {
|
|
$count++;
|
|
if ($count === 1) {
|
|
$this->listsIndex[$type][] = $key;
|
|
}
|
|
}
|
|
|
|
if ($this->isClosingListTag($listItem)) {
|
|
$count--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the opening or closing list html element, based on listType.
|
|
*
|
|
* @param boolean $opening
|
|
* @return string
|
|
*/
|
|
protected function addListTypeWrapper($opening = true)
|
|
{
|
|
|
|
if ($opening) {
|
|
return $this->newParentTag ?: $this->oldParentTag;
|
|
} else {
|
|
return "<" . (!$opening ? "/" : '') . $this->listType . ">";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replace nested list with placeholders.
|
|
*/
|
|
public function replaceListIsolatedDiffTags()
|
|
{
|
|
$this->listIsolatedDiffTags['old'] = $this->createIsolatedDiffTagPlaceholders($this->list['old']);
|
|
$this->listIsolatedDiffTags['new'] = $this->createIsolatedDiffTagPlaceholders($this->list['new']);
|
|
}
|
|
|
|
/**
|
|
* Grab the contents of a list node.
|
|
*
|
|
* @param array $contentArray
|
|
* @param boolean $stripTags
|
|
* @return array
|
|
*/
|
|
protected function getListsContent(array $contentArray, $stripTags = true)
|
|
{
|
|
$lematches = array();
|
|
$arrayDepth = 0;
|
|
$nestedCount = array();
|
|
foreach ($contentArray as $index => $word) {
|
|
|
|
if ($this->isOpeningListTag($word)) {
|
|
$arrayDepth++;
|
|
if (!array_key_exists($arrayDepth, $nestedCount)) {
|
|
$nestedCount[$arrayDepth] = 1;
|
|
} else {
|
|
$nestedCount[$arrayDepth]++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($this->isClosingListTag($word)) {
|
|
$arrayDepth--;
|
|
continue;
|
|
}
|
|
|
|
if ($arrayDepth > 0) {
|
|
$this->addStringToArrayByDepth($word, $lematches, $arrayDepth, 1, $nestedCount);
|
|
}
|
|
}
|
|
|
|
return $lematches;
|
|
}
|
|
|
|
/**
|
|
* This function helps build the list content array of a list.
|
|
* If a list has another list within it, the inner list is replaced with the list placeholder and the inner list
|
|
* content becomes a child of the parent list.
|
|
* This goes recursively down.
|
|
*
|
|
* @param string $word
|
|
* @param array $array
|
|
* @param integer $targetDepth
|
|
* @param integer $thisDepth
|
|
* @param array $nestedCount
|
|
*/
|
|
protected function addStringToArrayByDepth($word, array &$array, $targetDepth, $thisDepth, array $nestedCount)
|
|
{
|
|
// determine what depth we're at
|
|
if ($targetDepth == $thisDepth) {
|
|
// decide on what to do at this level
|
|
|
|
if (array_key_exists('content', $array)) {
|
|
$array['content'] .= $word;
|
|
} else {
|
|
// if we're on depth 1, add content
|
|
if ($nestedCount[$targetDepth] > count($array)) {
|
|
$array[] = array('content' => '', 'kids' => array());
|
|
}
|
|
|
|
$array[count($array) - 1]['content'] .= $word;
|
|
}
|
|
|
|
} else {
|
|
|
|
// create first kid if not exist
|
|
$newArray = array('content' => '', 'kids' => array());
|
|
|
|
if (array_key_exists('kids', $array)) {
|
|
if ($nestedCount[$targetDepth] > count($array['kids'])) {
|
|
$array['kids'][] = $newArray;
|
|
$array['content'] .= self::$listPlaceHolder;
|
|
}
|
|
|
|
// continue to the next depth
|
|
$thisDepth++;
|
|
|
|
// get last kid and send to next depth
|
|
|
|
$this->addStringToArrayByDepth(
|
|
$word,
|
|
$array['kids'][count($array['kids']) - 1],
|
|
$targetDepth,
|
|
$thisDepth,
|
|
$nestedCount
|
|
);
|
|
|
|
} else {
|
|
|
|
if ($nestedCount[$targetDepth] > count($array[count($array) - 1]['kids'])) {
|
|
$array[count($array) - 1]['kids'][] = $newArray;
|
|
$array[count($array) - 1]['content'] .= self::$listPlaceHolder;
|
|
}
|
|
// continue to the next depth
|
|
$thisDepth++;
|
|
|
|
// get last kid and send to next depth
|
|
|
|
$this->addStringToArrayByDepth(
|
|
$word,
|
|
$array[count($array) - 1]['kids'][count($array[count($array) - 1]['kids']) - 1],
|
|
$targetDepth,
|
|
$thisDepth,
|
|
$nestedCount
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if text is opening list tag.
|
|
*
|
|
* @param string $item
|
|
* @return boolean
|
|
*/
|
|
protected function isOpeningListTag($item)
|
|
{
|
|
if (preg_match("#<li[^>]*>\\s*#iU", $item)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if text is closing list tag.
|
|
*
|
|
* @param string $item
|
|
* @return boolean
|
|
*/
|
|
protected function isClosingListTag($item)
|
|
{
|
|
if (preg_match("#</li[^>]*>\\s*#iU", $item)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|