diff --git a/docs/dom.md b/docs/dom.md index 96d26db..246605e 100644 --- a/docs/dom.md +++ b/docs/dom.md @@ -75,6 +75,21 @@ try { } ``` +#### assert_cdata + +Assert if a node is of type `DOMCdataSection`. + +```php +use Psl\Type\Exception\AssertException; +use function VeeWee\Xml\Dom\Assert\assert_cdata; + +try { + assert_cdata($someNode) +} catch (AssertException $e) { + // Deal with it +} +``` + #### assert_document Assert if a node is of type `DOMDocument`. @@ -205,6 +220,26 @@ element('foo', ``` +#### cdata + +Operates on a `DOMNode` and creates a `DOMCdataSection`. +It can contain a set of configurators that can be used to dynamically change the cdata's contents. + +```php +use function VeeWee\Xml\Dom\Builder\attribute; +use function VeeWee\Xml\Dom\Builder\element; +use function VeeWee\Xml\Dom\Builder\cdata; +use function VeeWee\Xml\Dom\Builder\children; + +element('hello', children( + cdata('world') +)); +``` + +```xml +world]]> +``` + #### children Operates on a `DOMNode` and attaches multiple child nodes. @@ -1157,6 +1192,18 @@ if (is_attribute($someNode)) { } ``` +#### is_cdata + +Checks if a node is of type `DOMCdataSection`. + +```php +use function VeeWee\Xml\Dom\Predicate\is_cdata; + +if (is_cdata($someNode)) { + // ... +} +``` + #### is_default_xmlns_attribute Checks if a node is of type `DOMNameSpaceNode` and is the default xmlns. diff --git a/docs/encoding.md b/docs/encoding.md index 5db005b..3ce3121 100644 --- a/docs/encoding.md +++ b/docs/encoding.md @@ -232,7 +232,7 @@ More information about [the PHP format can be found here](#php-format). 'id' => 2, 'test:type' => 'hello' ], - '@value' => 'Moon' + '@cdata' => 'Moon' ] ] ] @@ -253,6 +253,10 @@ More information about [the PHP format can be found here](#php-format). - You can provide any value that can be coerced to a string as an element value. - If the element does not have attributes, you can directly pass the value to the element name. In that case there is no need for the `@value` section. - All XML entities `<>"'` will be encoded before inserting a value into XML. +- The `@cdata` section can be used for escaping HTML/XML content inside your XML element. + - You can provide any value that can be coerced to a string as an element value. + - The content will be wrapped with `<[CDATA[ ]]>`, special XML entities will not be escaped + - During decoding, cdata information will get lost: the element's `@value` will contain escaped XML entities. This is because an element can contain a mix of cdata and text nodes. - You can nest a single element or an array of elements into a parent element. #### Decoded types diff --git a/src/Xml/Dom/Assert/assert_cdata.php b/src/Xml/Dom/Assert/assert_cdata.php new file mode 100644 index 0000000..de96d00 --- /dev/null +++ b/src/Xml/Dom/Assert/assert_cdata.php @@ -0,0 +1,18 @@ +assert($node); +} diff --git a/src/Xml/Dom/Builder/cdata.php b/src/Xml/Dom/Builder/cdata.php new file mode 100644 index 0000000..186e387 --- /dev/null +++ b/src/Xml/Dom/Builder/cdata.php @@ -0,0 +1,31 @@ + $configurators + * + * @return \Closure(DOMNode): DOMCdataSection + */ +function cdata(string $data, ...$configurators): Closure +{ + return static function (DOMNode $node) use ($data, $configurators): DOMCdataSection { + $document = is_document($node) ? $node : $node->ownerDocument; + Assert::isInstanceOf($document, DOMDocument::class, 'Can not create cdata without a DOM document.'); + + return assert_cdata( + configure(...$configurators)($document->createCDATASection($data)) + ); + }; +} diff --git a/src/Xml/Dom/Predicate/is_cdata.php b/src/Xml/Dom/Predicate/is_cdata.php new file mode 100644 index 0000000..ee4884d --- /dev/null +++ b/src/Xml/Dom/Predicate/is_cdata.php @@ -0,0 +1,16 @@ +assert($data['@attributes'] ?? null); $namespaces = $nullableMap->assert($data['@namespaces'] ?? null); + $cdata = union(string(), null())->assert($data['@cdata'] ?? null); $value = union(string(), null())->assert($data['@value'] ?? null); $element = filter_keys( $data, - static fn (string $key): bool => !in_array($key, ['@attributes', '@namespaces', '@value'], true) + static fn (string $key): bool => !in_array($key, ['@attributes', '@namespaces', '@value', '@cdata'], true) ); $currentNamespace = $namespaces[''] ?? null; @@ -49,6 +52,7 @@ function element(string $name, array $data): Closure $children = filter_nulls([ $attributes ? attributes($attributes) : null, $namedNamespaces ? xmlns_attributes($namedNamespaces) : null, + $cdata !== null ? childrenBuilder(cdata($cdata)) : null, $value !== null ? escaped_value($value) : null, ...values(map_with_key( $element, diff --git a/src/bootstrap.php b/src/bootstrap.php index 8fcbf65..607c4ca 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -1,11 +1,13 @@ expectException(AssertException::class); + } + + $actual = assert_cdata($node); + static::assertSame($node, $actual); + } + + public function provideTestCases() + { + $doc = Document::fromXmlString( + <<HELLO + EOXML + )->toUnsafeDocument(); + + yield [$doc, false]; + yield [$doc->documentElement, false]; + yield [$doc->documentElement->firstChild, true]; + yield [null, false]; + } +} diff --git a/tests/Xml/Dom/Builder/CdataTest.php b/tests/Xml/Dom/Builder/CdataTest.php new file mode 100644 index 0000000..e2915d6 --- /dev/null +++ b/tests/Xml/Dom/Builder/CdataTest.php @@ -0,0 +1,34 @@ +hello')($doc); + + static::assertInstanceOf(DOMCdataSection::class, $node); + static::assertSame($data, $node->textContent); + static::assertSame(xml_string()($node), ''); + } + + public function test_it_can_build_cdata_with_configurators(): void + { + $doc = new DOMDocument(); + $node = cdata($data = 'hello', identity())($doc); + + static::assertInstanceOf(DOMCdataSection::class, $node); + static::assertSame($data, $node->textContent); + } +} diff --git a/tests/Xml/Dom/Builder/ChildrenTest.php b/tests/Xml/Dom/Builder/ChildrenTest.php index 5d38300..6f24d5c 100644 --- a/tests/Xml/Dom/Builder/ChildrenTest.php +++ b/tests/Xml/Dom/Builder/ChildrenTest.php @@ -6,8 +6,10 @@ use DOMDocument; use PHPUnit\Framework\TestCase; +use function VeeWee\Xml\Dom\Builder\cdata; use function VeeWee\Xml\Dom\Builder\children; use function VeeWee\Xml\Dom\Builder\element; +use function VeeWee\Xml\Dom\Mapper\xml_string; final class ChildrenTest extends TestCase { @@ -27,7 +29,6 @@ public function test_it_can_build_document_children(): void static::assertSame('world2', $children->item(1)->nodeName); } - public function test_it_can_build_an_element_with_children(): void { $doc = new DOMDocument(); @@ -48,4 +49,20 @@ public function test_it_can_build_an_element_with_children(): void static::assertSame('world1', $children->item(0)->nodeName); static::assertSame('world2', $children->item(1)->nodeName); } + + public function test_it_can_add_cdata(): void + { + $doc = new DOMDocument(); + $node = element( + 'hello', + children( + cdata('world'), + ) + )($doc); + + static::assertXmlStringEqualsXmlString( + 'world]]>', + xml_string()($node) + ); + } } diff --git a/tests/Xml/Dom/Builder/NodesTest.php b/tests/Xml/Dom/Builder/NodesTest.php index 36cb1d7..810cf40 100644 --- a/tests/Xml/Dom/Builder/NodesTest.php +++ b/tests/Xml/Dom/Builder/NodesTest.php @@ -20,7 +20,7 @@ public function test_it_can_build_nodes(): void static fn (DOMDocument $doc): array => [ element('many1')($doc), element('many2')($doc), - ] + ], )($doc); static::assertCount(4, $nodes); diff --git a/tests/Xml/Dom/Manipulator/Node/RenameTest.php b/tests/Xml/Dom/Manipulator/Node/RenameTest.php index 4337d90..83f41bf 100644 --- a/tests/Xml/Dom/Manipulator/Node/RenameTest.php +++ b/tests/Xml/Dom/Manipulator/Node/RenameTest.php @@ -13,7 +13,6 @@ use function VeeWee\Xml\Dom\Locator\document_element; use function VeeWee\Xml\Dom\Manipulator\Node\remove_namespace; use function VeeWee\Xml\Dom\Manipulator\Node\rename; -use function VeeWee\Xml\Dom\Mapper\xml_string; use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces; final class RenameTest extends TestCase diff --git a/tests/Xml/Dom/Predicate/IsCDataTest.php b/tests/Xml/Dom/Predicate/IsCDataTest.php new file mode 100644 index 0000000..779d834 --- /dev/null +++ b/tests/Xml/Dom/Predicate/IsCDataTest.php @@ -0,0 +1,36 @@ +HELLO + EOXML + )->toUnsafeDocument(); + + yield [$doc, false]; + yield [$doc->documentElement, false]; + yield [$doc->documentElement->firstChild, true]; + } +} diff --git a/tests/Xml/Encoding/EncodingTest.php b/tests/Xml/Encoding/EncodingTest.php index 7efe660..3b923c0 100644 --- a/tests/Xml/Encoding/EncodingTest.php +++ b/tests/Xml/Encoding/EncodingTest.php @@ -239,6 +239,16 @@ public function provideBidirectionalCases() ] ] ]; + yield 'cdata' => [ + 'xml' => 'world]]>', + 'data' => ['hello' => [ + '@cdata' => 'world' + ]] + ]; + yield 'mixed cdata' => [ + 'xml' => 'hello world]]>', + 'data' => ['hello' => 'hello world'] + ]; } public function provideRiskyBidirectionalCases()