Skip to content

Commit

Permalink
Add dom cdata helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Jul 20, 2023
1 parent b03e9fc commit c147116
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 5 deletions.
47 changes: 47 additions & 0 deletions docs/dom.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -205,6 +220,26 @@ element('foo',
<foo hello="world" bar="baz" />
```

#### 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('<html>world</html>')
));
```

```xml
<hello><![CDATA[<html>world</html>]]></hello>
```

#### children

Operates on a `DOMNode` and attaches multiple child nodes.
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion docs/encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ More information about [the PHP format can be found here](#php-format).
'id' => 2,
'test:type' => 'hello'
],
'@value' => 'Moon'
'@cdata' => '<html>Moon</html>'
]
]
]
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/Xml/Dom/Assert/assert_cdata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Assert;

use DOMCdataSection;
use Psl\Type\Exception\AssertException;
use function Psl\Type\instance_of;

/**
* @psalm-assert DOMCdataSection $node
* @throws AssertException
*/
function assert_cdata(mixed $node): DOMCdataSection
{
return instance_of(DOMCdataSection::class)->assert($node);
}
31 changes: 31 additions & 0 deletions src/Xml/Dom/Builder/cdata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Builder;

use Closure;
use DOMCdataSection;
use DOMDocument;
use DOMNode;
use Webmozart\Assert\Assert;
use function VeeWee\Xml\Dom\Assert\assert_cdata;
use function VeeWee\Xml\Dom\Predicate\is_document;
use function VeeWee\Xml\Internal\configure;

/**
* @param list<callable(DOMCdataSection): DOMCdataSection> $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))
);
};
}
16 changes: 16 additions & 0 deletions src/Xml/Dom/Predicate/is_cdata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Predicate;

use DOMCdataSection;
use DOMNode;

/**
* @psalm-assert-if-true DOMCdataSection $node
*/
function is_cdata(DOMNode $node): bool
{
return $node instanceof DOMCdataSection;
}
6 changes: 5 additions & 1 deletion src/Xml/Encoding/Internal/Encoder/Builder/element.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use function Psl\Vec\filter_nulls;
use function Psl\Vec\values;
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;
Expand All @@ -36,11 +38,12 @@ function element(string $name, array $data): Closure
$nullableMap = union(dict(string(), string()), null());
$attributes = $nullableMap->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;
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?php declare(strict_types=1);

require_once __DIR__.'/Xml/Dom/Assert/assert_attribute.php';
require_once __DIR__.'/Xml/Dom/Assert/assert_cdata.php';
require_once __DIR__.'/Xml/Dom/Assert/assert_document.php';
require_once __DIR__.'/Xml/Dom/Assert/assert_dom_node_list.php';
require_once __DIR__.'/Xml/Dom/Assert/assert_element.php';
require_once __DIR__.'/Xml/Dom/Builder/attribute.php';
require_once __DIR__.'/Xml/Dom/Builder/attributes.php';
require_once __DIR__.'/Xml/Dom/Builder/cdata.php';
require_once __DIR__.'/Xml/Dom/Builder/children.php';
require_once __DIR__.'/Xml/Dom/Builder/element.php';
require_once __DIR__.'/Xml/Dom/Builder/escaped_value.php';
Expand Down Expand Up @@ -67,6 +69,7 @@
require_once __DIR__.'/Xml/Dom/Mapper/xml_string.php';
require_once __DIR__.'/Xml/Dom/Mapper/xslt_template.php';
require_once __DIR__.'/Xml/Dom/Predicate/is_attribute.php';
require_once __DIR__.'/Xml/Dom/Predicate/is_cdata.php';
require_once __DIR__.'/Xml/Dom/Predicate/is_default_xmlns_attribute.php';
require_once __DIR__.'/Xml/Dom/Predicate/is_document.php';
require_once __DIR__.'/Xml/Dom/Predicate/is_document_element.php';
Expand Down
42 changes: 42 additions & 0 deletions tests/Xml/Dom/Assert/AssertCDataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Assert;

use DOMNode;
use PHPUnit\Framework\TestCase;
use Psl\Type\Exception\AssertException;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Assert\assert_cdata;

final class AssertCDataTest extends TestCase
{
/**
*
* @dataProvider provideTestCases
*/
public function test_it_knows_cdata(?DOMNode $node, bool $expected): void
{
if (!$expected) {
$this->expectException(AssertException::class);
}

$actual = assert_cdata($node);
static::assertSame($node, $actual);
}

public function provideTestCases()
{
$doc = Document::fromXmlString(
<<<EOXML
<doc><![CDATA[<html>HELLO</html]]></doc>
EOXML
)->toUnsafeDocument();

yield [$doc, false];
yield [$doc->documentElement, false];
yield [$doc->documentElement->firstChild, true];
yield [null, false];
}
}
34 changes: 34 additions & 0 deletions tests/Xml/Dom/Builder/CdataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Builder;

use DOMCdataSection;
use DOMDocument;
use PHPUnit\Framework\TestCase;
use function Psl\Fun\identity;
use function VeeWee\Xml\Dom\Builder\cdata;
use function VeeWee\Xml\Dom\Mapper\xml_string;

final class CdataTest extends TestCase
{
public function test_it_can_build_cdata(): void
{
$doc = new DOMDocument();
$node = cdata($data = '<html>hello</html>')($doc);

static::assertInstanceOf(DOMCdataSection::class, $node);
static::assertSame($data, $node->textContent);
static::assertSame(xml_string()($node), '<![CDATA['.$data.']]>');
}

public function test_it_can_build_cdata_with_configurators(): void
{
$doc = new DOMDocument();
$node = cdata($data = '<html>hello</html>', identity())($doc);

static::assertInstanceOf(DOMCdataSection::class, $node);
static::assertSame($data, $node->textContent);
}
}
19 changes: 18 additions & 1 deletion tests/Xml/Dom/Builder/ChildrenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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();
Expand All @@ -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('<html>world</html>'),
)
)($doc);

static::assertXmlStringEqualsXmlString(
'<hello><![CDATA[<html>world</html>]]></hello>',
xml_string()($node)
);
}
}
2 changes: 1 addition & 1 deletion tests/Xml/Dom/Builder/NodesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion tests/Xml/Dom/Manipulator/Node/RenameTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions tests/Xml/Dom/Predicate/IsCDataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Predicate;

use DOMNode;
use PHPUnit\Framework\TestCase;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Predicate\is_cdata;

final class IsCDataTest extends TestCase
{
/**
*
* @dataProvider provideTestCases
*/
public function test_it_knows_cdata(?DOMNode $node, bool $expected): void
{
$actual = is_cdata($node);
static::assertSame($expected, $actual);
}

public function provideTestCases()
{
$doc = Document::fromXmlString(
<<<EOXML
<doc><![CDATA[<html>HELLO</html]]></doc>
EOXML
)->toUnsafeDocument();

yield [$doc, false];
yield [$doc->documentElement, false];
yield [$doc->documentElement->firstChild, true];
}
}
10 changes: 10 additions & 0 deletions tests/Xml/Encoding/EncodingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ public function provideBidirectionalCases()
]
]
];
yield 'cdata' => [
'xml' => '<hello><![CDATA[<html>world</html>]]></hello>',
'data' => ['hello' => [
'@cdata' => '<html>world</html>'
]]
];
yield 'mixed cdata' => [
'xml' => '<hello>hello <![CDATA[<html>world</html>]]></hello>',
'data' => ['hello' => 'hello <html>world</html>']
];
}

public function provideRiskyBidirectionalCases()
Expand Down

0 comments on commit c147116

Please sign in to comment.