Skip to content

Commit

Permalink
New namspaces system
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Sep 16, 2024
1 parent dec15b1 commit 58ee298
Show file tree
Hide file tree
Showing 37 changed files with 356 additions and 414 deletions.
4 changes: 2 additions & 2 deletions docs/dom.md
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ This function returns a list of all namespaces that are linked to a specific DOM
use VeeWee\Xml\Dom\Collection\NodeList;
use function VeeWee\Xml\Dom\Locator\Xmlns\linked_namespaces;

/** @var NodeList<DOMNameSpaceNode> $namespaces */
/** @var array<string, string> $namespaces - A lookup of prefix -> namespace */
$namespaces = linked_namespaces($element);
```

Expand All @@ -908,7 +908,7 @@ This function returns a list of all namespaces that are linked to a specific DOM
use VeeWee\Xml\Dom\Collection\NodeList;
use function VeeWee\Xml\Dom\Locator\Xmlns\recursive_linked_namespaces;

/** @var NodeList<DOMNameSpaceNode> $namespaces */
/** @var array<string, string> $namespaces - A lookup of prefix -> namespace */
$namespaces = recursive_linked_namespaces($element);
```

Expand Down
5 changes: 1 addition & 4 deletions src/Xml/Dom/Builder/value.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@

use Closure;
use \DOM\Element;
use function VeeWee\Xml\Dom\Locator\Node\detect_document;

/**
* @return Closure(\DOM\Element): \DOM\Element
*/
function value(string $value): Closure
{
return static function (\DOM\Element $node) use ($value): \DOM\Element {
$document = detect_document($node);
$text = $document->createTextNode($value);
$node->appendChild($text);
$node->substitutedNodeValue = $value;

return $node;
};
Expand Down
16 changes: 11 additions & 5 deletions src/Xml/Dom/Configurator/pretty_print.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
namespace VeeWee\Xml\Dom\Configurator;

use Closure;
use \DOM\XMLDocument;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Loader\xml_string_loader;

/**
* @return Closure(\DOM\XMLDocument): \DOM\XMLDocument
*/
function pretty_print(): Closure
{
return static function (\DOM\XMLDocument $document): \DOM\XMLDocument {
// TODO : not fully implemented yet in the new API
//$document->preserveWhiteSpace = false;
$document->formatOutput = true;
$trimmed = Document::fromLoader(
xml_string_loader(
Document::fromUnsafeDocument($document)->toXmlString(),
LIBXML_NOBLANKS
)
)->toUnsafeDocument();

return $document;
$trimmed->formatOutput = true;

return $trimmed;
};
}
15 changes: 11 additions & 4 deletions src/Xml/Dom/Configurator/trim_spaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@

use Closure;
use \DOM\XMLDocument;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Loader\xml_string_loader;

/**
* @return Closure(\DOM\XMLDocument): \DOM\XMLDocument
*/
function trim_spaces(): Closure
{
return static function (\DOM\XMLDocument $document): \DOM\XMLDocument {
// TODO : not fully implemented yet in the new API
//$document->preserveWhiteSpace = false;
$document->formatOutput = false;
$trimmed = Document::fromLoader(
xml_string_loader(
Document::fromUnsafeDocument($document)->toXmlString(),
LIBXML_NOBLANKS
)
)->toUnsafeDocument();

return $document;
$trimmed->formatOutput = false;

return $trimmed;
};
}
15 changes: 4 additions & 11 deletions src/Xml/Dom/Locator/Attribute/xmlns_attributes_list.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,16 @@

namespace VeeWee\Xml\Dom\Locator\Attribute;

use \DOM\NameSpaceNode;
use \DOM\Node;
use VeeWee\Xml\Dom\Collection\NodeList;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Locator\Xmlns\linked_namespaces;
use function VeeWee\Xml\Dom\Predicate\is_element;
use function VeeWee\Xml\Dom\Predicate\is_xmlns_attribute;

/**
* @return NodeList<\DOM\NameSpaceNode>
* @return NodeList<\DOM\Attr>
* @throws RuntimeException
*/
function xmlns_attributes_list(\DOM\Node $node): NodeList
{
if (! is_element($node)) {
return NodeList::empty();
}

return linked_namespaces($node)
->filter(static fn (\DOM\NameSpaceNode $namespace): bool => $node->hasAttribute($namespace->nodeName));
return attributes_list($node)
->filter(static fn (\DOM\Attr $attribute): bool => is_xmlns_attribute($attribute));
}
5 changes: 1 addition & 4 deletions src/Xml/Dom/Locator/Node/value.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,5 @@
*/
function value(\DOM\Node $node, TypeInterface $type)
{
// TODO : nodeValue did entity substitution
// TODO : nodeValue returns null for elements
// TODO : How to best deal with this?
return $type->coerce($node->textContent ?? '');
return $type->coerce($node->substitutedNodeValue ?? '');
}
18 changes: 3 additions & 15 deletions src/Xml/Dom/Locator/Xmlns/linked_namespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,10 @@

namespace VeeWee\Xml\Dom\Locator\Xmlns;

use \DOM\NameSpaceNode;
use \DOM\Node;
use InvalidArgumentException;
use VeeWee\Xml\Dom\Collection\NodeList;
use VeeWee\Xml\Dom\Xpath;
use VeeWee\Xml\Exception\RuntimeException;

/**
* @return NodeList<\DOM\NameSpaceNode>
*
* @throws RuntimeException
* @throws InvalidArgumentException
* @return list<\DOM\NamespaceInfo>
*/
function linked_namespaces(\DOM\Node $node): NodeList
function linked_namespaces(\DOM\Element $node): array
{
$xpath = Xpath::fromUnsafeNode($node);

return $xpath->query('./namespace::*', $node)->expectAllOfType(\DOM\NameSpaceNode::class);
return $node->getInScopeNamespaces();
}
18 changes: 3 additions & 15 deletions src/Xml/Dom/Locator/Xmlns/recursive_linked_namespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,10 @@

namespace VeeWee\Xml\Dom\Locator\Xmlns;

use \DOM\NameSpaceNode;
use \DOM\Node;
use InvalidArgumentException;
use VeeWee\Xml\Dom\Collection\NodeList;
use VeeWee\Xml\Dom\Xpath;
use VeeWee\Xml\Exception\RuntimeException;

/**
* @return NodeList<\DOM\NameSpaceNode>
*
* @throws RuntimeException
* @throws InvalidArgumentException
* @return list<\DOM\NamespaceInfo>
*/
function recursive_linked_namespaces(\DOM\Node $node): NodeList
function recursive_linked_namespaces(\DOM\Element $node): array
{
$xpath = Xpath::fromUnsafeNode($node);

return $xpath->query('.//namespace::*', $node)->expectAllOfType(\DOM\NameSpaceNode::class);
return $node->getDescendantNamespaces();
}
38 changes: 8 additions & 30 deletions src/Xml/Dom/Manipulator/Attribute/rename.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,18 @@

use \DOM\Attr;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Builder\attribute;
use function VeeWee\Xml\Dom\Builder\namespaced_attribute;
use function VeeWee\Xml\Dom\Locator\Element\parent_element;
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
use function VeeWee\Xml\Dom\Predicate\is_attribute;
use function Psl\Fun\tap;
use function VeeWee\Xml\Dom\Manipulator\Xmlns\rename as rename_xmlns_attribute;
use function VeeWee\Xml\Dom\Predicate\is_xmlns_attribute;
use function VeeWee\Xml\ErrorHandling\disallow_issues;

/**
* @throws RuntimeException
*/
function rename(\DOM\Attr $target, string $newQName, ?string $newNamespaceURI = null): \DOM\Attr
{
$element = parent_element($target);
$namespace = $newNamespaceURI ?? $target->namespaceURI;
$value = $target->nodeValue ?? '';

$builder = $namespace !== null
? namespaced_attribute($namespace, $newQName, $value)
: attribute($newQName, $value);

remove($target);
$builder($element);

// If the namespace prefix of the target still exists, PHP will fallback into using that prefix.
// In that case it is not possible to fully rename an attribute.
// If you want to rename a prefix, you'll have to remove the xmlns first
// or make sure the new prefix is found first for the given namespace URI.
$result = $element->getAttributeNode($newQName);

/** @psalm-suppress TypeDoesNotContainType - It can actually be null if the exact node name is not found. */
if (!$result || !is_attribute($result)) {
throw RuntimeException::withMessage(
'Unable to rename attribute '.$target->nodeName.' into '.$newQName.'. You might need to swap xmlns prefix first!'
);
}

return $result;
return disallow_issues(static fn (): \DOM\Attr => match(true) {
is_xmlns_attribute($target) => rename_xmlns_attribute($target, $newQName),
default => tap(fn () => $target->rename($newNamespaceURI, $newQName))($target)
});
}
24 changes: 8 additions & 16 deletions src/Xml/Dom/Manipulator/Document/optimize_namespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,26 @@

namespace VeeWee\Xml\Dom\Manipulator\Document;

use \DOM\XMLDocument;
use \DOM\NameSpaceNode;
use VeeWee\Xml\Exception\RuntimeException;
use VeeWee\Xml\Xmlns\Xmlns;
use function Psl\Dict\unique;
use function Psl\Vec\map;
use function Psl\Vec\sort;
use function Psl\Vec\values;
use function VeeWee\Xml\Dom\Locator\Xmlns\recursive_linked_namespaces;
use function VeeWee\Xml\Dom\Manipulator\Xmlns\rename;
use function VeeWee\Xml\Dom\Manipulator\Xmlns\rename_element_namespace;

/**
* @throws RuntimeException
*/
function optimize_namespaces(\DOM\XMLDocument $document, string $prefix = 'ns'): void
{
$namespaceURIs = recursive_linked_namespaces($document)
->filter(static fn (\DOM\NameSpaceNode $node): bool => $node->namespaceURI !== Xmlns::xml()->value())
->reduce(
/**
* @param list<string> $grouped
* @return list<string>
*/
static fn (array $grouped, \DOM\NameSpaceNode $node): array
=> values(unique([...$grouped, $node->namespaceURI])),
[]
);
$documentElement = $document->documentElement;
$namespaceURIs = values(unique(map(
recursive_linked_namespaces($documentElement),
static fn (\DOM\NamespaceInfo $info): string => $info->namespaceURI
)));

foreach (sort($namespaceURIs) as $index => $namespaceURI) {
rename($document, $namespaceURI, $prefix . ((string) ($index+1)));
rename_element_namespace($documentElement, $namespaceURI, $prefix . ((string) ($index+1)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@

namespace VeeWee\Xml\Dom\Manipulator\Element;

use \DOM\Element;
use \DOM\NameSpaceNode;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Builder\xmlns_attribute;
use function VeeWee\Xml\Dom\Locator\Xmlns\linked_namespaces;
use function VeeWee\Xml\Dom\Locator\Attribute\xmlns_attributes_list;

/**
* @throws RuntimeException
*/
function copy_named_xmlns_attributes(\DOM\Element $target, \DOM\Element $source): void
{
linked_namespaces($source)->forEach(static function (\DOM\NameSpaceNode $xmlns) use ($target) {
xmlns_attributes_list($source)->forEach(static function (\DOM\Attr $xmlns) use ($target) {
if ($xmlns->prefix && !$target->hasAttribute($xmlns->nodeName)) {
xmlns_attribute($xmlns->prefix, $xmlns->namespaceURI)($target);
xmlns_attribute($xmlns->localName, $xmlns->value)($target);
}
});
}
60 changes: 20 additions & 40 deletions src/Xml/Dom/Manipulator/Element/rename.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,31 @@

namespace VeeWee\Xml\Dom\Manipulator\Element;

use \DOM\NameSpaceNode;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Builder\element;
use function VeeWee\Xml\Dom\Builder\namespaced_element;
use function VeeWee\Xml\Dom\Builder\xmlns_attribute;
use function VeeWee\Xml\Dom\Locator\Attribute\attributes_list;
use function VeeWee\Xml\Dom\Locator\Attribute\xmlns_attributes_list;
use function VeeWee\Xml\Dom\Locator\Element\parent_element;
use function VeeWee\Xml\Dom\Locator\Node\children;
use function VeeWee\Xml\Dom\Manipulator\append;
use function VeeWee\Xml\Dom\Predicate\is_default_xmlns_attribute;
use function VeeWee\Xml\ErrorHandling\disallow_issues;

/**
* @throws RuntimeException
*/
function rename(\DOM\Element $target, string $newQName, ?string $newNamespaceURI = null): \DOM\Element
{
$isRootElement = $target === $target->ownerDocument->documentElement;
$parent = $isRootElement ? $target->ownerDocument : parent_element($target);
$namespace = $newNamespaceURI ?? $target->namespaceURI;
$builder = $namespace !== null
? namespaced_element($namespace, $newQName)
: element($newQName);

$newElement = $builder($parent);

append(...children($target))($newElement);

xmlns_attributes_list($target)->forEach(
static function (\DOM\NameSpaceNode $attribute) use ($target, $newElement): void {
if (is_default_xmlns_attribute($attribute) || $target->prefix === $attribute->prefix) {
return;
}
xmlns_attribute($attribute->prefix, $attribute->namespaceURI)($newElement);
}
);

attributes_list($target)->forEach(
static function (\DOM\Attr $attribute) use ($target, $newElement): void {
$target->removeAttributeNode($attribute);
$newElement->setAttributeNode($attribute);
}
);

$parent->replaceChild($newElement, $target);

return $newElement;
$parts = explode(':', $newQName, 2);
$newPrefix = $parts[0] ?? '';

/*
* To make sure the new namespace prefix is being used, we need to apply an additional xmlns declaration chech:
* This is due to a particular rule in the XML serialization spec,
* that enforces that a namespaceURI on an element is only associated with exactly one prefix.
* See the note of bullet point 2 of https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml.
*
* If you rename a:xx to b:xx an xmlns:b="xx" attribute gets added at the end, but prefix a: will still be serialized.
* So in this case, we need to remove the xmlns declaration first.
*/
if ($newPrefix && $newPrefix !== $target->prefix && $target->hasAttribute('xmlns:'.$target->prefix)) {
$target->removeAttribute('xmlns:'.$target->prefix);
}

disallow_issues(static fn () => $target->rename($newNamespaceURI, $newQName));

return $target;
}
Loading

0 comments on commit 58ee298

Please sign in to comment.