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.=''." . $phpElName . ".'>';";
}
// 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;
}
}