createXPath($ir);
// Get a snapshot of current internal representation
$xml = $ir->saveXML();
// Set a maximum number of loops to ward against infinite loops
$remainingLoops = 10;
// From now on, keep looping until no further modifications are applied
do
{
$old = $xml;
$this->optimizeCloseTagElements();
$xml = $ir->saveXML();
}
while (--$remainingLoops > 0 && $xml !== $old);
$this->removeCloseTagSiblings();
$this->removeContentFromVoidElements();
$this->mergeConsecutiveLiteralOutputElements();
$this->removeEmptyDefaultCases();
}
/**
* Clone closeTag elements that follow a switch into said switch
*
* If there's a right after a , clone the at the end of
* the every that does not end with a
*
* @return void
*/
protected function cloneCloseTagElementsIntoSwitch()
{
$query = '//switch[name(following-sibling::*[1]) = "closeTag"]';
foreach ($this->query($query) as $switch)
{
$closeTag = $switch->nextSibling;
foreach ($this->query('case', $switch) as $case)
{
if (!$case->lastChild || $case->lastChild->nodeName !== 'closeTag')
{
$case->appendChild($closeTag->cloneNode());
}
}
}
}
/**
* Clone closeTag elements from the head of a switch's cases before said switch
*
* If there's a at the beginning of every , clone it and insert it
* right before the unless there's already one
*
* @return void
*/
protected function cloneCloseTagElementsOutOfSwitch()
{
$query = '//switch[case/closeTag][not(case[name(*[1]) != "closeTag"])]';
foreach ($this->query($query) as $switch)
{
$case = $this->query('case/closeTag', $switch)->item(0);
$switch->parentNode->insertBefore($case->cloneNode(), $switch);
}
}
/**
* Merge consecutive literal outputs
*
* @return void
*/
protected function mergeConsecutiveLiteralOutputElements()
{
foreach ($this->query('//output[@type="literal"]') as $output)
{
$disableOutputEscaping = $output->getAttribute('disable-output-escaping');
while ($this->nextSiblingIsLiteralOutput($output, $disableOutputEscaping))
{
$output->nodeValue = htmlspecialchars($output->nodeValue . $output->nextSibling->nodeValue, ENT_COMPAT);
$output->parentNode->removeChild($output->nextSibling);
}
}
}
/**
* Test whether the next sibling of an element is a literal output element with matching escaping
*
* @param DOMElement $node
* @param string $disableOutputEscaping
* @return bool
*/
protected function nextSiblingIsLiteralOutput(DOMElement $node, $disableOutputEscaping)
{
return isset($node->nextSibling) && $node->nextSibling->nodeName === 'output' && $node->nextSibling->getAttribute('type') === 'literal' && $node->nextSibling->getAttribute('disable-output-escaping') === $disableOutputEscaping;
}
/**
* Optimize closeTags elements
*
* @return void
*/
protected function optimizeCloseTagElements()
{
$this->cloneCloseTagElementsIntoSwitch();
$this->cloneCloseTagElementsOutOfSwitch();
$this->removeRedundantCloseTagElementsInSwitch();
$this->removeRedundantCloseTagElements();
}
/**
* Remove redundant closeTag siblings after a switch
*
* If all branches of a switch have a closeTag we can remove any closeTag siblings of the switch
*
* @return void
*/
protected function removeCloseTagSiblings()
{
$query = '//switch[not(case[not(closeTag)])]/following-sibling::closeTag';
$this->removeNodes($query);
}
/**
* Remove content from void elements
*
* For each void element, we find whichever elements close it and remove everything
* after
*
* @return void
*/
protected function removeContentFromVoidElements()
{
foreach ($this->query('//element[@void="yes"]') as $element)
{
$id = $element->getAttribute('id');
$query = './/closeTag[@id="' . $id . '"]/following-sibling::*';
$this->removeNodes($query, $element);
}
}
/**
* Remove empty default cases (no test and no descendants)
*
* @return void
*/
protected function removeEmptyDefaultCases()
{
$query = '//case[not(@test)][not(*)][. = ""]';
$this->removeNodes($query);
}
/**
* Remove all nodes that match given XPath query
*
* @param string $query
* @param DOMNode $contextNode
* @return void
*/
protected function removeNodes($query, DOMNode $contextNode = null)
{
foreach ($this->query($query, $contextNode) as $node)
{
if ($node->parentNode instanceof DOMElement)
{
$node->parentNode->removeChild($node);
}
}
}
/**
* Remove redundant closeTag elements from the tail of a switch's cases
*
* For each remove duplicate nodes that are either siblings or
* descendants of a sibling
*
* @return void
*/
protected function removeRedundantCloseTagElements()
{
foreach ($this->query('//closeTag') as $closeTag)
{
$id = $closeTag->getAttribute('id');
$query = 'following-sibling::*/descendant-or-self::closeTag[@id="' . $id . '"]';
$this->removeNodes($query, $closeTag);
}
}
/**
* Remove redundant closeTag elements from the tail of a switch's cases
*
* If there's a right after a , remove all nodes at the
* end of every
*
* @return void
*/
protected function removeRedundantCloseTagElementsInSwitch()
{
$query = '//switch[name(following-sibling::*[1]) = "closeTag"]';
foreach ($this->query($query) as $switch)
{
foreach ($this->query('case', $switch) as $case)
{
while ($case->lastChild && $case->lastChild->nodeName === 'closeTag')
{
$case->removeChild($case->lastChild);
}
}
}
}
}