Skip to content

Commit

Permalink
Merge pull request #88 from veewee/xmlns-inheriting-encoding
Browse files Browse the repository at this point in the history
Inherit XMLNS during encoding like we did in v3
  • Loading branch information
veewee authored Jan 26, 2025
2 parents 86a8497 + c7b1171 commit a732fcd
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 21 deletions.
25 changes: 25 additions & 0 deletions docs/dom.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ element('foo',
<foo hello="world" bar="baz" />
```

#### default_xmlns_attribute

Operates on a `Dom\Element` and adds a default xmlns attribute.
Given how XML serialization works in PHP, this function only works on already prefixed + namespaced elements:

```php
use function VeeWee\Xml\Dom\Builder\namespaced_element;
use function VeeWee\Xml\Dom\Builder\default_xmlns_attribute;

namespaced_element('uri://x', x:hello', default_xmlns_attribute(http://default'));
```

#### cdata

Operates on a `Dom\Node` and creates a `Dom\CDATASection`.
Expand Down Expand Up @@ -1289,6 +1301,19 @@ if (is_non_empty_text($someNode)) {
}
```

#### is_prefixed_node_name

Checks if a given node name is prefixed or not.
This will validate for pattern `^[^:]+:[^:]+$` to make sure that there are no multiple colons in the node name and that all parts are set.

```php
use function VeeWee\Xml\Dom\Predicate\is_prefixed_node_name;

if (is_prefixed_node_name('prefixed:nodeName')) {
// ...
}
```

#### is_text

Checks if a node is of type `Dom\Text`.
Expand Down
21 changes: 21 additions & 0 deletions src/Xml/Dom/Builder/default_xmlns_attribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Builder;

use Closure;
use Dom\Element;
use VeeWee\Xml\Xmlns\Xmlns;

/**
* @return Closure(Element): Element
*/
function default_xmlns_attribute(string $namespaceURI): Closure
{
return static function (Element $node) use ($namespaceURI): Element {
$node->setAttributeNS(Xmlns::xmlns()->value(), 'xmlns', $namespaceURI);

return $node;
};
}
7 changes: 3 additions & 4 deletions src/Xml/Dom/Builder/nodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@

use Closure;
use Dom\Node;
use Dom\XMLDocument;
use function is_array;
use function Psl\Iter\reduce;
use function VeeWee\Xml\Dom\Locator\Node\detect_document;

/**
* @param list<callable(XMLDocument): (list<Node>|Node)> $builders
* @param list<callable(Node): (list<Node>|Node)> $builders
*
* @return Closure(XMLDocument): list<Node>
* @return Closure(Node): list<Node>
*/
function nodes(callable ... $builders): Closure
{
Expand All @@ -27,7 +26,7 @@ function nodes(callable ... $builders): Closure
$builders,
/**
* @param list<Node> $builds
* @param callable(XMLDocument): (Node|list<Node>) $builder
* @param callable(Node): (Node|list<Node>) $builder
* @return list<Node>
*/
static function (array $builds, callable $builder) use ($node): array {
Expand Down
2 changes: 2 additions & 0 deletions src/Xml/Dom/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ public function manipulate(callable $manipulator): self
}

/**
* @psalm-suppress ArgumentTypeCoercion - nodes() works on node but we provide the parent type XMLDocument.
*
* @param list<callable(XMLDocument): (list<Node>|Node)> $builders
*
* @return list<Node>
Expand Down
10 changes: 10 additions & 0 deletions src/Xml/Dom/Predicate/is_prefixed_node_name.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Predicate;

function is_prefixed_node_name(string $nodeName): bool
{
return (bool)preg_match('/^[^:]+:[^:]+$/', $nodeName);
}
9 changes: 3 additions & 6 deletions src/Xml/Encoding/Internal/Decoder/Builder/namespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ function namespaces(Element $element): array
{
return filter([
'@namespaces' => xmlns_attributes_list($element)->reduce(
static fn (array $namespaces, Attr $node)
=> $node->value
? merge($namespaces, [
($node->prefix !== null ? $node->localName : '') => $node->value
])
: $namespaces,
static fn (array $namespaces, Attr $node) => merge($namespaces, [
($node->prefix !== null ? $node->localName : '') => $node->value
]),
[]
),
]);
Expand Down
3 changes: 1 addition & 2 deletions src/Xml/Encoding/Internal/Encoder/Builder/children.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Dom\Element;
use function Psl\Dict\map;
use function VeeWee\Xml\Dom\Builder\children as buildChildren;
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
use function VeeWee\Xml\Dom\Builder\value;

/**
Expand All @@ -28,7 +27,7 @@ function children(string $name, array $children): Closure
*/
static fn (array|string $data): Closure => is_array($data)
? element($name, $data)
: elementBuilder($name, value($data))
: xmlns_inheriting_element($name, [value($data)])
)
);
}
7 changes: 1 addition & 6 deletions src/Xml/Encoding/Internal/Encoder/Builder/element.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
use function VeeWee\Xml\Dom\Builder\attributes;
use function VeeWee\Xml\Dom\Builder\cdata;
use function VeeWee\Xml\Dom\Builder\children as childrenBuilder;
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
use function VeeWee\Xml\Dom\Builder\escaped_value;
use function VeeWee\Xml\Dom\Builder\namespaced_element as namespacedElementBuilder;
use function VeeWee\Xml\Dom\Builder\xmlns_attributes;

/**
Expand All @@ -46,7 +44,6 @@ function element(string $name, array $data): Closure
static fn (string $key): bool => !in_array($key, ['@attributes', '@namespaces', '@value', '@cdata'], true)
);

$currentNamespace = $namespaces[''] ?? null;
$namedNamespaces = filter_keys($namespaces ?? []);

/** @var list<Closure(Element): Element> $children */
Expand All @@ -66,7 +63,5 @@ function element(string $name, array $data): Closure
)),
]);

return $currentNamespace !== null
? namespacedElementBuilder($currentNamespace, $name, ...$children)
: elementBuilder($name, ...$children);
return xmlns_inheriting_element($name, $children, $namespaces);
}
3 changes: 1 addition & 2 deletions src/Xml/Encoding/Internal/Encoder/Builder/parent_node.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Psl\Exception\InvariantViolationException;
use Psl\Type\Exception\AssertException;
use function VeeWee\Xml\Dom\Builder\children as buildChildren;
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
use function VeeWee\Xml\Dom\Builder\escaped_value;

/**
Expand All @@ -25,7 +24,7 @@
function parent_node(string $name, array|string $data): Closure
{
if (is_string($data)) {
return buildChildren(elementBuilder($name, escaped_value($data)));
return buildChildren(xmlns_inheriting_element($name, [escaped_value($data)]));
}

if (is_node_list($data)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Encoding\Internal\Encoder\Builder;

use Closure;
use Dom\Element;
use Dom\XMLDocument;
use Webmozart\Assert\Assert;
use function VeeWee\Xml\Dom\Builder\default_xmlns_attribute;
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
use function VeeWee\Xml\Dom\Builder\namespaced_element as namespacedElementBuilder;
use function VeeWee\Xml\Dom\Predicate\is_element;
use function VeeWee\Xml\Dom\Predicate\is_prefixed_node_name;

/**
* This function can create element nodes that inherit the local xmlns namespace of their parent if none is configured.
*
* @param list<Closure(Element): Element> $children
* @param array<string, string> $namespaces
*
* @return Closure(Element): Element
*/
function xmlns_inheriting_element(string $name, array $children, ?array $namespaces = []): Closure
{
return static function (XMLDocument|Element $parent) use ($namespaces, $name, $children): Element {

$defaultNamespace = $namespaces[''] ?? null;

// These rules apply for non prefixed elements only:
// If no local namespace has been defined: lookup the default local namespace of the closest parent element.
// Use that specific local namespace to create the element if one could be found.
// Otherwise, just create a non-namespaced element.
if (!is_prefixed_node_name($name)) {
// Try to find the inherited default XMLNS for non prefixed elements without a desired local namespace.
if ($defaultNamespace === null && is_element($parent)) {
$defaultNamespace = $parent->lookupNamespaceURI('');
}

return $defaultNamespace !== null
? namespacedElementBuilder($defaultNamespace, $name, ...$children)($parent)
: elementBuilder($name, ...$children)($parent);
}

// Prefixed elements can be created as regular elements:
// The configured xmlns attributes will be added by the $children.
// If a local namespace is configured, make sure to register it on the node manually.
[$prefix] = explode(':', $name);
$prefixedNamespace = $namespaces[$prefix] ?? (is_element($parent) ? $parent->lookupNamespaceURI($prefix) : null);

Assert::notNull($prefixedNamespace, 'No namespace URI could be found for prefix: '.$prefix);

$defaultXmlns = $defaultNamespace !== null ? [default_xmlns_attribute($defaultNamespace)] : [];
return namespacedElementBuilder(
$prefixedNamespace,
$name,
...$defaultXmlns,
...$children,
)($parent);
};
}
3 changes: 3 additions & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'Xml\Dom\Builder\attributes' => __DIR__.'/Xml/Dom/Builder/attributes.php',
'Xml\Dom\Builder\cdata' => __DIR__.'/Xml/Dom/Builder/cdata.php',
'Xml\Dom\Builder\children' => __DIR__.'/Xml/Dom/Builder/children.php',
'Xml\Dom\Builder\default_xmlns_attribute' => __DIR__.'/Xml/Dom/Builder/default_xmlns_attribute.php',
'Xml\Dom\Builder\element' => __DIR__.'/Xml/Dom/Builder/element.php',
'Xml\Dom\Builder\escaped_value' => __DIR__.'/Xml/Dom/Builder/escaped_value.php',
'Xml\Dom\Builder\namespaced_attribute' => __DIR__.'/Xml/Dom/Builder/namespaced_attribute.php',
Expand Down Expand Up @@ -79,6 +80,7 @@
'Xml\Dom\Predicate\is_document_element' => __DIR__.'/Xml/Dom/Predicate/is_document_element.php',
'Xml\Dom\Predicate\is_element' => __DIR__.'/Xml/Dom/Predicate/is_element.php',
'Xml\Dom\Predicate\is_non_empty_text' => __DIR__.'/Xml/Dom/Predicate/is_non_empty_text.php',
'Xml\Dom\Predicate\is_prefixed_node_name' => __DIR__.'/Xml/Dom/Predicate/is_prefixed_node_name.php',
'Xml\Dom\Predicate\is_text' => __DIR__.'/Xml/Dom/Predicate/is_text.php',
'Xml\Dom\Predicate\is_whitespace' => __DIR__.'/Xml/Dom/Predicate/is_whitespace.php',
'Xml\Dom\Predicate\is_xmlns_attribute' => __DIR__.'/Xml/Dom/Predicate/is_xmlns_attribute.php',
Expand Down Expand Up @@ -107,6 +109,7 @@
'Xml\Encoding\Internal\Encoder\Builder\normalize_data' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/normalize_data.php',
'Xml\Encoding\Internal\Encoder\Builder\parent_node' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/parent_node.php',
'Xml\Encoding\Internal\Encoder\Builder\root' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/root.php',
'Xml\Encoding\Internal\Encoder\Builder\xmlns_inheriting_element' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/xmlns_inheriting_element.php',
'Xml\Encoding\Internal\wrap_exception' => __DIR__.'/Xml/Encoding/Internal/wrap_exception.php',
'Xml\Encoding\document_encode' => __DIR__.'/Xml/Encoding/document_encode.php',
'Xml\Encoding\element_decode' => __DIR__.'/Xml/Encoding/element_decode.php',
Expand Down
39 changes: 39 additions & 0 deletions tests/Xml/Dom/Builder/DefaultXmlnsAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Builder;

use PHPUnit\Framework\TestCase;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Builder\default_xmlns_attribute;
use function VeeWee\Xml\Dom\Builder\element;
use function VeeWee\Xml\Dom\Builder\namespaced_element;

final class DefaultXmlnsAttributeTest extends TestCase
{
public function test_it_can_build_an_element_with_default_xmlns_on_namespaced_element(): void
{
$doc = Document::empty()->toUnsafeDocument();

$node = namespaced_element(
'uri://x',
'x:foo',
default_xmlns_attribute('uri://default')
)($doc);

static::assertSame('<x:foo xmlns:x="uri://x" xmlns="uri://default"/>', $doc->saveXml($node));
}

public function test_it_can_not_build_an_element_with_default_xmlns_on_regular_element(): void
{
$doc = Document::empty()->toUnsafeDocument();

$node = element(
'foo',
default_xmlns_attribute('uri://default')
)($doc);

static::assertSame('<foo/>', $doc->saveXml($node));
}
}
45 changes: 45 additions & 0 deletions tests/Xml/Dom/Predicate/IsPrefixedNodeNameTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Predicate;

use PHPUnit\Framework\TestCase;
use function VeeWee\Xml\Dom\Predicate\is_prefixed_node_name;

final class IsPrefixedNodeNameTest extends TestCase
{
/**
*
* @dataProvider provideValidQNames
*/
public function test_it_does_nothing_on_valid_qnames(string $input): void
{
static::assertTrue(is_prefixed_node_name($input));
}

/**
*
* @dataProvider provideInvalidQNames
*/
public function test_it_throws_on_invalid_qnames(string $input): void
{
static::assertFalse(is_prefixed_node_name($input));
}

public static function provideValidQNames()
{
yield ['hello:world'];
yield ['a:b'];
yield ['---a----:----b---'];
}

public static function provideInvalidQNames()
{
yield [''];
yield ['aa'];
yield ['aa:'];
yield [':bb'];
yield [':b:c:cd:dz'];
}
}
Loading

0 comments on commit a732fcd

Please sign in to comment.