Skip to content

Commit a732fcd

Browse files
authored
Merge pull request #88 from veewee/xmlns-inheriting-encoding
Inherit XMLNS during encoding like we did in v3
2 parents 86a8497 + c7b1171 commit a732fcd

14 files changed

+254
-21
lines changed

docs/dom.md

+25
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ element('foo',
219219
<foo hello="world" bar="baz" />
220220
```
221221

222+
#### default_xmlns_attribute
223+
224+
Operates on a `Dom\Element` and adds a default xmlns attribute.
225+
Given how XML serialization works in PHP, this function only works on already prefixed + namespaced elements:
226+
227+
```php
228+
use function VeeWee\Xml\Dom\Builder\namespaced_element;
229+
use function VeeWee\Xml\Dom\Builder\default_xmlns_attribute;
230+
231+
namespaced_element('uri://x', x:hello', default_xmlns_attribute(http://default'));
232+
```
233+
222234
#### cdata
223235

224236
Operates on a `Dom\Node` and creates a `Dom\CDATASection`.
@@ -1289,6 +1301,19 @@ if (is_non_empty_text($someNode)) {
12891301
}
12901302
```
12911303

1304+
#### is_prefixed_node_name
1305+
1306+
Checks if a given node name is prefixed or not.
1307+
This will validate for pattern `^[^:]+:[^:]+$` to make sure that there are no multiple colons in the node name and that all parts are set.
1308+
1309+
```php
1310+
use function VeeWee\Xml\Dom\Predicate\is_prefixed_node_name;
1311+
1312+
if (is_prefixed_node_name('prefixed:nodeName')) {
1313+
// ...
1314+
}
1315+
```
1316+
12921317
#### is_text
12931318

12941319
Checks if a node is of type `Dom\Text`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace VeeWee\Xml\Dom\Builder;
6+
7+
use Closure;
8+
use Dom\Element;
9+
use VeeWee\Xml\Xmlns\Xmlns;
10+
11+
/**
12+
* @return Closure(Element): Element
13+
*/
14+
function default_xmlns_attribute(string $namespaceURI): Closure
15+
{
16+
return static function (Element $node) use ($namespaceURI): Element {
17+
$node->setAttributeNS(Xmlns::xmlns()->value(), 'xmlns', $namespaceURI);
18+
19+
return $node;
20+
};
21+
}

src/Xml/Dom/Builder/nodes.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
use Closure;
88
use Dom\Node;
9-
use Dom\XMLDocument;
109
use function is_array;
1110
use function Psl\Iter\reduce;
1211
use function VeeWee\Xml\Dom\Locator\Node\detect_document;
1312

1413
/**
15-
* @param list<callable(XMLDocument): (list<Node>|Node)> $builders
14+
* @param list<callable(Node): (list<Node>|Node)> $builders
1615
*
17-
* @return Closure(XMLDocument): list<Node>
16+
* @return Closure(Node): list<Node>
1817
*/
1918
function nodes(callable ... $builders): Closure
2019
{
@@ -27,7 +26,7 @@ function nodes(callable ... $builders): Closure
2726
$builders,
2827
/**
2928
* @param list<Node> $builds
30-
* @param callable(XMLDocument): (Node|list<Node>) $builder
29+
* @param callable(Node): (Node|list<Node>) $builder
3130
* @return list<Node>
3231
*/
3332
static function (array $builds, callable $builder) use ($node): array {

src/Xml/Dom/Document.php

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ public function manipulate(callable $manipulator): self
130130
}
131131

132132
/**
133+
* @psalm-suppress ArgumentTypeCoercion - nodes() works on node but we provide the parent type XMLDocument.
134+
*
133135
* @param list<callable(XMLDocument): (list<Node>|Node)> $builders
134136
*
135137
* @return list<Node>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace VeeWee\Xml\Dom\Predicate;
6+
7+
function is_prefixed_node_name(string $nodeName): bool
8+
{
9+
return (bool)preg_match('/^[^:]+:[^:]+$/', $nodeName);
10+
}

src/Xml/Encoding/Internal/Decoder/Builder/namespaces.php

+3-6
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ function namespaces(Element $element): array
2020
{
2121
return filter([
2222
'@namespaces' => xmlns_attributes_list($element)->reduce(
23-
static fn (array $namespaces, Attr $node)
24-
=> $node->value
25-
? merge($namespaces, [
26-
($node->prefix !== null ? $node->localName : '') => $node->value
27-
])
28-
: $namespaces,
23+
static fn (array $namespaces, Attr $node) => merge($namespaces, [
24+
($node->prefix !== null ? $node->localName : '') => $node->value
25+
]),
2926
[]
3027
),
3128
]);

src/Xml/Encoding/Internal/Encoder/Builder/children.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Dom\Element;
99
use function Psl\Dict\map;
1010
use function VeeWee\Xml\Dom\Builder\children as buildChildren;
11-
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
1211
use function VeeWee\Xml\Dom\Builder\value;
1312

1413
/**
@@ -28,7 +27,7 @@ function children(string $name, array $children): Closure
2827
*/
2928
static fn (array|string $data): Closure => is_array($data)
3029
? element($name, $data)
31-
: elementBuilder($name, value($data))
30+
: xmlns_inheriting_element($name, [value($data)])
3231
)
3332
);
3433
}

src/Xml/Encoding/Internal/Encoder/Builder/element.php

+1-6
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
use function VeeWee\Xml\Dom\Builder\attributes;
2020
use function VeeWee\Xml\Dom\Builder\cdata;
2121
use function VeeWee\Xml\Dom\Builder\children as childrenBuilder;
22-
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
2322
use function VeeWee\Xml\Dom\Builder\escaped_value;
24-
use function VeeWee\Xml\Dom\Builder\namespaced_element as namespacedElementBuilder;
2523
use function VeeWee\Xml\Dom\Builder\xmlns_attributes;
2624

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

49-
$currentNamespace = $namespaces[''] ?? null;
5047
$namedNamespaces = filter_keys($namespaces ?? []);
5148

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

69-
return $currentNamespace !== null
70-
? namespacedElementBuilder($currentNamespace, $name, ...$children)
71-
: elementBuilder($name, ...$children);
66+
return xmlns_inheriting_element($name, $children, $namespaces);
7267
}

src/Xml/Encoding/Internal/Encoder/Builder/parent_node.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Psl\Exception\InvariantViolationException;
1111
use Psl\Type\Exception\AssertException;
1212
use function VeeWee\Xml\Dom\Builder\children as buildChildren;
13-
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
1413
use function VeeWee\Xml\Dom\Builder\escaped_value;
1514

1615
/**
@@ -25,7 +24,7 @@
2524
function parent_node(string $name, array|string $data): Closure
2625
{
2726
if (is_string($data)) {
28-
return buildChildren(elementBuilder($name, escaped_value($data)));
27+
return buildChildren(xmlns_inheriting_element($name, [escaped_value($data)]));
2928
}
3029

3130
if (is_node_list($data)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace VeeWee\Xml\Encoding\Internal\Encoder\Builder;
6+
7+
use Closure;
8+
use Dom\Element;
9+
use Dom\XMLDocument;
10+
use Webmozart\Assert\Assert;
11+
use function VeeWee\Xml\Dom\Builder\default_xmlns_attribute;
12+
use function VeeWee\Xml\Dom\Builder\element as elementBuilder;
13+
use function VeeWee\Xml\Dom\Builder\namespaced_element as namespacedElementBuilder;
14+
use function VeeWee\Xml\Dom\Predicate\is_element;
15+
use function VeeWee\Xml\Dom\Predicate\is_prefixed_node_name;
16+
17+
/**
18+
* This function can create element nodes that inherit the local xmlns namespace of their parent if none is configured.
19+
*
20+
* @param list<Closure(Element): Element> $children
21+
* @param array<string, string> $namespaces
22+
*
23+
* @return Closure(Element): Element
24+
*/
25+
function xmlns_inheriting_element(string $name, array $children, ?array $namespaces = []): Closure
26+
{
27+
return static function (XMLDocument|Element $parent) use ($namespaces, $name, $children): Element {
28+
29+
$defaultNamespace = $namespaces[''] ?? null;
30+
31+
// These rules apply for non prefixed elements only:
32+
// If no local namespace has been defined: lookup the default local namespace of the closest parent element.
33+
// Use that specific local namespace to create the element if one could be found.
34+
// Otherwise, just create a non-namespaced element.
35+
if (!is_prefixed_node_name($name)) {
36+
// Try to find the inherited default XMLNS for non prefixed elements without a desired local namespace.
37+
if ($defaultNamespace === null && is_element($parent)) {
38+
$defaultNamespace = $parent->lookupNamespaceURI('');
39+
}
40+
41+
return $defaultNamespace !== null
42+
? namespacedElementBuilder($defaultNamespace, $name, ...$children)($parent)
43+
: elementBuilder($name, ...$children)($parent);
44+
}
45+
46+
// Prefixed elements can be created as regular elements:
47+
// The configured xmlns attributes will be added by the $children.
48+
// If a local namespace is configured, make sure to register it on the node manually.
49+
[$prefix] = explode(':', $name);
50+
$prefixedNamespace = $namespaces[$prefix] ?? (is_element($parent) ? $parent->lookupNamespaceURI($prefix) : null);
51+
52+
Assert::notNull($prefixedNamespace, 'No namespace URI could be found for prefix: '.$prefix);
53+
54+
$defaultXmlns = $defaultNamespace !== null ? [default_xmlns_attribute($defaultNamespace)] : [];
55+
return namespacedElementBuilder(
56+
$prefixedNamespace,
57+
$name,
58+
...$defaultXmlns,
59+
...$children,
60+
)($parent);
61+
};
62+
}

src/bootstrap.php

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'Xml\Dom\Builder\attributes' => __DIR__.'/Xml/Dom/Builder/attributes.php',
1313
'Xml\Dom\Builder\cdata' => __DIR__.'/Xml/Dom/Builder/cdata.php',
1414
'Xml\Dom\Builder\children' => __DIR__.'/Xml/Dom/Builder/children.php',
15+
'Xml\Dom\Builder\default_xmlns_attribute' => __DIR__.'/Xml/Dom/Builder/default_xmlns_attribute.php',
1516
'Xml\Dom\Builder\element' => __DIR__.'/Xml/Dom/Builder/element.php',
1617
'Xml\Dom\Builder\escaped_value' => __DIR__.'/Xml/Dom/Builder/escaped_value.php',
1718
'Xml\Dom\Builder\namespaced_attribute' => __DIR__.'/Xml/Dom/Builder/namespaced_attribute.php',
@@ -79,6 +80,7 @@
7980
'Xml\Dom\Predicate\is_document_element' => __DIR__.'/Xml/Dom/Predicate/is_document_element.php',
8081
'Xml\Dom\Predicate\is_element' => __DIR__.'/Xml/Dom/Predicate/is_element.php',
8182
'Xml\Dom\Predicate\is_non_empty_text' => __DIR__.'/Xml/Dom/Predicate/is_non_empty_text.php',
83+
'Xml\Dom\Predicate\is_prefixed_node_name' => __DIR__.'/Xml/Dom/Predicate/is_prefixed_node_name.php',
8284
'Xml\Dom\Predicate\is_text' => __DIR__.'/Xml/Dom/Predicate/is_text.php',
8385
'Xml\Dom\Predicate\is_whitespace' => __DIR__.'/Xml/Dom/Predicate/is_whitespace.php',
8486
'Xml\Dom\Predicate\is_xmlns_attribute' => __DIR__.'/Xml/Dom/Predicate/is_xmlns_attribute.php',
@@ -107,6 +109,7 @@
107109
'Xml\Encoding\Internal\Encoder\Builder\normalize_data' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/normalize_data.php',
108110
'Xml\Encoding\Internal\Encoder\Builder\parent_node' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/parent_node.php',
109111
'Xml\Encoding\Internal\Encoder\Builder\root' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/root.php',
112+
'Xml\Encoding\Internal\Encoder\Builder\xmlns_inheriting_element' => __DIR__.'/Xml/Encoding/Internal/Encoder/Builder/xmlns_inheriting_element.php',
110113
'Xml\Encoding\Internal\wrap_exception' => __DIR__.'/Xml/Encoding/Internal/wrap_exception.php',
111114
'Xml\Encoding\document_encode' => __DIR__.'/Xml/Encoding/document_encode.php',
112115
'Xml\Encoding\element_decode' => __DIR__.'/Xml/Encoding/element_decode.php',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace VeeWee\Tests\Xml\Dom\Builder;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use VeeWee\Xml\Dom\Document;
9+
use function VeeWee\Xml\Dom\Builder\default_xmlns_attribute;
10+
use function VeeWee\Xml\Dom\Builder\element;
11+
use function VeeWee\Xml\Dom\Builder\namespaced_element;
12+
13+
final class DefaultXmlnsAttributeTest extends TestCase
14+
{
15+
public function test_it_can_build_an_element_with_default_xmlns_on_namespaced_element(): void
16+
{
17+
$doc = Document::empty()->toUnsafeDocument();
18+
19+
$node = namespaced_element(
20+
'uri://x',
21+
'x:foo',
22+
default_xmlns_attribute('uri://default')
23+
)($doc);
24+
25+
static::assertSame('<x:foo xmlns:x="uri://x" xmlns="uri://default"/>', $doc->saveXml($node));
26+
}
27+
28+
public function test_it_can_not_build_an_element_with_default_xmlns_on_regular_element(): void
29+
{
30+
$doc = Document::empty()->toUnsafeDocument();
31+
32+
$node = element(
33+
'foo',
34+
default_xmlns_attribute('uri://default')
35+
)($doc);
36+
37+
static::assertSame('<foo/>', $doc->saveXml($node));
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace VeeWee\Tests\Xml\Dom\Predicate;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use function VeeWee\Xml\Dom\Predicate\is_prefixed_node_name;
9+
10+
final class IsPrefixedNodeNameTest extends TestCase
11+
{
12+
/**
13+
*
14+
* @dataProvider provideValidQNames
15+
*/
16+
public function test_it_does_nothing_on_valid_qnames(string $input): void
17+
{
18+
static::assertTrue(is_prefixed_node_name($input));
19+
}
20+
21+
/**
22+
*
23+
* @dataProvider provideInvalidQNames
24+
*/
25+
public function test_it_throws_on_invalid_qnames(string $input): void
26+
{
27+
static::assertFalse(is_prefixed_node_name($input));
28+
}
29+
30+
public static function provideValidQNames()
31+
{
32+
yield ['hello:world'];
33+
yield ['a:b'];
34+
yield ['---a----:----b---'];
35+
}
36+
37+
public static function provideInvalidQNames()
38+
{
39+
yield [''];
40+
yield ['aa'];
41+
yield ['aa:'];
42+
yield [':bb'];
43+
yield [':b:c:cd:dz'];
44+
}
45+
}

0 commit comments

Comments
 (0)