Formatter/Format/XmlFormat.php
<?php
namespace Phug\Formatter\Format;
use Closure;
use Generator;
use InvalidArgumentException;
use Phug\Formatter;
use Phug\Formatter\AbstractFormat;
use Phug\Formatter\AssignmentContainerInterface;
use Phug\Formatter\Element\AbstractValueElement;
use Phug\Formatter\Element\AssignmentElement;
use Phug\Formatter\Element\AttributeElement;
use Phug\Formatter\Element\CodeElement;
use Phug\Formatter\Element\ExpressionElement;
use Phug\Formatter\Element\MarkupElement;
use Phug\Formatter\Element\MixinCallElement;
use Phug\Formatter\Element\TextElement;
use Phug\Formatter\ElementInterface;
use Phug\Formatter\MarkupInterface;
use Phug\Formatter\Partial\AssignmentHelpersTrait;
use Phug\FormatterException;
use Phug\Util\AttributesInterface;
use Phug\Util\Joiner;
use Phug\Util\OrderedValue;
use SplObjectStorage;
class XmlFormat extends AbstractFormat
{
use AssignmentHelpersTrait;
const DOCTYPE = '<?xml version="1.0" encoding="utf-8" ?>';
const OPEN_PAIR_TAG = '<%s>';
const CLOSE_PAIR_TAG = '</%s>';
const SELF_CLOSING_TAG = '<%s />';
const ATTRIBUTE_PATTERN = ' %s="%s"';
const BOOLEAN_ATTRIBUTE_PATTERN = ' %s="%s"';
const BUFFER_VARIABLE = '$__value';
public function __construct(Formatter $formatter = null)
{
parent::__construct($formatter);
$this
->setOptionsDefaults([
'attributes_mapping' => [],
'assignment_handlers' => [],
'attribute_assignments' => [],
])
->registerHelper('available_attribute_assignments', [])
->addPatterns([
'open_pair_tag' => static::OPEN_PAIR_TAG,
'close_pair_tag' => static::CLOSE_PAIR_TAG,
'self_closing_tag' => static::SELF_CLOSING_TAG,
'attribute_pattern' => static::ATTRIBUTE_PATTERN,
'boolean_attribute_pattern' => static::BOOLEAN_ATTRIBUTE_PATTERN,
'save_value' => static::SAVE_VALUE,
'buffer_variable' => static::BUFFER_VARIABLE,
])
->provideAttributeAssignments()
->provideAttributeAssignment()
->provideStandAloneAttributeAssignment()
->provideMergeAttributes()
->provideArrayEscape()
->provideAttributesAssignment()
->provideClassAttributeAssignment()
->provideStandAloneClassAttributeAssignment()
->provideStyleAttributeAssignment()
->provideStandAloneStyleAttributeAssignment();
$handlers = $this->getOption('attribute_assignments');
foreach ($handlers as $name => $handler) {
$this->addAttributeAssignment($name, $handler);
}
}
protected function addAttributeAssignment($name, $handler)
{
$availableAssignments = $this->getHelper('available_attribute_assignments');
$this->registerHelper($name.'_attribute_assignment', $handler);
$availableAssignments[] = $name;
return $this->registerHelper('available_attribute_assignments', $availableAssignments);
}
public function requireHelper($name)
{
$provider = $this->formatter
->getDependencies()
->getProvider(
$this->helperName('available_attribute_assignments')
);
$required = $provider->isRequired();
parent::requireHelper($name);
if (!$required && $provider->isRequired()) {
foreach ($this->getHelper('available_attribute_assignments') as $assignment) {
$this->requireHelper($assignment.'_attribute_assignment');
}
}
return $this;
}
public function __invoke(ElementInterface $element)
{
return $this->format($element);
}
protected function isSelfClosingTag(MarkupInterface $element, $isSelfClosing = null)
{
if (is_null($isSelfClosing)) {
$isSelfClosing = $element->isAutoClosed();
}
if ($isSelfClosing && $element->hasChildren()) {
$visibleChildren = array_filter($element->getChildren(), function ($child) {
return $child && (
!($child instanceof TextElement) ||
trim($child->getValue()) !== ''
);
});
if (count($visibleChildren) > 0) {
$this->throwException(
$element->getName().' is a self closing element: '.
'<'.$element->getName().'/> but contains nested content.',
$element
);
}
}
return $isSelfClosing;
}
protected function isBlockTag(MarkupInterface $element)
{
return true;
}
public function isWhiteSpaceSensitive(MarkupInterface $element)
{
return false;
}
protected function hasNonStaticAttributes(MarkupInterface $element)
{
if ($element instanceof MarkupElement || $element instanceof MixinCallElement) {
foreach ($element->getAttributes() as $attribute) {
if ($attribute->hasStaticMember('value')) {
continue;
}
if ($attribute->getValue() instanceof ExpressionElement &&
$attribute->getValue()->hasStaticMember('value')) {
continue;
}
return true;
}
}
return false;
}
protected function formatAttributeElement(AttributeElement $element)
{
$value = $element->getValue();
$name = $element->getName();
$nonEmptyAttribute = ($name === 'class' || $name === 'id');
if ($nonEmptyAttribute && (
!$value ||
($value instanceof TextElement && ((string) $value->getValue()) === '') ||
(is_string($value) && in_array(trim($value), ['', '""', "''"], true))
)) {
return '';
}
if ($value instanceof ExpressionElement) {
if ($nonEmptyAttribute && in_array(trim($value->getValue()), ['', '""', "''"], true)) {
return '';
}
if (strtolower($value->getValue()) === 'true') {
$formattedValue = null;
if ($name instanceof ExpressionElement) {
$bufferVariable = $this->pattern('buffer_variable');
$name = $this->pattern(
'php_display_code',
$this->pattern(
'save_value',
$bufferVariable,
$this->formatCode($name->getValue(), $name->isChecked())
)
);
$value = new ExpressionElement($bufferVariable);
$formattedValue = $this->format($value);
}
$formattedName = $this->format($name);
$formattedValue = $formattedValue || $formattedValue === '0'
? $formattedValue
: $formattedName;
return $this->pattern(
'boolean_attribute_pattern',
$formattedName,
$formattedValue
);
}
if (in_array(strtolower($value->getValue()), ['false', 'null', 'undefined'], true)) {
return '';
}
}
return $this->pattern(
'attribute_pattern',
$this->format($name),
$this->format($value)
);
}
protected function formatPairTagChildren(MarkupElement $element)
{
$firstChild = $element->getChildAt(0);
$needIndent = (
(
(
$firstChild instanceof CodeElement &&
$this->isBlockTag($element)
) || (
$firstChild instanceof MarkupInterface &&
$this->isBlockTag($firstChild)
)
) &&
!$this->isWhiteSpaceSensitive($element)
);
return sprintf(
$needIndent
? $this->getNewLine().'%s'.$this->getIndent()
: '%s',
$this->formatElementChildren($element)
);
}
protected function formatPairTag($open, $close, MarkupElement $element)
{
return $this->pattern(
'pair_tag',
$open,
$element->hasChildren()
? $this->formatPairTagChildren($element)
: '',
$close
);
}
/**
* @param AssignmentElement $element
*
* @throws FormatterException
*
* @return iterable
*/
protected function yieldAssignmentElement(AssignmentElement $element)
{
foreach ($this->getOption('assignment_handlers') as $handler) {
$iterator = $handler($element) ?: [];
foreach ($iterator as $newElement) {
yield $newElement;
}
}
/* @var MarkupElement $markup */
$markup = $element->getContainer();
$attributeOrder = $this->hasOption('attribute_precedence')
? $this->getOption('attribute_precedence')
: 'assignment';
switch ($attributeOrder) {
case 'assignment':
case 'assignments':
$arguments = array_merge(
$markup instanceof AttributesInterface
? $this->formatMarkupAttributes($markup)
: [],
$markup instanceof AssignmentContainerInterface
? $this->formatAttributeAssignments($markup)
: []
);
break;
case 'attribute':
case 'attributes':
$arguments = array_merge(
$markup instanceof AssignmentContainerInterface
? $this->formatAttributeAssignments($markup)
: [],
$markup instanceof AttributesInterface
? $this->formatMarkupAttributes($markup)
: []
);
break;
case 'left':
$arguments = $this->getSortedAttributes($markup, static function (OrderedValue $a, OrderedValue $b) {
return $b->getOrder() - $a->getOrder();
});
break;
case 'right':
$arguments = $this->getSortedAttributes($markup, static function (OrderedValue $a, OrderedValue $b) {
return $a->getOrder() - $b->getOrder();
});
break;
default:
if (!is_callable($attributeOrder)) {
throw new InvalidArgumentException(
'Option attribute_precedence must be '.
'"assignment" (default), "attribute", "left", "right" or a callable.'
);
}
$arguments = array_map(static function ($argument) {
return $argument instanceof OrderedValue ? $argument->getValue() : $argument;
}, $attributeOrder(
$markup instanceof AssignmentContainerInterface
? $this->formatOrderedAttributeAssignments($markup)
: [],
$markup instanceof AttributesInterface
? $this->formatOrderedMarkupAttributes($markup)
: []
));
}
foreach ($markup->getAssignments() as $assignment) {
/* @var AssignmentElement $assignment */
$this->throwException(
'Unable to handle '.$assignment->getName().' assignment',
$assignment
);
}
if (count($arguments)) {
yield $this->attributesAssignmentsFromPairs($arguments);
}
}
/**
* @param AssignmentContainerInterface $markup
*
* @return array<string>
*/
protected function formatAttributeAssignments(AssignmentContainerInterface $markup)
{
$arguments = [];
foreach ($this->yieldAssignmentAttributes($markup) as $attribute) {
$arguments[] = $this->formatInnerCodeValue($attribute);
}
return $arguments;
}
/**
* @param AssignmentContainerInterface $markup
*
* @return list<OrderedValue<string>>
*/
protected function formatOrderedAttributeAssignments(AssignmentContainerInterface $markup)
{
$arguments = [];
foreach ($this->yieldAssignmentOrderedAttributes($markup) as $attribute => $order) {
$arguments[] = new OrderedValue($this->formatInnerCodeValue($attribute), $order);
}
return $arguments;
}
/**
* @param AbstractValueElement|mixed $value
*
* @return string
*/
protected function formatInnerCodeValue($value)
{
$checked = method_exists($value, 'isChecked') && $value->isChecked();
while (method_exists($value, 'getValue')) {
$value = $value->getValue();
}
return $this->formatCode($value, $checked);
}
/**
* @param AssignmentContainerInterface $markup
*
* @return Generator<AbstractValueElement>
*/
protected function yieldAssignmentAttributes(AssignmentContainerInterface $markup)
{
foreach ($markup->getAssignmentsByName('attributes') as $attributesAssignment) {
/* @var AssignmentElement $attributesAssignment */
foreach ($attributesAssignment->getAttributes() as $attribute) {
/* @var AbstractValueElement $attribute */
yield $attribute;
}
$markup->removedAssignment($attributesAssignment);
}
}
/**
* @param AssignmentContainerInterface $markup
*
* @return Generator<AbstractValueElement, int|null>
*/
protected function yieldAssignmentOrderedAttributes(AssignmentContainerInterface $markup)
{
foreach ($markup->getAssignmentsByName('attributes') as $attributesAssignment) {
/* @var AssignmentElement $attributesAssignment */
foreach ($attributesAssignment->getAttributes() as $attribute) {
/* @var AbstractValueElement $attribute */
yield $attribute => $attributesAssignment->getOrder();
}
$markup->removedAssignment($attributesAssignment);
}
}
/**
* @param AttributesInterface $markup
*
* @return list<string>
*/
protected function formatMarkupAttributes(AttributesInterface $markup)
{
$arguments = [];
$attributes = $markup->getAttributes();
foreach ($attributes as $attribute) {
/* @var AttributeElement $attribute */
$arguments[] = $this->formatAttributeAsArrayItem($attribute);
}
$attributes->removeAll($attributes);
return $arguments;
}
/**
* @param AttributesInterface $markup
*
* @return list<OrderedValue<string>>
*/
protected function formatOrderedMarkupAttributes(AttributesInterface $markup)
{
$arguments = [];
$attributes = $markup->getAttributes();
foreach ($attributes as $attribute) {
/* @var AttributeElement $attribute */
$arguments[] = new OrderedValue($this->formatAttributeAsArrayItem($attribute), $attribute->getOrder());
}
$attributes->removeAll($attributes);
return $arguments;
}
/**
* @param AssignmentElement $element
*
* @throws FormatterException
*
* @return string
*/
protected function formatAssignmentElement(AssignmentElement $element)
{
return (new Joiner($this->yieldAssignmentElement($element)))->mapAndJoin([$this, 'format'], '');
}
protected function hasDuplicateAttributeNames(MarkupInterface $element)
{
if ($element instanceof MarkupElement || $element instanceof MixinCallElement) {
$names = [];
foreach ($element->getAttributes() as $attribute) {
$name = $attribute->getName();
if (($name instanceof ExpressionElement && !$name->hasStaticValue()) ||
in_array($name, $names, true)
) {
return true;
}
$names[] = $name;
}
}
return false;
}
protected function formatAttributes(MarkupElement $element)
{
if ($this->hasNonStaticAttributes($element) ||
$this->hasDuplicateAttributeNames($element)) {
$empty = true;
foreach ($element->getAssignmentsByName('attributes') as $attribute) {
$empty = false;
break;
}
if ($empty) {
$data = new SplObjectStorage();
$data->attach(new ExpressionElement('[]'));
$element->addAssignment(new AssignmentElement('attributes', $data, $element));
}
}
foreach ($element->getAssignments() as $assignment) {
return $this->format($assignment);
}
$code = '';
foreach ($element->getAttributes() as $attribute) {
$code .= $this->format($attribute);
}
return $code;
}
protected function formatMarkupElement(MarkupElement $element)
{
$tag = $this->format($element->getName());
$saveAttributes = clone $element->getAttributes();
$saveAssignments = clone $element->getAssignments();
$attributes = $this->formatAttributes($element);
$dirtyAttributes = $element->getAttributes();
$dirtyAttributes->removeAll($dirtyAttributes);
$dirtyAttributes->addAll($saveAttributes);
$dirtyAssignments = $element->getAssignments();
$dirtyAssignments->removeAll($dirtyAssignments);
$dirtyAssignments->addAll($saveAssignments);
$tag = $this->isSelfClosingTag($element)
? $this->pattern(
$element->isAutoClosed() && $this->hasPattern('explicit_closing_tag')
? 'explicit_closing_tag'
: 'self_closing_tag',
$tag.$attributes
)
: $this->formatPairTag(
$this->pattern('open_pair_tag', $tag.$attributes),
$this->pattern('close_pair_tag', $tag),
$element
);
return !$element->isAutoClosed() && $this->isBlockTag($element)
? $this->getIndent().$tag.$this->getNewLine()
: $tag;
}
/**
* @param AssignmentContainerInterface|AttributesInterface|mixed $markup
* @param Closure(OrderedValue, OrderedValue): int $sorter
*
* @return list<string>
*/
private function getSortedAttributes($markup, Closure $sorter)
{
$arguments = array_merge(
$markup instanceof AssignmentContainerInterface
? $this->formatOrderedAttributeAssignments($markup)
: [],
$markup instanceof AttributesInterface
? $this->formatOrderedMarkupAttributes($markup)
: []
);
usort($arguments, $sorter);
return array_map(static function (OrderedValue $value) {
return $value->getValue();
}, $arguments);
}
}