optimizer = $optimizer;
}
/**
* Normalize an IR
*
* @param DOMDocument $ir
* @return void
*/
public function normalize(DOMDocument $ir)
{
$this->createXPath($ir);
$this->addDefaultCase();
$this->addElementIds();
$this->addCloseTagElements($ir);
$this->markVoidElements();
$this->optimizer->optimize($ir);
$this->markConditionalCloseTagElements();
$this->setOutputContext();
$this->markBranchTables();
$this->markBooleanAttributes();
}
/**
* Add elements everywhere an open start tag should be closed
*
* @param DOMDocument $ir
* @return void
*/
protected function addCloseTagElements(DOMDocument $ir)
{
$exprs = [
'//applyTemplates[not(ancestor::attribute)]',
'//comment',
'//element',
'//output[not(ancestor::attribute)]'
];
foreach ($this->query(implode('|', $exprs)) as $node)
{
$parentElementId = $this->getParentElementId($node);
if (isset($parentElementId))
{
$node->parentNode
->insertBefore($ir->createElement('closeTag'), $node)
->setAttribute('id', $parentElementId);
}
// Append a to nodes to ensure that empty elements get closed
if ($node->nodeName === 'element')
{
$id = $node->getAttribute('id');
$this->appendElement($node, 'closeTag')->setAttribute('id', $id);
}
}
}
/**
* Add an empty default to nodes that don't have one
*
* @return void
*/
protected function addDefaultCase()
{
foreach ($this->query('//switch[not(case[not(@test)])]') as $switch)
{
$this->appendElement($switch, 'case');
}
}
/**
* Add an id attribute to nodes
*
* @return void
*/
protected function addElementIds()
{
$id = 0;
foreach ($this->query('//element') as $element)
{
$element->setAttribute('id', ++$id);
}
}
/**
* Get the context type for given output element
*
* @param DOMNode $output
* @return string
*/
protected function getOutputContext(DOMNode $output)
{
$contexts = [
'boolean(ancestor::attribute)' => 'attribute',
'@disable-output-escaping="yes"' => 'raw',
'count(ancestor::element[@name="script"])' => 'raw'
];
foreach ($contexts as $expr => $context)
{
if ($this->evaluate($expr, $output))
{
return $context;
}
}
return 'text';
}
/**
* Get the ID of the closest "element" ancestor
*
* @param DOMNode $node Context node
* @return string|null
*/
protected function getParentElementId(DOMNode $node)
{
$parentNode = $node->parentNode;
while (isset($parentNode))
{
if ($parentNode->nodeName === 'element')
{
return $parentNode->getAttribute('id');
}
$parentNode = $parentNode->parentNode;
}
}
/**
* Mark switch elements that are used as branch tables
*
* If a switch is used for a series of equality tests against the same attribute or variable, the
* attribute/variable is stored within the switch as "branch-key" and the values it is compared
* against are stored JSON-encoded in the case as "branch-values". It can be used to create
* optimized branch tables
*
* @return void
*/
protected function markBranchTables()
{
// Iterate over switch elements that have at least two case children with a test attribute
foreach ($this->query('//switch[case[2][@test]]') as $switch)
{
$this->markSwitchTable($switch);
}
}
/**
* Mark given switch element if it's used as a branch table
*
* @param DOMElement $switch
* @return void
*/
protected function markSwitchTable(DOMElement $switch)
{
$cases = [];
$maps = [];
foreach ($this->query('./case[@test]', $switch) as $i => $case)
{
$map = XPathHelper::parseEqualityExpr($case->getAttribute('test'));
if ($map === false)
{
return;
}
$maps += $map;
$cases[$i] = [$case, end($map)];
}
if (count($maps) !== 1)
{
return;
}
$switch->setAttribute('branch-key', key($maps));
foreach ($cases as list($case, $values))
{
sort($values);
$case->setAttribute('branch-values', serialize($values));
}
}
/**
* Mark conditional nodes
*
* @return void
*/
protected function markConditionalCloseTagElements()
{
foreach ($this->query('//closeTag') as $closeTag)
{
$id = $closeTag->getAttribute('id');
// For each ancestor, look for a and that is either a sibling or
// the descendant of a sibling, and that matches the id
$query = 'ancestor::switch/'
. 'following-sibling::*/'
. 'descendant-or-self::closeTag[@id = "' . $id . '"]';
foreach ($this->query($query, $closeTag) as $following)
{
// Mark following nodes to indicate that the status of this tag must
// be checked before it is closed
$following->setAttribute('check', '');
// Mark the current to indicate that it must set a flag to indicate
// that its tag has been closed
$closeTag->setAttribute('set', '');
}
}
}
/**
* Mark boolean attributes
*
* The test is case-sensitive and only covers attribute that are minimized by libxslt
*
* @return void
*/
protected function markBooleanAttributes(): void
{
$attrNames = ['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected'];
foreach ($this->query('//attribute') as $attribute)
{
if (in_array($attribute->getAttribute('name'), $attrNames, true))
{
$attribute->setAttribute('boolean', 'yes');
}
}
}
/**
* Mark void elements
*
* @return void
*/
protected function markVoidElements()
{
foreach ($this->query('//element') as $element)
{
// Test whether this element is (maybe) void
$elName = $element->getAttribute('name');
if (strpos($elName, '{') !== false)
{
// Dynamic element names must be checked at runtime
$element->setAttribute('void', 'maybe');
}
elseif (preg_match($this->voidRegexp, $elName))
{
// Static element names can be checked right now
$element->setAttribute('void', 'yes');
}
}
}
/**
* Fill in output context
*
* @return void
*/
protected function setOutputContext()
{
foreach ($this->query('//output') as $output)
{
$output->setAttribute('escape', $this->getOutputContext($output));
}
}
}