convertor = new XPathConvertor; } /** * Convert an XPath expression (used in a condition) into PHP code * * This method is similar to convertXPath() but it selectively replaces some simple conditions * with the corresponding DOM method for performance reasons * * @param string $expr XPath expression * @return string PHP code */ public function convertCondition($expr) { return $this->convertor->convertCondition($expr); } /** * Convert an XPath expression (used as value) into PHP code * * @param string $expr XPath expression * @return string PHP code */ public function convertXPath($expr) { $php = $this->convertor->convertXPath($expr); if (is_numeric($php)) { $php = "'" . $php . "'"; } return $php; } /** * Serialize the internal representation of a template into PHP * * @param DOMElement $ir Internal representation * @return string */ public function serialize(DOMElement $ir) { $this->xpath = new DOMXPath($ir->ownerDocument); $this->isVoid = []; foreach ($this->xpath->query('//element') as $element) { $this->isVoid[$element->getAttribute('id')] = $element->getAttribute('void'); } return $this->serializeChildren($ir); } /** * Convert an attribute value template into PHP * * NOTE: escaping must be performed by the caller * * @link https://www.w3.org/TR/1999/REC-xslt-19991116#dt-attribute-value-template * * @param string $attrValue Attribute value template * @return string */ protected function convertAttributeValueTemplate($attrValue) { $phpExpressions = []; foreach (AVTHelper::parse($attrValue) as $token) { if ($token[0] === 'literal') { $phpExpressions[] = var_export($token[1], true); } else { $phpExpressions[] = $this->convertXPath($token[1]); } } return implode('.', $phpExpressions); } /** * Convert a dynamic xsl:attribute/xsl:element name into PHP * * @param string $attrValue Attribute value template * @return string */ protected function convertDynamicNodeName(string $attrValue): string { if (strpos($attrValue, '{') === false) { return var_export(htmlspecialchars($attrValue, ENT_QUOTES), true); } return 'htmlspecialchars(' . $this->convertAttributeValueTemplate($attrValue) . ',' . ENT_QUOTES . ')'; } /** * Escape given literal * * @param string $text Literal * @param string $context Either "raw", "text" or "attribute" * @return string Escaped literal */ protected function escapeLiteral($text, $context) { if ($context === 'raw') { return $text; } $escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES; return htmlspecialchars($text, $escapeMode); } /** * Escape the output of given PHP expression * * @param string $php PHP expression * @param string $context Either "raw", "text" or "attribute" * @return string PHP expression, including escaping mechanism */ protected function escapePHPOutput($php, $context) { if ($context === 'raw') { return $php; } $escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES; return 'htmlspecialchars(' . $php . ',' . $escapeMode . ')'; } /** * Test whether given switch has more than one non-default case * * @param DOMElement $switch node * @return bool */ protected function hasMultipleCases(DOMElement $switch) { return $this->xpath->evaluate('count(case[@test]) > 1', $switch); } /** * Test whether given attribute declaration is a minimizable boolean attribute * * @param DOMElement $attribute node * @param string $php Attribute content, in PHP * @return boolean */ protected function isBooleanAttribute(DOMElement $attribute, string $php): bool { if ($attribute->getAttribute('boolean') !== 'yes') { return false; } return ($php === '' || $php === "\$this->out.='" . $attribute->getAttribute('name') . "';"); } /** * Serialize an node * * @param DOMElement $applyTemplates node * @return string */ protected function serializeApplyTemplates(DOMElement $applyTemplates) { $php = '$this->at($node'; if ($applyTemplates->hasAttribute('select')) { $php .= ',' . var_export($applyTemplates->getAttribute('select'), true); } $php .= ');'; return $php; } /** * Serialize an node * * @param DOMElement $attribute node * @return string */ protected function serializeAttribute(DOMElement $attribute) { $attrName = $attribute->getAttribute('name'); // PHP representation of this attribute's name $phpAttrName = $this->convertDynamicNodeName($attrName); $php = "\$this->out.=' '." . $phpAttrName; $content = $this->serializeChildren($attribute); if (!$this->isBooleanAttribute($attribute, $content)) { $php .= ".'=\"';" . $content . "\$this->out.='\"'"; } $php .= ';'; return $php; } /** * Serialize all the children of given node into PHP * * @param DOMElement $ir Internal representation * @return string */ protected function serializeChildren(DOMElement $ir) { $php = ''; foreach ($ir->childNodes as $node) { if ($node instanceof DOMElement) { $methodName = 'serialize' . ucfirst($node->localName); $php .= $this->$methodName($node); } } return $php; } /** * Serialize a node * * @param DOMElement $closeTag node * @return string */ protected function serializeCloseTag(DOMElement $closeTag) { $php = "\$this->out.='>';"; $id = $closeTag->getAttribute('id'); if ($closeTag->hasAttribute('set')) { $php .= '$t' . $id . '=1;'; } if ($closeTag->hasAttribute('check')) { $php = 'if(!isset($t' . $id . ')){' . $php . '}'; } if ($this->isVoid[$id] === 'maybe') { // Check at runtime whether this element is not void $php .= 'if(!$v' . $id . '){'; } return $php; } /** * Serialize a node * * @param DOMElement $comment node * @return string */ protected function serializeComment(DOMElement $comment) { return "\$this->out.='';"; } /** * Serialize a node * * @param DOMElement $copyOfAttributes node * @return string */ protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes) { return 'foreach($node->attributes as $attribute)' . '{' . "\$this->out.=' ';" . "\$this->out.=\$attribute->name;" . "\$this->out.='=\"';" . "\$this->out.=htmlspecialchars(\$attribute->value," . ENT_COMPAT . ");" . "\$this->out.='\"';" . '}'; } /** * Serialize an node * * @param DOMElement $element node * @return string */ protected function serializeElement(DOMElement $element) { $php = ''; $elName = $element->getAttribute('name'); $id = $element->getAttribute('id'); $isVoid = $element->getAttribute('void'); // Test whether this element name is dynamic $isDynamic = (bool) (strpos($elName, '{') !== false); // PHP representation of this element's name $phpElName = $this->convertDynamicNodeName($elName); // If the element name is dynamic, we cache its name for convenience and performance if ($isDynamic) { $varName = '$e' . $id; // Add the var declaration to the source $php .= $varName . '=' . $phpElName . ';'; // Replace the element name with the var $phpElName = $varName; } // Test whether this element is void if we need this information if ($isVoid === 'maybe') { $php .= '$v' . $id . '=preg_match(' . var_export(TemplateParser::$voidRegexp, true) . ',' . $phpElName . ');'; } // Open the start tag $php .= "\$this->out.='<'." . $phpElName . ';'; // Serialize this element's content $php .= $this->serializeChildren($element); // Close that element unless we know it's void if ($isVoid !== 'yes') { $php .= "\$this->out.='';"; } // If this element was maybe void, serializeCloseTag() has put its content within an if // block. We need to close that block if ($isVoid === 'maybe') { $php .= '}'; } return $php; } /** * Serialize a node that has a branch-key attribute * * @param DOMElement $switch node * @return string */ protected function serializeHash(DOMElement $switch) { $statements = []; foreach ($this->xpath->query('case[@branch-values]', $switch) as $case) { foreach (unserialize($case->getAttribute('branch-values')) as $value) { $statements[$value] = $this->serializeChildren($case); } } if (!isset($case)) { throw new RuntimeException; } $defaultCase = $this->xpath->query('case[not(@branch-values)]', $switch)->item(0); $defaultCode = ($defaultCase instanceof DOMElement) ? $this->serializeChildren($defaultCase) : ''; $expr = $this->convertXPath($switch->getAttribute('branch-key')); return SwitchStatement::generate($expr, $statements, $defaultCode); } /** * Serialize an node * * @param DOMElement $output node * @return string */ protected function serializeOutput(DOMElement $output) { $context = $output->getAttribute('escape'); $php = '$this->out.='; if ($output->getAttribute('type') === 'xpath') { $php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context); } else { $php .= var_export($this->escapeLiteral($output->textContent, $context), true); } $php .= ';'; return $php; } /** * Serialize a node * * @param DOMElement $switch node * @return string */ protected function serializeSwitch(DOMElement $switch) { // Use a specialized branch table if the minimum number of branches is reached if ($switch->hasAttribute('branch-key') && $this->hasMultipleCases($switch)) { return $this->serializeHash($switch); } $php = ''; $if = 'if'; foreach ($this->xpath->query('case', $switch) as $case) { if ($case->hasAttribute('test')) { $php .= $if . '(' . $this->convertCondition($case->getAttribute('test')) . ')'; } else { $php .= 'else'; } $php .= '{' . $this->serializeChildren($case) . '}'; $if = 'elseif'; } return $php; } }