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)); } } }