From 8192246b3301b4079267830f502ff9e6363726c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Sun, 10 May 2026 23:36:27 +0200 Subject: [PATCH 1/7] fix: escape XML special characters in addChild text content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOMElement::__construct does not escape & or < in its value argument, causing DOMException when user data contains XML special characters. Replace the value parameter with a createTextNode call after attachment, which handles escaping at the DOM level. Fixes #14 Signed-off-by: Guillaume Delré --- source/FluidXml/FluidInsertionHandler.php | 8 ++++++- specs/FluidXml.php | 26 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/source/FluidXml/FluidInsertionHandler.php b/source/FluidXml/FluidInsertionHandler.php index f47fd46..23b5b21 100644 --- a/source/FluidXml/FluidInsertionHandler.php +++ b/source/FluidXml/FluidInsertionHandler.php @@ -269,9 +269,15 @@ protected function insertStringSimple($parent, $k, $v, $fn): array // The user has passed an element name and an element value: // [ 'element' => 'Element content' ] - $el = $this->createElement($k, $v); + $el = $this->createElement($k); $el = $fn($parent, $el); + // createTextNode escapes XML special characters (&, <, >, etc.) + // DOMElement's constructor does not, so we avoid passing value there. + if ($v !== null && $v !== '') { + $el->appendChild($this->dom->createTextNode((string) $v)); + } + return [ $el ]; } diff --git a/specs/FluidXml.php b/specs/FluidXml.php index 6cac1ef..7dd9bca 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -1181,6 +1181,32 @@ function addchild($parent, $i) assert_equal_xml($xml, $expected); }); + it('should escape XML special characters in text content using the argument syntax', function () { + $xml = new FluidXml(); + $xml->addChild('child1', 'a & b') + ->addChild('parent', true) + ->addChild('child2', 'a < b'); + + $expected = "\n" + . " a & b\n" + . " \n" + . " a < b\n" + . " \n" + . ""; + assert_equal_xml($xml, $expected); + }); + + it('should escape XML special characters in text content using the array syntax', function () { + $xml = new FluidXml(); + $xml->addChild(['child1' => 'Hello & World', 'child2' => 'Tom & Jerry']); + + $expected = "\n" + . " Hello & World\n" + . " Tom & Jerry\n" + . ""; + assert_equal_xml($xml, $expected); + }); + it('should add many children with and without a value', function () { $xml = new FluidXml(); $xml->addChild(['child1', 'child2', 'child3' => 'value3', 'child4' => 'value4']) From faa29386f3a57cb75b2d79e5277113d7a9d5b82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Sun, 10 May 2026 23:42:02 +0200 Subject: [PATCH 2/7] feat: add libxml options support to xml() and save() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xml() and save() now accept an $options integer bitmask forwarded to DOMDocument::saveXML(), enabling LIBXML_NOEMPTYTAG and other libxml flags. The same parameter propagates through FluidContext::xml(), FluidSaveTrait, and the FluidHelper serialization helpers. Closes #15 Signed-off-by: Guillaume Delré --- source/FluidXml/FluidContext.php | 4 ++-- source/FluidXml/FluidHelper.php | 16 ++++++++-------- source/FluidXml/FluidInterface.php | 4 ++-- source/FluidXml/FluidSaveTrait.php | 6 +++--- source/FluidXml/FluidXml.php | 6 +++--- specs/FluidXml.php | 26 ++++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/source/FluidXml/FluidContext.php b/source/FluidXml/FluidContext.php index 6f76e41..a3f3790 100644 --- a/source/FluidXml/FluidContext.php +++ b/source/FluidXml/FluidContext.php @@ -397,9 +397,9 @@ public function __toString(): string return (string) $this->xml(); } - public function xml($strip = false): string + public function xml($strip = false, int $options = 0): string { - return FluidHelper::domnodesToString($this->nodes); + return FluidHelper::domnodesToString($this->nodes, false, $options); } public function html($strip = false): string diff --git a/source/FluidXml/FluidHelper.php b/source/FluidXml/FluidHelper.php index 25b0b71..7f6db6e 100644 --- a/source/FluidXml/FluidHelper.php +++ b/source/FluidXml/FluidHelper.php @@ -13,7 +13,7 @@ public static function isAnXmlString($string): bool return $string && $string[0] === '<'; } - public static function exportNode(\DOMDocument $dom, \DOMNode $node, $html = false): array|bool|string|null + public static function exportNode(\DOMDocument $dom, \DOMNode $node, $html = false, int $options = 0): array|bool|string|null { // $delegate = $html ? 'saveHTML' : 'saveXML'; // return $dom->$delegate($node); @@ -22,15 +22,15 @@ public static function exportNode(\DOMDocument $dom, \DOMNode $node, $html = fal return static::domnodeToHtml($node); } - return $dom->saveXML($node); + return $dom->saveXML($node, $options); } - public static function domdocumentToStringWithoutHeaders(\DOMDocument $dom, $html = false): bool|array|string|null + public static function domdocumentToStringWithoutHeaders(\DOMDocument $dom, $html = false, int $options = 0): bool|array|string|null { - return static::exportNode($dom, $dom->documentElement, $html); + return static::exportNode($dom, $dom->documentElement, $html, $options); } - public static function domnodelistToString(\DOMNodeList $nodelist, $html = false): string + public static function domnodelistToString(\DOMNodeList $nodelist, $html = false, int $options = 0): string { $nodes = []; @@ -44,16 +44,16 @@ public static function domnodelistToString(\DOMNodeList $nodelist, $html = false // Algorithm 1 is faster than Algorithm 2. - return static::domnodesToString($nodes, $html); + return static::domnodesToString($nodes, $html, $options); } - public static function domnodesToString(array $nodes, $html = false): string + public static function domnodesToString(array $nodes, $html = false, int $options = 0): string { $dom = $nodes[0]->ownerDocument; $xml = ''; foreach ($nodes as $n) { - $xml .= static::exportNode($dom, $n, $html) . PHP_EOL; + $xml .= static::exportNode($dom, $n, $html, $options) . PHP_EOL; } return \rtrim($xml); diff --git a/source/FluidXml/FluidInterface.php b/source/FluidXml/FluidInterface.php index 0bfc6e4..a39ce60 100644 --- a/source/FluidXml/FluidInterface.php +++ b/source/FluidXml/FluidInterface.php @@ -9,9 +9,9 @@ public function length(); public function dom(); public function array_(); public function __toString(); - public function xml($strip = false); + public function xml($strip = false, int $options = 0); public function html($strip = false); - public function save($file, $strip = false); + public function save($file, $strip = false, int $options = 0); public function query(...$query); public function __invoke(...$query); public function times($times, callable $fn = null); diff --git a/source/FluidXml/FluidSaveTrait.php b/source/FluidXml/FluidSaveTrait.php index c0af9d2..20e9be3 100644 --- a/source/FluidXml/FluidSaveTrait.php +++ b/source/FluidXml/FluidSaveTrait.php @@ -7,9 +7,9 @@ trait FluidSaveTrait /** * @throws \Exception */ - public function save($file, $strip = false): static + public function save($file, $strip = false, int $options = 0): static { - $status = \file_put_contents($file, $this->xml($strip)); + $status = \file_put_contents($file, $this->xml($strip, $options)); if (! $status) { throw new \Exception("The file '$file' is not writable."); @@ -18,5 +18,5 @@ public function save($file, $strip = false): static return $this; } - abstract public function xml($strip = false); + abstract public function xml($strip = false, int $options = 0); } diff --git a/source/FluidXml/FluidXml.php b/source/FluidXml/FluidXml.php index 1f8b11d..8d71646 100644 --- a/source/FluidXml/FluidXml.php +++ b/source/FluidXml/FluidXml.php @@ -152,13 +152,13 @@ public function __toString(): string return (string) $this->xml(); } - public function xml($strip = false) + public function xml($strip = false, int $options = 0) { if ($strip) { - return FluidHelper::domdocumentToStringWithoutHeaders($this->document->dom); + return FluidHelper::domdocumentToStringWithoutHeaders($this->document->dom, false, $options); } - return $this->document->dom->saveXML(); + return $this->document->dom->saveXML(null, $options); } public function html($strip = false): string diff --git a/specs/FluidXml.php b/specs/FluidXml.php index 7dd9bca..8043871 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -2273,6 +2273,32 @@ function addchild($parent, $i) . "content2"; \assert($actual === $expected, __($actual, $expected)); }); + + it('should render empty elements as explicit closing tags with LIBXML_NOEMPTYTAG', function () { + $xml = new FluidXml(); + $xml->addChild(['parent' => ['child1', 'child2']]); + + $actual = $xml->xml(true, \LIBXML_NOEMPTYTAG); + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should pass LIBXML_NOEMPTYTAG through when querying a context', function () { + $xml = new FluidXml(); + $xml->addChild(['parent' => ['child1', 'child2']]); + + $actual = $xml->query('//parent')->xml(false, \LIBXML_NOEMPTYTAG); + $expected = "\n" + . " \n" + . " \n" + . ""; + \assert($actual === $expected, __($actual, $expected)); + }); }); describe('.__toString()', function () { From 932142c8fa9f10ea30e3fd09f8350ca87b12b503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Sun, 10 May 2026 23:46:53 +0200 Subject: [PATCH 3/7] feat: add @:text, @:cdata, @:comment to array syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @ special key already sets text content in the concise array syntax. This extends it with typed variants: - @:text — explicit alias for @ - @:cdata — wraps content in a CDATA section - @:comment — inserts an XML comment Closes #23 Signed-off-by: Guillaume Delré --- source/FluidXml/FluidInsertionHandler.php | 28 +++++++++++++++++- specs/FluidXml.php | 35 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/source/FluidXml/FluidInsertionHandler.php b/source/FluidXml/FluidInsertionHandler.php index 23b5b21..ae39ca1 100644 --- a/source/FluidXml/FluidInsertionHandler.php +++ b/source/FluidXml/FluidInsertionHandler.php @@ -110,9 +110,15 @@ protected function recognizeStringMixed($k, $v) } if ($k[0] === '@') { - if ($k === '@') { + if ($k === '@' || $k === '@:text') { return 'insertSpecialContent'; } + if ($k === '@:cdata') { + return 'insertSpecialCdata'; + } + if ($k === '@:comment') { + return 'insertSpecialComment'; + } return 'insertSpecialAttribute'; } @@ -250,6 +256,26 @@ protected function insertSpecialContent($parent, $k, $v): array return []; } + protected function insertSpecialCdata($parent, $k, $v): array + { + // The user has passed CDATA content: + // [ '@:cdata' => 'Content with chars.' ] + + $this->newContext($parent)->addCdata($v); + + return []; + } + + protected function insertSpecialComment($parent, $k, $v): array + { + // The user has passed a comment: + // [ '@:comment' => 'This is a comment.' ] + + $this->newContext($parent)->addComment($v); + + return []; + } + protected function insertSpecialAttribute($parent, $k, $v): array { // The user has passed an attribute name and an attribute value: diff --git a/specs/FluidXml.php b/specs/FluidXml.php index 8043871..be7a6af 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -1345,6 +1345,41 @@ function addchild($parent, $i) assert_equal_xml($xml, $expected); }); + it('should add text content using @:text as alias for @', function () { + $xml = new FluidXml(); + $xml->addChild(['child1' => [ '@:text' => 'Hello' ]]); + + $expected = "\n" + . " Hello\n" + . ""; + assert_equal_xml($xml, $expected); + }); + + it('should add CDATA content using @:cdata syntax', function () { + $xml = new FluidXml(); + $xml->addChild(['chapter' => [ + '@id' => '1', + '@:cdata' => 'Ideas About Universe', + ]]); + + $expected = "\n" + . " Universe]]>\n" + . ""; + assert_equal_xml($xml, $expected); + }); + + it('should add comment content using @:comment syntax', function () { + $xml = new FluidXml(); + $xml->addChild(['node' => [ '@:comment' => 'This is a comment' ]]); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . ""; + assert_equal_xml($xml, $expected); + }); + it('should switch context', function () { $xml = new FluidXml(); From 8d11c2cfae0eb07b564f0b56ca994db2ee1353a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Sun, 10 May 2026 23:51:42 +0200 Subject: [PATCH 4/7] feat: add toArray() and toObject() with namespace support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FluidXml::toArray() returns ['rootTag' => value] for the document. FluidContext::toArray() returns an array of ['tag' => value] entries for each selected node. Conversion rules: - Text-only element -> plain string value - Empty element -> null - Attributes -> '@name' keys - Text with siblings -> '@' key - CDATA content -> '@:cdata' key - Comment -> '@:comment' key - Repeated child tags -> indexed sub-arrays - Namespace decls -> '@xmlns' / '@xmlns:prefix' keys (only those introduced on the element, not inherited) The output mirrors the array syntax accepted by addChild(), enabling round-trip import/export. FluidXml::toObject() and FluidContext::toObject() return the same structure as stdClass instead of arrays, which integrates more naturally with JSON workflows and property-access code. Associative arrays become stdClass objects; indexed arrays (repeated siblings) remain PHP arrays. Namespace declarations are detected via the XPath namespace::* axis, comparing in-scope namespaces with the parent element to emit only the declarations local to each node. Closes #28 Signed-off-by: Guillaume Delré --- source/FluidXml/FluidContext.php | 21 +++++ source/FluidXml/FluidHelper.php | 82 ++++++++++++++++++ source/FluidXml/FluidInterface.php | 2 + source/FluidXml/FluidXml.php | 16 ++++ specs/FluidXml.php | 129 +++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+) diff --git a/source/FluidXml/FluidContext.php b/source/FluidXml/FluidContext.php index a3f3790..275baa4 100644 --- a/source/FluidXml/FluidContext.php +++ b/source/FluidXml/FluidContext.php @@ -402,6 +402,27 @@ public function xml($strip = false, int $options = 0): string return FluidHelper::domnodesToString($this->nodes, false, $options); } + public function toArray(): array + { + $result = []; + + foreach ($this->nodes as $node) { + if ($node instanceof \DOMElement) { + $result[] = [$node->nodeName => FluidHelper::domNodeToArray($node)]; + } + } + + return $result; + } + + public function toObject(): array + { + return \array_map( + fn($item) => FluidHelper::arrayToObject($item), + $this->toArray() + ); + } + public function html($strip = false): string { return FluidHelper::domnodesToString($this->nodes, true); diff --git a/source/FluidXml/FluidHelper.php b/source/FluidXml/FluidHelper.php index 7f6db6e..d1c1609 100644 --- a/source/FluidXml/FluidHelper.php +++ b/source/FluidXml/FluidHelper.php @@ -4,6 +4,88 @@ class FluidHelper { + public static function domNodeToArray(\DOMElement $node): mixed + { + $result = []; + + if ($node->hasAttributes()) { + foreach ($node->attributes as $attr) { + $result['@' . $attr->name] = $attr->value; + } + } + + // Capture namespace declarations introduced on this element (not inherited). + $xpath = new \DOMXPath($node->ownerDocument); + $parentNs = []; + + if ($node->parentNode instanceof \DOMElement) { + foreach ($xpath->query('namespace::*', $node->parentNode) as $ns) { + $parentNs[$ns->nodeName] = $ns->nodeValue; + } + } + + foreach ($xpath->query('namespace::*', $node) as $ns) { + if ($ns->localName === 'xml') { + continue; + } + if (! isset($parentNs[$ns->nodeName]) || $parentNs[$ns->nodeName] !== $ns->nodeValue) { + $result['@' . $ns->nodeName] = $ns->nodeValue; + } + } + + $childElements = []; + + foreach ($node->childNodes as $child) { + if ($child instanceof \DOMCDATASection) { + $result['@:cdata'] = $child->nodeValue; + } elseif ($child instanceof \DOMText) { + $text = \trim($child->nodeValue); + if ($text !== '') { + $result['@'] = ($result['@'] ?? '') . $text; + } + } elseif ($child instanceof \DOMComment) { + $result['@:comment'] = $child->nodeValue; + } elseif ($child instanceof \DOMElement) { + $childElements[] = $child; + } + } + + $names = \array_map(fn($el) => $el->nodeName, $childElements); + $hasRepeat = \count($names) !== \count(\array_unique($names)); + + if ($hasRepeat) { + foreach ($childElements as $child) { + $result[] = [$child->nodeName => static::domNodeToArray($child)]; + } + } else { + foreach ($childElements as $child) { + $result[$child->nodeName] = static::domNodeToArray($child); + } + } + + // Simplify text-only element to a plain string + if (\array_keys($result) === ['@']) { + return $result['@']; + } + + return empty($result) ? null : $result; + } + + public static function arrayToObject(array $array): \stdClass + { + $obj = new \stdClass(); + + foreach ($array as $k => $v) { + $obj->$k = \is_array($v) + ? (\array_is_list($v) + ? \array_map(fn($i) => \is_array($i) ? static::arrayToObject($i) : $i, $v) + : static::arrayToObject($v)) + : $v; + } + + return $obj; + } + public static function isAnXmlString($string): bool { // Removes any empty new line at the beginning, diff --git a/source/FluidXml/FluidInterface.php b/source/FluidXml/FluidInterface.php index a39ce60..760e65e 100644 --- a/source/FluidXml/FluidInterface.php +++ b/source/FluidXml/FluidInterface.php @@ -12,6 +12,8 @@ public function __toString(); public function xml($strip = false, int $options = 0); public function html($strip = false); public function save($file, $strip = false, int $options = 0); + public function toArray(): array; + public function toObject(): \stdClass|array; public function query(...$query); public function __invoke(...$query); public function times($times, callable $fn = null); diff --git a/source/FluidXml/FluidXml.php b/source/FluidXml/FluidXml.php index 8d71646..630d1a6 100644 --- a/source/FluidXml/FluidXml.php +++ b/source/FluidXml/FluidXml.php @@ -161,6 +161,22 @@ public function xml($strip = false, int $options = 0) return $this->document->dom->saveXML(null, $options); } + public function toArray(): array + { + $root = $this->document->dom->documentElement; + + if ($root === null) { + return []; + } + + return [$root->nodeName => FluidHelper::domNodeToArray($root)]; + } + + public function toObject(): \stdClass + { + return FluidHelper::arrayToObject($this->toArray()); + } + public function html($strip = false): string { $header = "\n"; diff --git a/specs/FluidXml.php b/specs/FluidXml.php index be7a6af..946df10 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -2336,6 +2336,135 @@ function addchild($parent, $i) }); }); + describe('.toArray()', function () { + it('should export a simple element with text content', function () { + $xml = new FluidXml(); + $xml->addChild(['type' => 'Herbivore']); + + $actual = $xml->toArray(); + $expected = ['doc' => ['type' => 'Herbivore']]; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should export attributes using the @ prefix', function () { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['@operation' => 'create', '@' => 'data']]); + + $actual = $xml->toArray(); + $expected = ['doc' => ['animal' => ['@operation' => 'create', '@' => 'data']]]; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should export nested elements as nested arrays', function () { + $xml = new FluidXml(); + $xml->addChild(['animal' => [ + '@operation' => 'create', + 'type' => 'Herbivore', + 'attribute' => ['legs' => '4', 'head' => '1'], + ]]); + + $actual = $xml->toArray(); + $expected = ['doc' => [ + 'animal' => [ + '@operation' => 'create', + 'type' => 'Herbivore', + 'attribute' => ['legs' => '4', 'head' => '1'], + ], + ]]; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should export repeated sibling elements as indexed arrays', function () { + $xml = new FluidXml(); + $xml->addChild(['chapters' => [ + ['chapter' => 'One'], + ['chapter' => 'Two'], + ]]); + + $actual = $xml->toArray(); + $expected = ['doc' => [ + 'chapters' => [ + ['chapter' => 'One'], + ['chapter' => 'Two'], + ], + ]]; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should export an empty element as null', function () { + $xml = new FluidXml(); + $xml->addChild('empty'); + + $actual = $xml->toArray(); + $expected = ['doc' => ['empty' => null]]; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should export queried nodes via FluidContext', function () { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['type' => 'Herbivore', 'legs' => '4']]); + + $actual = $xml->query('//type')->toArray(); + $expected = [['type' => 'Herbivore']]; + \assert($actual === $expected, __($actual, $expected)); + }); + + it('should export namespace declarations using the @xmlns prefix', function () { + $xml = new FluidXml('animal'); + $xml->namespace(new FluidNamespace('foo', 'http://foo.com')); + $xml->addChild('foo:legs', '4'); + + $actual = $xml->toArray(); + // The xmlns:foo declaration appears on foo:legs (where it is first used). + // Text content is under '@' when co-present with namespace metadata. + $expected = ['animal' => [ + 'foo:legs' => [ + '@xmlns:foo' => 'http://foo.com', + '@' => '4', + ], + ]]; + \assert($actual === $expected, __($actual, $expected)); + }); + }); + + describe('.toObject()', function () { + it('should return a stdClass mirroring toArray()', function () { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['@operation' => 'create', 'type' => 'Herbivore']]); + + $obj = $xml->toObject(); + \assert($obj instanceof \stdClass, __($obj, 'stdClass')); + \assert($obj->doc instanceof \stdClass, __($obj->doc, 'stdClass')); + \assert($obj->doc->animal instanceof \stdClass, __($obj->doc->animal, 'stdClass')); + \assert($obj->doc->animal->{'@operation'} === 'create', __($obj->doc->animal->{'@operation'}, 'create')); + \assert($obj->doc->animal->type === 'Herbivore', __($obj->doc->animal->type, 'Herbivore')); + }); + + it('should preserve indexed arrays for repeated siblings', function () { + $xml = new FluidXml(); + $xml->addChild(['chapters' => [ + ['chapter' => 'One'], + ['chapter' => 'Two'], + ]]); + + $obj = $xml->toObject(); + \assert(\is_array($obj->doc->chapters), __($obj->doc->chapters, 'array')); + \assert($obj->doc->chapters[0] instanceof \stdClass, __($obj->doc->chapters[0], 'stdClass')); + \assert($obj->doc->chapters[0]->chapter === 'One', __($obj->doc->chapters[0]->chapter, 'One')); + }); + + it('should return an array of stdClass for FluidContext', function () { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['type' => 'Herbivore', 'legs' => '4']]); + + $result = $xml->query('//animal')->toObject(); + \assert(\is_array($result), __($result, 'array')); + \assert($result[0] instanceof \stdClass, __($result[0], 'stdClass')); + \assert($result[0]->animal instanceof \stdClass, __($result[0]->animal, 'stdClass')); + \assert($result[0]->animal->type === 'Herbivore', __($result[0]->animal->type, 'Herbivore')); + }); + }); + describe('.__toString()', function () { it('should behave like .xml()', function () { $xml = new FluidXml(); From 7026964ca7761339cac5719fc5f50683de05c4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 00:11:21 +0200 Subject: [PATCH 5/7] fix: skip DOMDocumentType when importing XML with DOCTYPE declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOMDocument::importNode() returns false for DOMDocumentType nodes, causing appendChild() to receive a boolean and throw a fatal error. This happens when loading or inserting XML that contains a declaration: the node list from childNodes includes the DOMDocumentType node alongside the root element. Skip DOMDocumentType entries in attachNodes() before attempting the import, so the document content is imported correctly and the DOCTYPE declaration is silently discarded. Fixes #44 Fixes #50 Signed-off-by: Guillaume Delré --- source/FluidXml/FluidInsertionHandler.php | 5 ++++ specs/FluidXml.php | 28 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/source/FluidXml/FluidInsertionHandler.php b/source/FluidXml/FluidInsertionHandler.php index ae39ca1..ad472d7 100644 --- a/source/FluidXml/FluidInsertionHandler.php +++ b/source/FluidXml/FluidInsertionHandler.php @@ -232,6 +232,11 @@ protected function attachNodes($parent, $nodes, $fn): array $context = []; foreach ($nodes as $el) { + // DOMDocumentType cannot be imported as a child node. + if ($el instanceof \DOMDocumentType) { + continue; + } + $el = $this->dom->importNode($el, true); $context[] = $fn($parent, $el); } diff --git a/specs/FluidXml.php b/specs/FluidXml.php index 946df10..6a587f3 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -142,6 +142,34 @@ assert_is_a($actual, \Exception::class); }); + + it('should import an XML file containing a DOCTYPE declaration', function () { + $file = "{$this->out_dir}.test_load_doctype.xml"; + $doc = "\n" + . "\n" + . ""; + + \file_put_contents($file, $doc); + $xml = FluidXml::load($file); + \unlink($file); + + $expected = "\n \n"; + assert_equal_xml($xml, $expected); + }); + }); + + describe('.addChild() with DOCTYPE', function () { + it('should not throw when adding an XML string with a DOCTYPE declaration', function () { + $xml = new FluidXml(null); + $xmlStr = "\n" + . "\n" + . ""; + + $xml->addChild($xmlStr); + + $expected = "\n \n"; + assert_equal_xml($xml, $expected); + }); }); if (\version_compare(\phpversion(), '7', '>=')) { From 3aff4c682381e0dc78eb759bd866a4d92164ecd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 00:15:18 +0200 Subject: [PATCH 6/7] feat: add libxml flags parameter to FluidXml::load() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Large XML files (or files served over HTTP) can exceed libxml's default parser limits, causing load() to silently return empty data. Pass an optional $flags int to load() and forward it to DOMDocument::load(), which lets callers opt into LIBXML_PARSEHUGE, LIBXML_COMPACT, or any other libxml flag as needed. The implementation also switches from file_get_contents() + loadXML() to DOMDocument::load() directly, which avoids loading the whole file into a PHP string before parsing. Closes #52 Signed-off-by: Guillaume Delré --- source/FluidXml/FluidXml.php | 14 +++++++------- specs/FluidXml.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/source/FluidXml/FluidXml.php b/source/FluidXml/FluidXml.php index 630d1a6..400fe5d 100644 --- a/source/FluidXml/FluidXml.php +++ b/source/FluidXml/FluidXml.php @@ -26,17 +26,17 @@ class FluidXml implements FluidInterface private ?\FluidXml\FluidContext $context = null; private $contextEl; - public static function load($document) + public static function load($document, int $flags = 0) { - $file = $document; - $document = \file_get_contents($file); + $dom = new \DOMDocument(); + $dom->formatOutput = true; + $dom->preserveWhiteSpace = false; - // file_get_contents() returns false in case of error. - if (! $document) { - throw new \Exception("File '$file' not accessible."); + if (! @$dom->load($document, $flags)) { + throw new \Exception("File '$document' not accessible."); } - return (new FluidXml(null))->addChild($document); + return (new FluidXml(null))->addChild($dom); } public function __construct(...$arguments) diff --git a/specs/FluidXml.php b/specs/FluidXml.php index 6a587f3..f1c41c6 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -156,6 +156,20 @@ $expected = "\n \n"; assert_equal_xml($xml, $expected); }); + + it('should accept libxml flags', function () { + $file = "{$this->out_dir}.test_load_flags.xml"; + $doc = "\n" + . " value\n" + . ""; + + \file_put_contents($file, $doc); + $xml = FluidXml::load($file, \LIBXML_PARSEHUGE); + \unlink($file); + + $expected = $doc; + assert_equal_xml($xml, $expected); + }); }); describe('.addChild() with DOCTYPE', function () { From 0c76ff4be2cd6832096a36e71cc0118d4c432412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 02:01:04 +0200 Subject: [PATCH 7/7] feat(tests): migrate Peridot test suite to PHPUnit 11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Peridot BDD spec runner with PHPUnit 11. All 208 tests ported to 5 test files under tests/ covering FluidXml, FluidContext, FluidNamespace, FluidHelper and CssTranslator. Renames source/ to src/ to align with PHP/OSS convention. Removes specs/, support/ and their Peridot dependency. Updates all CI configs (CircleCI, Scrutinizer, Travis, Coveralls) to use composer install and phpunit directly, removes hardcoded GitHub access tokens. Upgrades rector to ^2.0 (required for compatibility with phpstan ^2.0 pulled by phpunit 11) and updates rector.php to the 2.x API. Updates README to reference tests/ instead of specs/. Adds vendor/, build/ and .phpunit.result.cache to .gitignore. Signed-off-by: Guillaume Delré --- .coveralls.yml | 3 + .gitignore | 3 + .scrutinizer.yml | 13 +- .travis.yml | 9 +- README.md | 4 +- circle.yml | 10 +- composer.json | 13 +- phpunit.xml | 16 + rector.php | 13 +- specs/.common.php | 57 - specs/FluidXml.php | 3276 ----------------- {source => src}/FluidXml.php | 0 {source => src}/FluidXml/CssTranslator.php | 0 .../FluidXml/FluidAliasesTrait.php | 0 {source => src}/FluidXml/FluidContext.php | 0 {source => src}/FluidXml/FluidDocument.php | 0 {source => src}/FluidXml/FluidHelper.php | 0 .../FluidXml/FluidInsertionHandler.php | 0 {source => src}/FluidXml/FluidInterface.php | 0 {source => src}/FluidXml/FluidNamespace.php | 0 {source => src}/FluidXml/FluidRepeater.php | 0 {source => src}/FluidXml/FluidSaveTrait.php | 0 {source => src}/FluidXml/FluidXml.php | 0 {source => src}/FluidXml/NewableTrait.php | 0 .../FluidXml/ReservedCallStaticTrait.php | 0 .../FluidXml/ReservedCallTrait.php | 0 {source => src}/FluidXml/fluid.php | 0 support/.common.sh | 32 - support/Codevelox.php | 96 - support/coverage | 44 - support/coveralls | 25 - support/coveralls.php | 61 - support/gendoc | 47 - support/init | 16 - support/peridot.php | 29 - support/speedtest | 55 - support/speedtest.php | 62 - support/test | 21 - support/testd | 19 - support/testdbg | 6 - support/testv | 24 - tests/CssTranslatorTest.php | 143 + tests/FluidContextTest.php | 235 ++ tests/FluidHelperTest.php | 86 + tests/FluidNamespaceTest.php | 90 + tests/FluidTestCase.php | 33 + tests/FluidXmlTest.php | 2737 ++++++++++++++ 47 files changed, 3370 insertions(+), 3908 deletions(-) create mode 100644 phpunit.xml delete mode 100644 specs/.common.php delete mode 100644 specs/FluidXml.php rename {source => src}/FluidXml.php (100%) rename {source => src}/FluidXml/CssTranslator.php (100%) rename {source => src}/FluidXml/FluidAliasesTrait.php (100%) rename {source => src}/FluidXml/FluidContext.php (100%) rename {source => src}/FluidXml/FluidDocument.php (100%) rename {source => src}/FluidXml/FluidHelper.php (100%) rename {source => src}/FluidXml/FluidInsertionHandler.php (100%) rename {source => src}/FluidXml/FluidInterface.php (100%) rename {source => src}/FluidXml/FluidNamespace.php (100%) rename {source => src}/FluidXml/FluidRepeater.php (100%) rename {source => src}/FluidXml/FluidSaveTrait.php (100%) rename {source => src}/FluidXml/FluidXml.php (100%) rename {source => src}/FluidXml/NewableTrait.php (100%) rename {source => src}/FluidXml/ReservedCallStaticTrait.php (100%) rename {source => src}/FluidXml/ReservedCallTrait.php (100%) rename {source => src}/FluidXml/fluid.php (100%) delete mode 100644 support/.common.sh delete mode 100644 support/Codevelox.php delete mode 100755 support/coverage delete mode 100755 support/coveralls delete mode 100644 support/coveralls.php delete mode 100755 support/gendoc delete mode 100755 support/init delete mode 100644 support/peridot.php delete mode 100755 support/speedtest delete mode 100644 support/speedtest.php delete mode 100755 support/test delete mode 100755 support/testd delete mode 100755 support/testdbg delete mode 100755 support/testv create mode 100644 tests/CssTranslatorTest.php create mode 100644 tests/FluidContextTest.php create mode 100644 tests/FluidHelperTest.php create mode 100644 tests/FluidNamespaceTest.php create mode 100644 tests/FluidTestCase.php create mode 100644 tests/FluidXmlTest.php diff --git a/.coveralls.yml b/.coveralls.yml index b3d1afe..6282b9e 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1,4 @@ repo_token: c1DEnhEDEsdeHDUepRI24RibVJ6yDw2kN +src_dir: source +coverage_clover: build/logs/clover.xml +json_path: build/logs/coveralls-upload.json diff --git a/.gitignore b/.gitignore index b246d69..02cdaa2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ /.idea /*.code-workspace +/build/ /composer.lock /doc/api/ /private/ /sandbox/ +/vendor/ +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 841b653..7d86335 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,21 +1,20 @@ build: environment: php: - version: 8.1.24 + version: 8.2 tests: override: - - command: ./support/init && ./support/test && ./support/coverage clover-code-coverage + command: composer install --prefer-dist --no-interaction && php vendor/bin/phpunit --coverage-clover build/logs/clover.xml coverage: - file: 'sandbox/code-coverage-report/clover.xml' + file: 'build/logs/clover.xml' format: 'php-clover' cache: directories: - - sandbox/composer/ + - vendor/ filter: paths: - - source/ + - src/ excluded_paths: - sandbox/ - - specs/ - - support/ + - tests/ diff --git a/.travis.yml b/.travis.yml index 07e989d..81d115d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,14 +4,11 @@ php: - 8.2 cache: directories: - - sandbox/composer/ + - vendor/ install: - composer self-update - - git config --global github.accesstoken 21fd5f444e024f66f292461ca7ea7243f63a200d - - ./support/init + - composer install --prefer-dist --no-interaction script: - - ./support/test -after_script: - - ./support/coveralls "$TRAVIS_JOB_ID" + - php vendor/bin/phpunit --no-coverage notifications: email: false diff --git a/README.md b/README.md index 917ded7..f576a88 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [apis]: https://github.com/servo-php/fluidxml/wiki/APIs [gettingstarted]: https://github.com/servo-php/fluidxml/wiki/Getting-Started [examples]: https://github.com/servo-php/fluidxml/blob/master/doc/Examples/ -[specs]: https://github.com/servo-php/fluidxml/blob/master/specs/FluidXml.php +[specs]: https://github.com/downforcetech/fluidxml.php/blob/main/tests/FluidXmlTest.php [wiki]: https://github.com/servo-php/fluidxml/wiki [bsd]: https://opensource.org/licenses/BSD-2-Clause [license]: https://github.com/servo-php/fluidxml/blob/master/LICENSE.txt @@ -309,7 +309,7 @@ Follow the [Getting Started tutorial][gettingstarted] to become a [ninja][ninja] Many other examples are available: - inside the [`doc/Examples/`][examples] folder -- inside the [`specs/FluidXml.php`][specs] file (as test cases) +- inside the [`tests/FluidXmlTest.php`][specs] file (as test cases) All them cover from the simplest case to the most complex scenario. diff --git a/circle.yml b/circle.yml index 77dfb60..ba2b16b 100644 --- a/circle.yml +++ b/circle.yml @@ -1,13 +1,11 @@ machine: php: - version: 8.1.24 + version: 8.2 dependencies: - pre: - - git config --global github.accesstoken 21fd5f444e024f66f292461ca7ea7243f63a200d override: - - ./support/init + - composer install --prefer-dist --no-interaction cache_directories: - - ./sandbox/composer/ + - ./vendor/ test: override: - - ./support/test + - php vendor/bin/phpunit --no-coverage diff --git a/composer.json b/composer.json index d203f90..ee3d3a1 100644 --- a/composer.json +++ b/composer.json @@ -14,13 +14,10 @@ "role": "Developer" } ], - "config": { - "vendor-dir": "./sandbox/composer/" - }, "autoload": { - "files": ["./source/FluidXml/fluid.php"], + "files": ["./src/FluidXml/fluid.php"], "psr-4": { - "FluidXml\\": "./source/FluidXml/" + "FluidXml\\": "./src/FluidXml/" } }, "require": { @@ -29,9 +26,7 @@ "ext-simplexml": "*" }, "require-dev": { - "peridot-php/peridot": "1", - "peridot-php/peridot-code-coverage-reporters": "1", - "apigen/apigen": "4", - "rector/rector": "0.18" + "phpunit/phpunit": "^11", + "rector/rector": "^2.0" } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..7bf682e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,16 @@ + + + + + tests + + + + + src + + + diff --git a/rector.php b/rector.php index aae5c60..6b49623 100644 --- a/rector.php +++ b/rector.php @@ -4,23 +4,20 @@ use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector; use Rector\Config\RectorConfig; -use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; -use Rector\Set\ValueObject\LevelSetList; +use Rector\Set\ValueObject\SetList; return static function (RectorConfig $rectorConfig): void { $rectorConfig->paths([ __DIR__ . '/doc', - __DIR__ . '/source', - __DIR__ . '/specs', - __DIR__ . '/support', + __DIR__ . '/src', + __DIR__ . '/tests', ]); $rectorConfig->rules([ - InlineConstructorDefaultToPropertyRector::class, - NullToStrictStringFuncCallArgRector::class, + InlineConstructorDefaultToPropertyRector::class, ]); $rectorConfig->sets([ - LevelSetList::UP_TO_PHP_82 + SetList::PHP_82, ]); }; diff --git a/specs/.common.php b/specs/.common.php deleted file mode 100644 index b54c867..0000000 --- a/specs/.common.php +++ /dev/null @@ -1,57 +0,0 @@ - \var_export($actual, true), - 'expected' => \var_export($expected, true) ]; - - $sep = ' '; - $msg_l = \strlen($v['actual'] . $v['expected']); - - if ($msg_l > 60) { - $sep = "\n"; - } - - return "expected " . $v['expected'] . ",$sep" - . "given " . $v['actual'] . "."; -} - -function assert_equal_xml($actual, $expected) -{ - $xml_header = "\n"; - - $actual = \trim($actual->xml()); - $expected = \trim($xml_header . $expected); - \assert($actual === $expected, __($actual, $expected)); -} - -function assert_is_a($actual, $expected) -{ - \assert(\is_a($actual, $expected) === true, __(\get_class($actual), $expected)); -} - -function assert_is_fluid($method, ...$args) -{ - $instance = new FluidXml(); - - if (\method_exists($instance, $method)) { - $actual = \call_user_func([$instance, $method], ...$args); - $expected = FluidInterface::class; - assert_is_a($actual, $expected); - } - - $instance = $instance->query('/*'); - - if (\method_exists($instance, $method)) { - $actual = \call_user_func([$instance, $method], ...$args); - $expected = FluidInterface::class; - assert_is_a($actual, $expected); - } -} diff --git a/specs/FluidXml.php b/specs/FluidXml.php deleted file mode 100644 index f1c41c6..0000000 --- a/specs/FluidXml.php +++ /dev/null @@ -1,3276 +0,0 @@ -xml(); - $expected = $xml->xml(); - \assert($actual === $expected, __($actual, $expected)); - - $options = [ 'root' => 'root', - 'version' => '1.2', - 'encoding' => 'UTF-16', - 'stylesheet' => 'stylesheet.xsl' ]; - - $xml = new FluidXml(null, $options); - $alias = fluidxml(null, $options); - - $actual = $alias->xml(); - $expected = $xml->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); -}); - -describe('fluidify()', function () { - it('should behave like FluidXml::load()', function () { - $ds = \DIRECTORY_SEPARATOR; - $file = __DIR__ . "{$ds}..{$ds}sandbox{$ds}.test_fluidify.xml"; - $doc = "\n" - . " content\n" - . ""; - - \file_put_contents($file, $doc); - $xml = FluidXml::load($file); - $alias = fluidify($file); - \unlink($file); - - $actual = $alias->xml(); - $expected = $xml->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); -}); - -describe('fluidns()', function () { - it('should behave like FluidNamespace::__construct()', function () { - $ns = new FluidNamespace('x', 'x.com'); - $alias = fluidns('x', 'x.com'); - - $actual = $ns->id(); - $expected = $alias->id(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns->uri(); - $expected = $alias->uri(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns->mode(); - $expected = $alias->mode(); - \assert($actual === $expected, __($actual, $expected)); - - $ns = new FluidNamespace('x', 'x.com', FluidNamespace::MODE_IMPLICIT); - $alias = fluidns('x', 'x.com', FluidNamespace::MODE_IMPLICIT); - - $actual = $ns->id(); - $expected = $alias->id(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns->uri(); - $expected = $alias->uri(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns->mode(); - $expected = $alias->mode(); - \assert($actual === $expected, __($actual, $expected)); - }); -}); - -describe('FluidXml', function () { - $ds = \DIRECTORY_SEPARATOR; - $this->out_dir = __DIR__ . "{$ds}..{$ds}sandbox{$ds}"; - - it('should throw invoking not existing staic method', function () { - try { - FluidXml::lload(); - } catch (\Exception $e) { - $actual = $e; - } - - assert_is_a($actual, \Exception::class); - }); - - describe(':load()', function () { - it('should import an XML file', function () { - $file = "{$this->out_dir}.test_load.xml"; - $doc = "\n" - . " content\n" - . ""; - - \file_put_contents($file, $doc); - $xml = FluidXml::load($file); - \unlink($file); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should throw for not existing file', function () { - $err_handler = \set_error_handler(function () {}); - try { - $xml = FluidXml::load('.impossible.xml'); - } catch (\Exception $e) { - $actual = $e; - } - \set_error_handler($err_handler); - - assert_is_a($actual, \Exception::class); - }); - - it('should import an XML file containing a DOCTYPE declaration', function () { - $file = "{$this->out_dir}.test_load_doctype.xml"; - $doc = "\n" - . "\n" - . ""; - - \file_put_contents($file, $doc); - $xml = FluidXml::load($file); - \unlink($file); - - $expected = "\n \n"; - assert_equal_xml($xml, $expected); - }); - - it('should accept libxml flags', function () { - $file = "{$this->out_dir}.test_load_flags.xml"; - $doc = "\n" - . " value\n" - . ""; - - \file_put_contents($file, $doc); - $xml = FluidXml::load($file, \LIBXML_PARSEHUGE); - \unlink($file); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.addChild() with DOCTYPE', function () { - it('should not throw when adding an XML string with a DOCTYPE declaration', function () { - $xml = new FluidXml(null); - $xmlStr = "\n" - . "\n" - . ""; - - $xml->addChild($xmlStr); - - $expected = "\n \n"; - assert_equal_xml($xml, $expected); - }); - }); - - if (\version_compare(\phpversion(), '7', '>=')) { - describe(':new()', function () { - it('should behave like FluidXml::__construct()', function () { - $xml = new FluidXml(); - eval('$alias = \FluidXml\FluidXml::new();'); - - $actual = $alias->xml(); - $expected = $xml->xml(); - \assert($actual === $expected, __($actual, $expected)); - - $options = [ 'root' => 'root', - 'version' => '1.2', - 'encoding' => 'UTF-16', - 'stylesheet' => 'stylesheet.xsl' ]; - - $xml = new FluidXml($options); - eval('$alias = \FluidXml\FluidXml::new($options);'); - - $actual = $alias->xml(); - $expected = $xml->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - } - - describe('.__construct()', function () { - $doc = "\n" - . " content\n" - . ""; - $dom = new \DOMDocument(); - $dom->loadXML($doc); - $stylesheet = ""; - - it('should create an UTF-8 XML-1.0 document with one default root element', function () { - $xml = new FluidXml(); - - $expected = ""; - assert_equal_xml($xml, $expected); - }); - - it('should create an UTF-8 XML-1.0 document with one custom root element as first or second argument', function () { - $xml = new FluidXml('document'); - - $expected = ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(null, ['root' => 'document']); - - assert_equal_xml($xml, $expected); - }); - - it('should create an UTF-8 XML-1.0 document with no root element as first or second argument', function () { - $xml = new FluidXml(null); - - $expected = ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(null, ['root' => null]); - assert_equal_xml($xml, $expected); - - $xml = new FluidXml('doc', ['root' => null]); - assert_equal_xml($xml, $expected); - }); - - it('should create an UTF-8 XML-1.0 document with a stylesheet and a root element', function () use ($stylesheet) { - $xml = new FluidXml('doc', ['stylesheet' => 'http://servo-php.org/fluidxml']); - - $expected = $stylesheet . "\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should create an UTF-8 XML-1.0 document with a stylesheet and no root element', function () use ($stylesheet) { - $xml = new FluidXml(null, ['stylesheet' => 'http://servo-php.org/fluidxml']); - - $expected = $stylesheet; - assert_equal_xml($xml, $expected); - }); - - it('should import an XML string', function () use ($doc, $dom) { - $exp = $dom->saveXML(); - // This $exp has the XML header. - - // The first empty line is used to test the trim of the string. - $xml = new FluidXml("\n " . $exp); - - $expected = $doc; - assert_equal_xml($xml, $expected); - - // This $exp is deprived of the XML header. - $xml = new FluidXml("\n " . \substr($exp, \strpos($exp, "\n") + 1)); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should import an array of elements with the @ syntax', function () { - $xml = new FluidXml(['root' => [ 'child1' => [ '@id' => 1 ], - 'child2' => 'Text 2' ] ]); - - $expected = "\n" - . " \n" - . " Text 2\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should import a DOMDocument', function () use ($doc, $dom) { - $xml = new FluidXml($dom); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should import a DOMNode', function () use ($dom) { - $domxp = new \DOMXPath($dom); - $nodes = $domxp->query('/root/parent'); - $xml = new FluidXml($nodes[0]); - - $expected = "content"; - assert_equal_xml($xml, $expected); - }); - - it('should import a DOMNodeList', function () use ($dom) { - $domxp = new \DOMXPath($dom); - $nodes = $domxp->query('/root/parent'); - $xml = new FluidXml($nodes); - - $expected = "content"; - assert_equal_xml($xml, $expected); - }); - - it('should import a SimpleXMLElement', function () use ($doc, $dom) { - $xml = new FluidXml(\simplexml_import_dom($dom)); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should import a FluidXml', function () use ($doc) { - $xml = new FluidXml(new FluidXml($doc)); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should import a FluidContext', function () use ($doc) { - $cx = (new FluidXml($doc))->query('/root'); - $xml = new FluidXml($cx); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should throw for not supported documents', function () { - try { - $xml = new FluidXml(1); - } catch (\Exception $e) { - $actual = $e; - } - - assert_is_a($actual, \Exception::class); - }); - - it('should throw invoking not existing method', function () { - $xml = new FluidXml(); - try { - $xml->qquery(); - } catch (\Exception $e) { - $actual = $e; - } - - assert_is_a($actual, \Exception::class); - }); - }); - - describe('.namespace()', function () { - it('should be fluid', function () { - assert_is_fluid('namespace', 'a', 'b'); - }); - - it('should accept a namespace', function () { - $xml = new FluidXml(); - $x_ns = new FluidNamespace('x', 'x.com'); - $xx_ns = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); - $nss = $xml->namespace($x_ns) - ->namespace($xx_ns) - ->namespaces(); - - $actual = $nss[$x_ns->id()]; - $expected = $x_ns; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $nss[$xx_ns->id()]; - $expected = $xx_ns; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept an id, an uri and an optional mode flag', function () { - $xml = new FluidXml(); - - $nss = $xml->namespace('x', 'x.com') - ->namespace('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT) - ->namespaces(); - - $actual = $nss['x']->uri(); - $expected = 'x.com'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $nss['x']->mode(); - $expected = FluidNamespace::MODE_EXPLICIT; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $nss['xx']->uri(); - $expected = 'xx.com'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $nss['xx']->mode(); - $expected = FluidNamespace::MODE_IMPLICIT; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept variable namespaces arguments', function () { - $xml = new FluidXml(); - $x_ns = new FluidNamespace('x', 'x.com'); - $xx_ns = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); - - $nss = $xml->namespace($x_ns, $xx_ns) - ->namespaces(); - - $actual = $nss[$x_ns->id()]; - $expected = $x_ns; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $nss[$xx_ns->id()]; - $expected = $xx_ns; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.query()', function () { - it('should be fluid', function () { - assert_is_fluid('query', '.'); - }); - - it('should accept a query that return the root nodes of the document (XPath)', function () { - $xml = new FluidXml(); - $cx = $xml->query('/*'); - - $actual = $cx[0]->nodeName; - $expected = 'doc'; - \assert($actual === $expected, __($actual, $expected)); - - $xml->appendSibling('meta'); - $cx = $xml->query('/*'); - - $actual = $cx[0]->nodeName; - $expected = 'doc'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'meta'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept a query that return the root nodes of the document (CSS)', function () { - $xml = new FluidXml(); - $cx = $xml->query(':root'); - - $actual = $cx[0]->nodeName; - $expected = 'doc'; - \assert($actual === $expected, __($actual, $expected)); - - $xml->appendSibling('meta'); - $cx = $xml->query(':root'); - - $actual = $cx[0]->nodeName; - $expected = 'doc'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'meta'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept an array of queries (XPath)', function () { - $xml = new FluidXml(); - $xml->addChild('html', true) - ->addChild(['head','body']) - ->query(['//html', 'head', '//body']) - ->setAttribute('lang', 'en'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should accept an array of queries (XPath and CSS)', function () { - $xml = new FluidXml(); - $xml->addChild('html', true) - ->addChild(['head','body']) - ->query(['//html', 'head', '//body']) - ->setAttribute('lang', 'en'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should accept a variable number of queries (XPath and CSS)', function () { - $xml = new FluidXml(); - $xml->addChild('html', true) - ->addChild(['head','body']) - ->query('//html', 'head', '//body') - ->setAttribute('lang', 'en'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should support relative queries (XPath)', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('html', true) - ->addChild(['head','body']) - ->query('./body'); - - $actual = $cx[0]->nodeName; - $expected = 'body'; - \assert($actual === $expected, __($actual, $expected)); - - $xml = new FluidXml(); - $xml->addChild('html', true)->addChild(['head','body']); - $cx = $xml->query('/doc/html')->query('./head'); - - $actual = $cx[0]->nodeName; - $expected = 'head'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should query the root of the document from a sub query (XPath)', function () { - $xml = new FluidXml(); - $xml->addChild('html', true) - ->addChild(['head','body']); - $cx = $xml->query('/doc/html/body') - ->addChild('h1') - ->query('/doc/html/head'); - - $actual = $cx[0]->nodeName; - $expected = 'head'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should query the root of the document from a sub query (CSS)', function () { - $xml = new FluidXml(); - $xml->addChild('html', true) - ->addChild(['head','body']); - $cx = $xml->query('body') - ->addChild('h1') - ->query(':root head'); - - $actual = $cx[0]->nodeName; - $expected = 'head'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should perform relative queries (XPath) ascending the DOM tree', function () { - $xml = new FluidXml(); - $xml->addChild('html', true) - ->addChild(['head','body'], true) - ->query('../body') - ->addChild('h1') - ->query('../..') - ->addChild('extra'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . "

\n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should query namespaced nodes (XPath)', function () { - $xml = new FluidXml(); - $x_ns = new FluidNamespace('x', 'x.com'); - $xx_ns = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); - - $xml->namespace($x_ns, $xx_ns); - - $xml->addChild('x:a', true) - ->addChild('x:b', true) - ->addChild('xx:c', true) - ->addChild('xx:d', true) - ->addChild('e', true) - ->addChild('x:f', true) - ->addChild('g'); - - $r = $xml->query('/doc/a'); - - $actual = $r->length(); - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a'); - - $actual = $r[0]->nodeName; - $expected = 'x:a'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b'); - - $actual = $r[0]->nodeName; - $expected = 'x:b'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/c'); - - $actual = $r->length(); - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/xx:c'); - - $actual = $r[0]->nodeName; - $expected = 'c'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d'); - - $actual = $r[0]->nodeName; - $expected = 'd'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e'); - - $actual = $r[0]->nodeName; - $expected = 'e'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e/f'); - - $actual = $r->length(); - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e/x:f'); - - $actual = $r[0]->nodeName; - $expected = 'x:f'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e/x:f/g'); - - $actual = $r[0]->nodeName; - $expected = 'g'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should query namespaced nodes (CSS)', function () { - $xml = new FluidXml(); - $x_ns = new FluidNamespace('x', 'x.com'); - $xx_ns = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); - - $xml->namespace($x_ns, $xx_ns); - - $xml->addChild('x:a', true) - ->addChild('x:b', true) - ->addChild('xx:c', true) - ->addChild('xx:d', true) - ->addChild('e', true) - ->addChild('x:f', true) - ->addChild('g'); - - $r = $xml->query('a'); - - $actual = $r->length(); - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a'); - - $actual = $r[0]->nodeName; - $expected = 'x:a'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b'); - - $actual = $r[0]->nodeName; - $expected = 'x:b'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > c'); - - $actual = $r->length(); - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > xx|c'); - - $actual = $r[0]->nodeName; - $expected = 'c'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > xx|c > xx|d'); - - $actual = $r[0]->nodeName; - $expected = 'd'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > xx|c > xx|d > e'); - - $actual = $r[0]->nodeName; - $expected = 'e'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > xx|c > xx|d > e > f'); - - $actual = $r->length(); - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > xx|c > xx|d > e > x|f'); - - $actual = $r[0]->nodeName; - $expected = 'x:f'; - \assert($actual === $expected, __($actual, $expected)); - - $r = $xml->query('x|a > x|b > xx|c > xx|d > e > x|f > g'); - - $actual = $r[0]->nodeName; - $expected = 'g'; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.__invoke()', function () { - it('should be fluid', function () { - assert_is_fluid('__invoke', '/*'); - }); - - it('should behave like .query()', function () { - $xml = new FluidXml(); - - $actual = $xml('/*'); - $expected = $xml->query('/*'); - \assert($actual == $expected, __($actual, $expected)); - }); - }); - - describe('.each()', function () { - it('should be fluid', function () { - assert_is_fluid('each', function (){}); - }); - - it('should iterate the nodes inside the context', function () { - $xml = new FluidXml(); - - $xml->each(function ($i, $n) { - assert_is_a($this, FluidContext::class); - assert_is_a($n, \DOMNode::class); - $actual = $i; - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - }); - - function eachassert($cx, $i, $n) - { - assert_is_a($cx, FluidContext::class); - assert_is_a($n, \DOMNode::class); - $actual = $i; - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - } - - $xml->each('eachassert'); - - $xml->addChild('child1') - ->addChild('child2'); - - $nodes = []; - $index = 0; - $xml->query('/doc/*') - ->each(function ($i, $n) use (&$nodes, &$index) { - $idx = $i + 1; - $this->setText($n->nodeName . $idx); - $nodes[] = $n; - - $actual = $i; - $expected = $index; - \assert($actual === $expected, __($actual, $expected)); - - ++$index; - }); - - $actual = $nodes; - $expected = $xml->query('/doc/*')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $expected = "\n" - . " child11\n" - . " child22\n" - . ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('child1') - ->addChild('child2'); - - function eachsettext($cx, $i, $n) - { - $idx = $i + 1; - $cx->setText($n->nodeName . $idx); - } - - $xml->query('/doc/*') - ->each('eachsettext'); - - $expected = "\n" - . " child11\n" - . " child22\n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.map()', function () { - it('should map over the nodes inside the context', function () { - $xml = new FluidXml(); - - $xml->map(function ($i, $n) { - assert_is_a($this, FluidContext::class); - assert_is_a($n, \DOMNode::class); - $actual = $i; - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - }); - - function mapassert($cx, $i, $n) - { - assert_is_a($cx, FluidContext::class); - assert_is_a($n, \DOMNode::class); - $actual = $i; - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - } - - $xml->map('mapassert'); - - $xml->addChild(['child1' => 'child1']) - ->addChild(['child2' => 'child2']); - - $actual = $xml->query('/doc/*') - ->map(function ($i, $n) { - $idx = $i + 1; - return $n->nodeValue . $idx; - }); - - $expected = ['child11', 'child22']; - - \assert($actual === $expected, __($actual, $expected)); - - function mapfn($cx, $i, $n) - { - $idx = $i + 1; - return $n->nodeValue . $idx; - } - - $actual = $xml->query('/doc/*')->map('mapfn'); - - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.filter()', function () { - it('should be fluid', function () { - assert_is_fluid('filter', function (){}); - }); - - it('should filter the nodes inside the context', function () { - $xml = new FluidXml(); - - $xml->filter(function ($i, $n) { - assert_is_a($this, FluidContext::class); - assert_is_a($n, \DOMNode::class); - $actual = $i; - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - }); - - function filterassert($cx, $i, $n) - { - assert_is_a($cx, FluidContext::class); - assert_is_a($n, \DOMNode::class); - $actual = $i; - $expected = 0; - \assert($actual === $expected, __($actual, $expected)); - } - - $xml->each('filterassert'); - $xml->times(4)->addChild('child'); - - $index = 0; - $children = $xml->query('//child'); - - $cx = $children->filter(function ($i, $n) use (&$index) { - $actual = $i; - $expected = $index; - \assert($actual === $expected, __($actual, $expected)); - - ++$index; - - if ($i === 0) { - return true; - } - - if (($i % 2) === 0) { - return false; - } - }); - - $actual = $cx->array(); - $expected = [ $children[0], $children[1], $children[3] ]; - \assert($actual === $expected, __($actual, $expected)); - - $cx->setText('not filtered'); - - $expected = "\n" - . " not filtered\n" - . " not filtered\n" - . " \n" - . " not filtered\n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.times()', function () { - it('should be fluid', function () { - assert_is_a((new FluidXml())->times(4), FluidRepeater::class); - assert_is_fluid('times', 4, function () {}); - }); - - it('should repeat the following one method call (if no callable is passed)', function () { - $xml = new FluidXml(); - - $xml->times(2) - ->add('child') - ->add('lastchild'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should switch context', function () { - $xml = new FluidXml(); - - $xml->times(2) - ->add('child', true) - ->add('subchild'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should repeat a closure bound to $this of the context', function () { - $xml = new FluidXml(); - - $xml->add('parent', true) - ->times(2, function ($i) { - $this->add("child{$i}"); - }); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should repeat a callable', function () { - $xml = new FluidXml(); - - function addchild($parent, $i) - { - $parent->add("child{$i}"); - } - - $xml->add('parent', true) - ->times(2, 'addchild'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should repeat a callable without repeating the following method call', function () { - $xml = new FluidXml(); - - $xml->add('parent', true) - ->times(2, function ($i) { - $this->add("child{$i}"); - }) - ->add('lastchild'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.addChild()', function () { - it('should be fluid', function () { - assert_is_fluid('addChild', 'a'); - }); - - it('should add a child using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1') - ->addChild('parent', true) - ->addChild('child2'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1']) - ->addChild(['parent'], true) - ->addChild(['child2']); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with a string value using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1', 'value1') - ->addChild('parent', true) - ->addChild('child2', 'value2'); - - $expected = "\n" - . " value1\n" - . " \n" - . " value2\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with a string value using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => 'value1']) - ->addChild('parent', true) - ->addChild(['child2' => 'value2']); - - $expected = "\n" - . " value1\n" - . " \n" - . " value2\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with an empty string value using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1', '') - ->addChild('parent', true) - ->addChild('child2', ''); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with an empty string value using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => '']) - ->addChild('parent', true) - ->addChild(['child2' => '']); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with a null value using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1', null) - ->addChild('parent', true) - ->addChild('child2', null); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with a null value using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => null]) - ->addChild('parent', true) - ->addChild(['child2' => null]); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with an integer value using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1', 1) - ->addChild('parent', true) - ->addChild('child2', 1); - - $expected = "\n" - . " 1\n" - . " \n" - . " 1\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with an integer value using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => 1]) - ->addChild('parent', true) - ->addChild(['child2' => 1]); - - $expected = "\n" - . " 1\n" - . " \n" - . " 1\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with a 0 value using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1', 0) - ->addChild('parent', true) - ->addChild('child2', 0); - - $expected = "\n" - . " 0\n" - . " \n" - . " 0\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with a 0 value using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => 0]) - ->addChild('parent', true) - ->addChild(['child2' => 0]); - - $expected = "\n" - . " 0\n" - . " \n" - . " 0\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should escape XML special characters in text content using the argument syntax', function () { - $xml = new FluidXml(); - $xml->addChild('child1', 'a & b') - ->addChild('parent', true) - ->addChild('child2', 'a < b'); - - $expected = "\n" - . " a & b\n" - . " \n" - . " a < b\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should escape XML special characters in text content using the array syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => 'Hello & World', 'child2' => 'Tom & Jerry']); - - $expected = "\n" - . " Hello & World\n" - . " Tom & Jerry\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add many children with and without a value', function () { - $xml = new FluidXml(); - $xml->addChild(['child1', 'child2', 'child3' => 'value3', 'child4' => 'value4']) - ->addChild('parent', true) - ->addChild(['child5', 'child6', 'child7' => 'value7', 'child8' => 'value8']); - - $expected = "\n" - . " \n" - . " \n" - . " value3\n" - . " value4\n" - . " \n" - . " \n" - . " \n" - . " value7\n" - . " value8\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add many children of the same name with and without a value', function () { - $xml = new FluidXml(); - $xml->addChild(['child', ['child'], ['child' => 'value1'], ['child' => 'value2']]) - ->addChild('parent', true) - ->addChild(['child', ['child'], ['child' => 'value3'], ['child' => 'value4']]); - - $expected = "\n" - . " \n" - . " \n" - . " value1\n" - . " value2\n" - . " \n" - . " \n" - . " \n" - . " value3\n" - . " value4\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add many children with nested arrays', function () { - $xml = new FluidXml(); - $xml->addChild(['child1'=>['child11'=>['child111', 'child112'=>'value112'], 'child12'=>'value12'], - 'child2'=>['child21', 'child22'=>['child221', 'child222']]]) - ->addChild('parent', true) - ->addChild(['child3'=>['child31'=>['child311', 'child312'=>'value312'], 'child32'=>'value32'], - 'child4'=>['child41', 'child42'=>['child421', 'child422']]]); - - $expected = << - - - - value112 - - value12 - - - - - - - - - - - - - value312 - - value32 - - - - - - - - - - -EOF; - assert_equal_xml($xml, $expected); - }); - - it('should add a child with some attributes', function () { - $xml = new FluidXml(); - $xml->addChild('child1', ['class' => 'Class attr', 'id' => 'Id attr1']) - ->addChild('parent', true) - ->addChild('child2', ['class' => 'Class attr', 'id' => 'Id attr2']); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add many children with some attributes', function () { - $xml = new FluidXml(); - $xml->addChild(['child1', 'child2'], ['class' => 'Class attr', 'id' => 'Id attr1']) - ->addChild('parent', true) - ->addChild(['child3', 'child4'], ['class' => 'Class attr', 'id' => 'Id attr2']); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add children with some attributes and text using the @ syntax', function () { - $xml = new FluidXml(); - $attrs = [ '@class' => 'Class attr', - '@' => 'Text content', - '@id' => 'Id attr' ]; - $xml->addChild(['child1' => $attrs ]) - ->addChild(['child2' => $attrs ], true) - ->addChild(['child3' => $attrs ]); - - $expected = "\n" - . " Text content\n" - . " " - . "Text content" - . "Text content" - . "\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add text content using @:text as alias for @', function () { - $xml = new FluidXml(); - $xml->addChild(['child1' => [ '@:text' => 'Hello' ]]); - - $expected = "\n" - . " Hello\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add CDATA content using @:cdata syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['chapter' => [ - '@id' => '1', - '@:cdata' => 'Ideas About Universe', - ]]); - - $expected = "\n" - . " Universe]]>\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add comment content using @:comment syntax', function () { - $xml = new FluidXml(); - $xml->addChild(['node' => [ '@:comment' => 'This is a comment' ]]); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should switch context', function () { - $xml = new FluidXml(); - - $actual = $xml->addChild('child', true); - assert_is_a($actual, FluidContext::class); - - $actual = $xml->addChild('child', 'value', true); - assert_is_a($actual, FluidContext::class); - - $actual = $xml->addChild(['child1', 'child2'], true); - assert_is_a($actual, FluidContext::class); - - $actual = $xml->addChild(['child1' => 'value1', 'child2' => 'value2'], true); - assert_is_a($actual, FluidContext::class); - - $actual = $xml->addChild('child', ['attr' => 'value'], true); - assert_is_a($actual, FluidContext::class); - - $actual = $xml->addChild(['child1', 'child2'], ['attr' => 'value'], true); - assert_is_a($actual, FluidContext::class); - }); - - it('should add namespaced children', function () { - $xml = new FluidXml(); - $xml->namespace(new FluidNamespace('x', 'x.com')); - $xml->namespace(fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT)); - $xml->addChild('x:xTag1', true) - ->addChild('x:xTag2'); - $xml->addChild('xx:xxTag1', true) - ->addChild('xx:xxTag2') - ->addChild('tag3'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - $doc = "\n" - . " content\n" - . ""; - $dom = new \DOMDocument(); - $dom->loadXML($doc); - - it('should fill the document with an XML string', function () { - $xml = new FluidXml(null); - $xml->addChild(''); - - $expected = ""; - assert_equal_xml($xml, $expected); - }); - - it('should fill the document with an XML string with multiple root nodes', function () { - $xml = new FluidXml(null); - $xml->addChild(''); - - $expected = "\n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add an XML string with multiple root nodes', function () { - $xml = new FluidXml(); - $xml->addChild(''); - - $expected = "\n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild(''); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add a DOMDocument', function () use ($doc) { - $dom = new DOMDocument(); - $dom->loadXML('content'); - - $xml = new FluidXml(); - $xml->addChild($dom); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should add a DOMNode', function () use ($doc, $dom) { - $xp = new \DOMXPath($dom); - $nodes = $xp->query('/doc/parent'); - $xml = new FluidXml(); - $xml->addChild($nodes[0]); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should add a DOMNodeList', function () use ($doc, $dom) { - $xp = new \DOMXPath($dom); - $nodes = $xp->query('/doc/parent'); - $xml = new FluidXml(); - $xml->addChild($nodes); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should add a SimpleXMLElement', function () use ($doc, $dom) { - $sxml = \simplexml_import_dom($dom); - $xml = new FluidXml(); - $xml->addChild($sxml->children()); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should add a FluidXml', function () use ($doc, $dom) { - $nodes = $dom->documentElement->childNodes; - $fxml = new FluidXml($nodes); - $xml = new FluidXml(); - $xml->addChild($fxml); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should add a FluidContext', function () use ($doc, $dom) { - $fxml = (new FluidXml($dom))->query('/doc/parent'); - $xml = new FluidXml(); - $xml->addChild($fxml); - - $expected = $doc; - assert_equal_xml($xml, $expected); - }); - - it('should add many instances', function () use ($doc, $dom) { - $fxml = (new FluidXml($dom))->query('/doc/parent'); - $xml = new FluidXml(); - $xml->addChild([ $fxml, - 'imported' => $fxml ]); - - $expected = "\n" - . " content\n" - . " \n" - . " content\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should throw for not supported input', function () { - $xml = new FluidXml(); - try { - $xml->addChild(0); - } catch (\Exception $e) { - $actual = $e; - } - - assert_is_a($actual, \Exception::class); - }); - }); - - describe('.add()', function () { - it('should be fluid', function () { - assert_is_fluid('add', 'a'); - }); - - it('should behave like .addChild()', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild(['child1', 'child2'], ['class'=>'child']); - - $alias = new FluidXml(); - $alias->add('parent', true) - ->add(['child1', 'child2'], ['class'=>'child']); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.prependSibling()', function () { - it('should be fluid', function () { - assert_is_fluid('prependSibling', 'a'); - }); - - it('should add more than one root node to a document with one root node', function () { - $xml = new FluidXml(); - $xml->prependSibling('meta'); - $xml->prependSibling('extra'); - $cx = $xml->query('/*'); - - $actual = $cx[0]->nodeName; - $expected = 'extra'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'meta'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[2]->nodeName; - $expected = 'doc'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should add more than one root node to a document with no root node', function () { - $xml = new FluidXml(null); - $xml->prependSibling('meta'); - $xml->prependSibling('extra'); - $cx = $xml->query('/*'); - - $actual = $cx[0]->nodeName; - $expected = 'extra'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'meta'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should add a sibling node before a node', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->prependSibling('sibling1') - ->prependSibling('sibling2'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add an XML document instance before a node', function () { - $dom = new DOMDocument(); - $dom->loadXML('content'); - - $xml = new FluidXml(); - $xml->prependSibling($dom); - - $expected = "content\n" - . ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('sibling', true) - ->prependSibling($dom); - - $expected = "\n" - . " content\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.prepend()', function () { - it('should be fluid', function () { - assert_is_fluid('prepend', 'a'); - }); - - it('should behave like .prependSibling()', function () { - $xml = new FluidXml(); - $xml->prependSibling('sibling1', true) - ->prependSibling(['sibling2', 'sibling3'], ['class'=>'sibling']); - - $alias = new FluidXml(); - $alias->prepend('sibling1', true) - ->prepend(['sibling2', 'sibling3'], ['class'=>'sibling']); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.appendSibling()', function () { - it('should be fluid', function () { - assert_is_fluid('appendSibling', 'a'); - }); - - it('should add more than one root node to a document with one root node', function () { - $xml = new FluidXml(); - $xml->appendSibling('meta'); - $xml->appendSibling('extra'); - $cx = $xml->query('/*'); - - $actual = $cx[0]->nodeName; - $expected = 'doc'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'extra'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[2]->nodeName; - $expected = 'meta'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should add more than one root node to a document with no root node', function () { - $xml = new FluidXml(null); - $xml->appendSibling('meta'); - $xml->appendSibling('extra'); - $cx = $xml->query('/*'); - - $actual = $cx[0]->nodeName; - $expected = 'meta'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'extra'; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should add a sibling node after a node', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->appendSibling('sibling1') - ->appendSibling('sibling2'); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add an XML document instance after a node', function () { - $dom = new DOMDocument(); - $dom->loadXML('content'); - - $xml = new FluidXml(); - $xml->appendSibling($dom); - - $expected = "\n" - . "content"; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('sibling', true) - ->appendSibling($dom); - - $expected = "\n" - . " \n" - . " content\n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.append()', function () { - it('should be fluid', function () { - assert_is_fluid('append', 'a'); - }); - - it('should behave like .appendSibling()', function () { - $xml = new FluidXml(); - $xml->appendSibling('sibling1', true) - ->appendSibling(['sibling2', 'sibling3'], ['class'=>'sibling']); - - $alias = new FluidXml(); - $alias->append('sibling1', true) - ->append(['sibling2', 'sibling3'], ['class'=>'sibling']); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.setAttribute()', function () { - it('should be fluid', function () { - assert_is_fluid('setAttribute', 'a', 'b'); - }); - - it('should set the attributes of the root node', function () { - $xml = new FluidXml(); - $xml->setAttribute('attr1', 'Attr1 Value') - ->setAttribute('attr2', 'Attr2 Value'); - - $expected = ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->setAttribute(['attr1' => 'Attr1 Value', - 'attr2' => 'Attr2 Value']); - - $expected = ""; - assert_equal_xml($xml, $expected); - }); - - it('should change the attributes of the root node', function () { - $xml = new FluidXml(); - $xml->setAttribute('attr1', 'Attr1 Value') - ->setAttribute('attr2', 'Attr2 Value'); - - $xml->setAttribute('attr2', 'Attr2 New Value'); - - $expected = ""; - assert_equal_xml($xml, $expected); - - $xml->setAttribute('attr1', 'Attr1 New Value'); - - $expected = ""; - assert_equal_xml($xml, $expected); - }); - - it('should set the attributes of a node', function () { - $xml = new FluidXml(); - $xml->addChild('child', true) - ->setAttribute('attr1', 'Attr1 Value') - ->setAttribute('attr2', 'Attr2 Value'); - - $expected = "\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('child', true) - ->setAttribute(['attr1' => 'Attr1 Value', - 'attr2' => 'Attr2 Value']); - - $expected = "\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should set the attributes, without values, of a node', function () { - $xml = new FluidXml(); - $xml->addChild('child', true) - ->setAttribute('attr1') - ->setAttribute('attr2'); - - $expected = "\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('child', true) - ->setAttribute(['attr1', 'attr2']); - - $expected = "\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should change the attributes of a node', function () { - $xml = new FluidXml(); - $xml->addChild('child', true) - ->setAttribute('attr1', 'Attr1 Value') - ->setAttribute('attr2', 'Attr2 Value') - ->setAttribute('attr2', 'Attr2 New Value'); - - $expected = "\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - - $xml = new FluidXml(); - $xml->addChild('child', true) - ->setAttribute(['attr1' => 'Attr1 Value', - 'attr2' => 'Attr2 Value']) - ->setAttribute('attr1', 'Attr1 New Value'); - - $expected = "\n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.attr()', function () { - it('should be fluid', function () { - assert_is_fluid('attr', 'a', 'b'); - }); - - it('should behave like .setAttribute()', function () { - $xml = new FluidXml(); - $xml->setAttribute('attr1', 'Value 1') - ->setAttribute('attr2') - ->setAttribute(['attr3' => 'Value 3', 'attr4' => 'Value 4']) - ->setAttribute(['attr5', 'attr6']) - ->addChild('child', true) - ->setAttribute('attr1', 'Value 1') - ->setAttribute('attr2') - ->setAttribute(['attr3' => 'Value 3', 'attr4' => 'Value 4']) - ->setAttribute(['attr5', 'attr6']); - - $alias = new FluidXml(); - $alias->attr('attr1', 'Value 1') - ->attr('attr2') - ->attr(['attr3' => 'Value 3', 'attr4' => 'Value 4']) - ->attr(['attr5', 'attr6']) - ->addChild('child', true) - ->attr('attr1', 'Value 1') - ->attr('attr2') - ->attr(['attr3' => 'Value 3', 'attr4' => 'Value 4']) - ->attr(['attr5', 'attr6']); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.setText()', function () { - it('should be fluid', function () { - assert_is_fluid('setText', 'a'); - }); - - it('should set/change the text of the root node', function () { - $xml = new FluidXml(); - $xml->setText('First Text') - ->setText('Second Text'); - - $expected = "Second Text"; - assert_equal_xml($xml, $expected); - }); - - it('should set/change the text of a node', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('p', true); - $cx->setText('First Text') - ->setText('Second Text'); - - $expected = "\n" - . "

Second Text

\n" - . "
"; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.text()', function () { - it('should be fluid', function () { - assert_is_fluid('text', 'a'); - }); - - it('should behave like .setText()', function () { - $xml = new FluidXml(); - $xml->setText('Text1') - ->addChild('child', true) - ->setText('Text2'); - - $alias = new FluidXml(); - $alias->text('Text1') - ->addChild('child', true) - ->text('Text2'); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.addText()', function () { - it('should be fluid', function () { - assert_is_fluid('addText', 'a'); - }); - - it('should add text to the root node', function () { - $xml = new FluidXml(); - $xml->addText('First Line') - ->addText('Second Line'); - - $expected = "First LineSecond Line"; - assert_equal_xml($xml, $expected); - }); - - it('should add text to a node', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('p', true); - $cx->addText('First Line') - ->addText('Second Line'); - - $expected = "\n" - . "

First LineSecond Line

\n" - . "
"; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.setCdata()', function () { - it('should be fluid', function () { - assert_is_fluid('setCdata', 'a'); - }); - - it('should set/change the CDATA of the root node', function () { - $xml = new FluidXml(); - $xml->setCdata('First Data') - ->setCdata('Second Data'); - - $expected = ""; - assert_equal_xml($xml, $expected); - }); - - it('should set/change the CDATA of a node', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('p', true); - $cx->setCdata('First Data') - ->setCdata('Second Data'); - - $expected = "\n" - . "

\n" - . "
"; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.cdata()', function () { - it('should be fluid', function () { - assert_is_fluid('cdata', 'a'); - }); - - it('should behave like .setCdata()', function () { - $xml = new FluidXml(); - $xml->setCdata('Text1') - ->addChild('child', true) - ->setCdata('Text2'); - - $alias = new FluidXml(); - $alias->cdata('Text1') - ->addChild('child', true) - ->cdata('Text2'); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.addCdata()', function () { - it('should be fluid', function () { - assert_is_fluid('addCdata', 'a'); - }); - - it('should add CDATA to the root node', function () { - $xml = new FluidXml(); - $xml->addCdata('// <, > are characters that should be escaped in a XML context.') - ->addCdata('// Even & is a characters that should be escaped in a XML context.'); - - $expected = "" - . " are characters that should be escaped in a XML context.]]>" - . "" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add CDATA to a node', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('pre', true); - $cx->addCdata('// <, > are characters that should be escaped in a XML context.') - ->addCdata('// Even & is a characters that should be escaped in a XML context.'); - - $expected = "\n" - . "
"
-                                  . " are characters that should be escaped in a XML context.]]>"
-                                  . ""
-                                  .    "
\n" - . "
"; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.setComment()', function () { - it('should be fluid', function () { - assert_is_fluid('setComment', 'a'); - }); - - it('should set/change the comment of the root node', function () { - $xml = new FluidXml(); - $xml->setComment('First') - ->setComment('Second'); - - $expected = ""; - assert_equal_xml($xml, $expected); - }); - - it('should set/change the comment of a node', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('p', true); - $cx->setComment('First') - ->setComment('Second'); - - $expected = "\n" - . "

\n" - . "
"; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.comment()', function () { - it('should be fluid', function () { - assert_is_fluid('comment', 'a'); - }); - - it('should behave like .setComment()', function () { - $xml = new FluidXml(); - $xml->setComment('Text1') - ->addChild('child', true) - ->setComment('Text2'); - - $alias = new FluidXml(); - $alias->comment('Text1') - ->addChild('child', true) - ->comment('Text2'); - - $actual = $xml->xml(); - $expected = $alias->xml(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.addComment()', function () { - it('should be fluid', function () { - assert_is_fluid('addComment', 'a'); - }); - - it('should add comments to the root node', function () { - $xml = new FluidXml(); - $xml->addComment('First') - ->addComment('Second'); - - $expected = "\n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - - it('should add comments to a node', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('pre', true); - $cx->addComment('First') - ->addComment('Second'); - - $expected = "\n" - . "
\n"
-                                  . "    \n"
-                                  . "    \n"
-                                  . "  
\n" - . "
"; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.remove()', function () { - $expected = "\n" - . " \n" - . ""; - - $new_doc = function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild(['child1', 'child2'], ['class'=>'removable']); - - return $xml; - }; - - it('should be fluid', function () { - assert_is_fluid('remove', 'a'); - }); - - it('should remove the root node', function () use ($new_doc) { - $xml = $new_doc(); - $xml->remove(); - - assert_equal_xml($xml, ''); - }); - - it('should remove the results of the previous query', function () use ($new_doc, $expected) { - $xml = $new_doc(); - $xml->query('//*[@class="removable"]')->remove(); - - assert_equal_xml($xml, $expected); - }); - - it('should remove the absolute and relative targets of a query (XPath)', function () use ($new_doc, $expected) { - $xml = $new_doc(); - $xml->remove('//*[@class="removable"]'); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc')->remove('//*[@class="removable"]'); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc/parent')->remove('./*[@class="removable"]'); - - assert_equal_xml($xml, $expected); - }); - - it('should remove the absolute and relative targets of a query (CSS)', function () use ($new_doc, $expected) { - $xml = $new_doc(); - $xml->remove('.removable'); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc')->remove(':root .removable'); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc/parent')->remove('.removable'); - - assert_equal_xml($xml, $expected); - }); - - it('should remove the absolute and relative targets of an array of queries (XPath and CSS)', function () use ($new_doc, $expected) { - $xml = $new_doc(); - $xml->remove(['//child1', ':root child2']); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc')->remove(['//child1', ':root child2']); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc/parent')->remove(['./child1', 'child2']); - - assert_equal_xml($xml, $expected); - }); - - it('should remove the absolute and relative targets of a variable list of queries (XPath and CSS)', function () use ($new_doc, $expected) { - $xml = $new_doc(); - $xml->remove('//child1', ':root child2'); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc')->remove('//child1', ':root child2'); - - assert_equal_xml($xml, $expected); - - $xml = $new_doc(); - $xml->query('/doc/parent')->remove('./child1', 'child2'); - - assert_equal_xml($xml, $expected); - }); - }); - - describe('.dom()', function () { - it('should return the associated DOMDocument instace', function () { - $xml = new FluidXml(); - - $actual = $xml->dom(); - assert_is_a($actual, \DOMDocument::class); - - $actual = $xml->query('/*')->dom(); - assert_is_a($actual, \DOMDocument::class); - }); - }); - - describe('.xml()', function () { - it('should return the document as XML string', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild('child', 'content'); - - $expected = "\n" - . " \n" - . " content\n" - . " \n" - . ""; - - assert_equal_xml($xml, $expected); - }); - - it('should return the document as XML string without the XML headers (declaration and stylesheet)', function () { - $xml = new FluidXml('doc', ['stylesheet' => 'x.com/style.xsl']); - $xml->addChild('parent', true) - ->addChild('child', 'content'); - - $actual = $xml->xml(true); - $expected = "\n" - . " \n" - . " content\n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should return a node and the descendants as XML string', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addText('parent content') - ->addChild('child', 'content'); - - $actual = $xml->query('//parent')->xml(); - $expected = "parent contentcontent"; - \assert($actual === $expected, __($actual, $expected)); - - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild('child', 'content1') - ->addChild('child', 'content2'); - - $actual = $xml->query('//child')->xml(); - $expected = "content1\n" - . "content2"; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should render empty elements as explicit closing tags with LIBXML_NOEMPTYTAG', function () { - $xml = new FluidXml(); - $xml->addChild(['parent' => ['child1', 'child2']]); - - $actual = $xml->xml(true, \LIBXML_NOEMPTYTAG); - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should pass LIBXML_NOEMPTYTAG through when querying a context', function () { - $xml = new FluidXml(); - $xml->addChild(['parent' => ['child1', 'child2']]); - - $actual = $xml->query('//parent')->xml(false, \LIBXML_NOEMPTYTAG); - $expected = "\n" - . " \n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.toArray()', function () { - it('should export a simple element with text content', function () { - $xml = new FluidXml(); - $xml->addChild(['type' => 'Herbivore']); - - $actual = $xml->toArray(); - $expected = ['doc' => ['type' => 'Herbivore']]; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should export attributes using the @ prefix', function () { - $xml = new FluidXml(); - $xml->addChild(['animal' => ['@operation' => 'create', '@' => 'data']]); - - $actual = $xml->toArray(); - $expected = ['doc' => ['animal' => ['@operation' => 'create', '@' => 'data']]]; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should export nested elements as nested arrays', function () { - $xml = new FluidXml(); - $xml->addChild(['animal' => [ - '@operation' => 'create', - 'type' => 'Herbivore', - 'attribute' => ['legs' => '4', 'head' => '1'], - ]]); - - $actual = $xml->toArray(); - $expected = ['doc' => [ - 'animal' => [ - '@operation' => 'create', - 'type' => 'Herbivore', - 'attribute' => ['legs' => '4', 'head' => '1'], - ], - ]]; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should export repeated sibling elements as indexed arrays', function () { - $xml = new FluidXml(); - $xml->addChild(['chapters' => [ - ['chapter' => 'One'], - ['chapter' => 'Two'], - ]]); - - $actual = $xml->toArray(); - $expected = ['doc' => [ - 'chapters' => [ - ['chapter' => 'One'], - ['chapter' => 'Two'], - ], - ]]; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should export an empty element as null', function () { - $xml = new FluidXml(); - $xml->addChild('empty'); - - $actual = $xml->toArray(); - $expected = ['doc' => ['empty' => null]]; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should export queried nodes via FluidContext', function () { - $xml = new FluidXml(); - $xml->addChild(['animal' => ['type' => 'Herbivore', 'legs' => '4']]); - - $actual = $xml->query('//type')->toArray(); - $expected = [['type' => 'Herbivore']]; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should export namespace declarations using the @xmlns prefix', function () { - $xml = new FluidXml('animal'); - $xml->namespace(new FluidNamespace('foo', 'http://foo.com')); - $xml->addChild('foo:legs', '4'); - - $actual = $xml->toArray(); - // The xmlns:foo declaration appears on foo:legs (where it is first used). - // Text content is under '@' when co-present with namespace metadata. - $expected = ['animal' => [ - 'foo:legs' => [ - '@xmlns:foo' => 'http://foo.com', - '@' => '4', - ], - ]]; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.toObject()', function () { - it('should return a stdClass mirroring toArray()', function () { - $xml = new FluidXml(); - $xml->addChild(['animal' => ['@operation' => 'create', 'type' => 'Herbivore']]); - - $obj = $xml->toObject(); - \assert($obj instanceof \stdClass, __($obj, 'stdClass')); - \assert($obj->doc instanceof \stdClass, __($obj->doc, 'stdClass')); - \assert($obj->doc->animal instanceof \stdClass, __($obj->doc->animal, 'stdClass')); - \assert($obj->doc->animal->{'@operation'} === 'create', __($obj->doc->animal->{'@operation'}, 'create')); - \assert($obj->doc->animal->type === 'Herbivore', __($obj->doc->animal->type, 'Herbivore')); - }); - - it('should preserve indexed arrays for repeated siblings', function () { - $xml = new FluidXml(); - $xml->addChild(['chapters' => [ - ['chapter' => 'One'], - ['chapter' => 'Two'], - ]]); - - $obj = $xml->toObject(); - \assert(\is_array($obj->doc->chapters), __($obj->doc->chapters, 'array')); - \assert($obj->doc->chapters[0] instanceof \stdClass, __($obj->doc->chapters[0], 'stdClass')); - \assert($obj->doc->chapters[0]->chapter === 'One', __($obj->doc->chapters[0]->chapter, 'One')); - }); - - it('should return an array of stdClass for FluidContext', function () { - $xml = new FluidXml(); - $xml->addChild(['animal' => ['type' => 'Herbivore', 'legs' => '4']]); - - $result = $xml->query('//animal')->toObject(); - \assert(\is_array($result), __($result, 'array')); - \assert($result[0] instanceof \stdClass, __($result[0], 'stdClass')); - \assert($result[0]->animal instanceof \stdClass, __($result[0]->animal, 'stdClass')); - \assert($result[0]->animal->type === 'Herbivore', __($result[0]->animal->type, 'Herbivore')); - }); - }); - - describe('.__toString()', function () { - it('should behave like .xml()', function () { - $xml = new FluidXml(); - $cx = $xml->addChild('parent', true) - ->addChild(['child1', 'child2']); - - $actual = \trim("$xml"); - $expected = "\n" - . "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - - $actual = "$cx"; - $expected = "\n" - . " \n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.html()', function () { - it('should return the document as valid HTML 5 string', function () { - $xml = new FluidXml([ - 'html' => [ 'body' => [ 'input', // Void. - 'div', /* Not void. */ ] ] ]); - - $actual = $xml->html(); - $expected = "\n" - . "\n" - . " \n" - . " \n" - . "
\n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should return the document as valid HTML 5 string without the doctype', function () { - $xml = new FluidXml([ - 'html' => [ 'body' => [ 'input', // Void. - 'div', /* Not void. */ ] ] ]); - - $actual = $xml->html(true); - $expected = "\n" - . " \n" - . " \n" - . "
\n" - . " \n" - . ""; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should return a node and the descendants as HTML string', function () { - $xml = new FluidXml([ - 'html' => [ 'body' => [ 'input', // Void. - 'div', /* Not void. */ ] ] ]); - - $actual = $xml->query('//body/*')->html(); - $expected = "\n" - . "
"; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.save()', function () { - it('should be fluid', function () { - $file = "{$this->out_dir}.test_save0.xml"; - assert_is_fluid('save', $file); - \unlink($file); - }); - - it('should store the entire XML document in a file', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild('child', 'content'); - - $file = "{$this->out_dir}.test_save1.xml"; - $xml->save($file); - - $actual = \trim(\file_get_contents($file)); - $expected = "\n" - . "\n" - . " \n" - . " content\n" - . " \n" - . ""; - - \unlink($file); - - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should store a fragment of the XML document in a file', function () { - $xml = new FluidXml(); - $xml->addChild('parent', true) - ->addChild('child', 'content'); - - $file = "{$this->out_dir}.test_save2.xml"; - $xml->query('//child')->save($file); - - $actual = \trim(\file_get_contents($file)); - $expected = "content"; - - \unlink($file); - - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should throw for not writable file', function () { - $xml = new FluidXml(); - - $err_handler = \set_error_handler(function () {}); - try { - $xml->save('/.impossible/tmp/out.xml'); - } catch (\Exception $e) { - $actual = $e; - } - \set_error_handler($err_handler); - - assert_is_a($actual, \Exception::class); - }); - }); -}); - -describe('FluidContext', function () { - it('should be iterable returning the represented DOMNode objects', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head', 'body'], true); - - $actual = $cx; - assert_is_a($actual, \Iterator::class); - - $representation = []; - foreach ($cx as $k => $v) { - $actual = \is_int($k); - $expected = true; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $v; - assert_is_a($actual, \DOMNode::class); - - $representation[$k] = $v->nodeName; - } - - $actual = $representation; - $expected = [0 => 'head', 1 => 'body']; - \assert($actual === $expected, __($actual, $expected)); - }); - - describe('.__construct()', function () { - it('should accept a DOMDocument', function () { - $xml = new FluidXml(); - - $doc = new FluidDocument(); - $handler = new FluidInsertionHandler($doc); - $new_cx = new FluidContext($doc, $handler, $xml->dom()); - - $actual = $new_cx[0]; - $expected = $xml->dom(); - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept a DOMNode', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head'], true); - - $doc = new FluidDocument(); - $handler = new FluidInsertionHandler($doc); - $new_cx = new FluidContext($doc, $handler, $cx[0]); - - $actual = $new_cx->array(); - $expected = $cx->array(); - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept an array of DOMNode', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head', 'body'], true); - - $doc = new FluidDocument(); - $handler = new FluidInsertionHandler($doc); - $new_cx = new FluidContext($doc, $handler, $cx->array()); - - $actual = $new_cx->array(); - $expected = $cx->array(); - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept a DOMNodeList', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head', 'body'], true); - $dom = $xml->dom(); - - $domxp = new \DOMXPath($dom); - $nodes = $domxp->query('/doc/*'); - - $doc = new FluidDocument(); - $handler = new FluidInsertionHandler($doc); - $new_cx = new FluidContext($doc, $handler, $nodes); - - $actual = $new_cx->array(); - $expected = $cx->array(); - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should accept a FluidContext', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head', 'body'], true); - - $doc = new FluidDocument(); - $handler = new FluidInsertionHandler($doc); - $new_cx = new FluidContext($doc, $handler, $cx); - - $actual = $new_cx->array(); - $expected = $cx->array(); - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should throw for not supported document', function () { - $doc = new FluidDocument(); - $handler = new FluidInsertionHandler($doc); - - try { - new FluidContext($doc, $handler, 'node'); - } catch (\Exception $e) { - $actual = $e; - } - - assert_is_a($actual, \Exception::class); - }); - }); - - describe('[]', function () { - it('should access the nodes inside the context', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head', 'body'], true); - - $actual = $cx[0]; - assert_is_a($actual, \DOMElement::class); - - $actual = $cx[1]; - assert_is_a($actual, \DOMElement::class); - }); - - it('should behave like an array', function () { - $xml = new FluidXml(); - $cx = $xml->addChild(['head', 'body', 'extra'], true); - - $actual = isset($cx[0]); - $expected = true; - \assert($actual === $expected, __($actual, $expected)); - - $actual = isset($cx[3]); - $expected = false; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[3]; - $expected = null; - \assert($actual === $expected, __($actual, $expected)); - - try { - $cx[] = "value"; - } catch (\Exception $e) { - $actual = $e; - } - assert_is_a($actual, \Exception::class); - - unset($cx[1]); - - $actual = $cx[0]->nodeName; - $expected = 'head'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx[1]->nodeName; - $expected = 'extra'; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.array()', function () { - it('should return an array of nodes inside the context', function () { - $xml = new FluidXml(null); - - $a = $xml->array(); - - $actual = \is_array($a); - $expected = True; - \assert($actual === $expected, __($actual, $expected)); - - $actual = \count($a); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $a; - $expected = [ $xml->dom() ]; - \assert($actual === $expected, __($actual, $expected)); - - $xml = new FluidXml(); - - $a = $xml->array(); - - $actual = \is_array($a); - $expected = True; - \assert($actual === $expected, __($actual, $expected)); - - $actual = \count($a); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $a; - $expected = [ $xml->dom()->documentElement ]; - \assert($actual === $expected, __($actual, $expected)); - - $cx = $xml->addChild(['head', 'body'], true); - - $a = $cx->array(); - - $actual = \is_array($a); - $expected = True; - \assert($actual === $expected, __($actual, $expected)); - - $actual = \count($a); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.length()', function () { - it('should return the number of nodes inside the context', function () { - $xml = new FluidXml(); - $cx = $xml->query('/*'); - - $actual = $xml->length(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $cx->length(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - - $cx = $xml->addChild(['child1', 'child2'], true); - $actual = $cx->length(); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - - $cx = $cx->addChild(['subchild1', 'subchild2', 'subchild3']); - $actual = $cx->length(); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - - $cx = $cx->addChild(['subchild4', 'subchild5', 'subchild6', 'subchild7'], true); - $actual = $cx->length(); - $expected = 8; - \assert($actual === $expected, __($actual, $expected)); - - $expected = "\n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . " \n" - . ""; - assert_equal_xml($xml, $expected); - }); - }); - - describe('.size()', function () { - it('should behave like .length()', function () { - $xml = new FluidXml(); - - $actual = $xml->size(); - $expected = $xml->length(); - \assert($actual === $expected, __($actual, $expected)); - - $cx = $xml->addChild('parent', true) - ->addChild(['child1', 'child2']); - - $actual = $cx->size(); - $expected = $cx->length(); - \assert($actual === $expected, __($actual, $expected)); - }); - }); -}); - -describe('FluidNamespace', function () { - describe('.__construct()', function () { - it('should accept an id, an uri and an optional mode flag', function () { - $ns_id = 'x'; - $ns_uri = 'x.com'; - $ns_mode = FluidNamespace::MODE_EXPLICIT; - $ns = new FluidNamespace($ns_id, $ns_uri); - - $actual = $ns->id(); - $expected = $ns_id; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns->uri(); - $expected = $ns_uri; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns->mode(); - $expected = $ns_mode; - \assert($actual === $expected, __($actual, $expected)); - - $ns_mode = FluidNamespace::MODE_IMPLICIT; - $ns = new FluidNamespace($ns_id, $ns_uri, $ns_mode); - - $actual = $ns->mode(); - $expected = $ns_mode; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.id()', function () { - it('should return the namespace id', function () { - $ns_id = 'x'; - $ns_uri = 'x.com'; - $ns = new FluidNamespace($ns_id, $ns_uri); - - $actual = $ns->id(); - $expected = $ns_id; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.uri()', function () { - it('should return the namespace uri', function () { - $ns_id = 'x'; - $ns_uri = 'x.com'; - $ns = new FluidNamespace($ns_id, $ns_uri); - - $actual = $ns->uri(); - $expected = $ns_uri; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.mode()', function () { - it('should return the namespace mode', function () { - $ns_id = 'x'; - $ns_uri = 'x.com'; - $ns = new FluidNamespace($ns_id, $ns_uri); - $ns_mode = FluidNamespace::MODE_EXPLICIT; - - $actual = $ns->mode(); - $expected = $ns_mode; - \assert($actual === $expected, __($actual, $expected)); - - $ns_mode = FluidNamespace::MODE_IMPLICIT; - $ns = new FluidNamespace($ns_id, $ns_uri, $ns_mode); - - $actual = $ns->mode(); - $expected = $ns_mode; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('.querify()', function () { - it('should format an XPath query to use the namespace id', function () { - $ns = new FluidNamespace('x', 'x.com'); - - $actual = $ns('current/child'); - $expected = 'x:current/x:child'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns('//current/child'); - $expected = '//x:current/x:child'; - \assert($actual === $expected, __($actual, $expected)); - - $ns = new FluidNamespace('x', 'x.com', FluidNamespace::MODE_IMPLICIT); - - $actual = $ns('current/child'); - $expected = 'x:current/x:child'; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $ns('//current/child'); - $expected = '//x:current/x:child'; - \assert($actual === $expected, __($actual, $expected)); - }); - }); -}); - -describe('FluidHelper', function () { - describe(':isAnXmlString()', function () { - it('should understand if a string is an XML document', function () { - $xml = new FluidXml(); - - $actual = FluidHelper::isAnXmlString($xml->xml()); - $expected = true; - \assert($actual === $expected, __($actual, $expected)); - - $actual = FluidHelper::isAnXmlString(" \n \n \t" . $xml->xml()); - $expected = true; - \assert($actual === $expected, __($actual, $expected)); - - $actual = FluidHelper::isAnXmlString('item'); - $expected = false; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe(':domdocumentToHtml()', function () { - it('should convert a DOMDocument instance to an HTML string without respecting void and not void tags.', function () { - // This is only to analyze a condition (not used) for the code coverage reporter. - FluidHelper::domdocumentToHtml((new FluidXml())->dom(), true); - }); - }); - - describe(':domdocumentToStringWithoutHeaders()', function () { - it('should convert a DOMDocument instance to an XML string without the XML headers (declaration and stylesheets)', function () { - $xml = new FluidXml(); - - $actual = FluidHelper::domdocumentToStringWithoutHeaders($xml->dom()); - $expected = ""; - \assert($actual === $expected, __($actual, $expected)); - - $xml = new FluidXml('doc', ['stylesheet' => 'x.com/style.xsl']); - - $actual = FluidHelper::domdocumentToStringWithoutHeaders($xml->dom()); - $expected = ""; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe(':domnodelistToString()', function () { - it('should convert a DOMNodeList instance to an XML string', function () { - $xml = new FluidXml(); - $nodes = $xml->dom()->childNodes; - - $actual = FluidHelper::domnodelistToString($nodes); - $expected = ""; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe(':domnodesToString()', function () { - it('should convert an array of DOMNode instances to an XML string', function () { - $xml = new FluidXml(); - $nodes = [ $xml->dom()->documentElement ]; - - $actual = FluidHelper::domnodesToString($nodes); - $expected = ""; - \assert($actual === $expected, __($actual, $expected)); - }); - }); - - describe('simplexmlToStringWithoutHeaders()', function () { - it('should convert a SimpleXMLElement instance to an XML string without the XML headers (declaration and stylesheets)', function () { - $xml = \simplexml_import_dom((new FluidXml())->dom()); - - $actual = FluidHelper::simplexmlToStringWithoutHeaders($xml); - $expected = ""; - \assert($actual === $expected, __($actual, $expected)); - }); - }); -}); - -describe('CssTranslator', function () { - describe('.xpath()', function () { - $hml = new FluidXml([ 'html' => [ - 'body' => [ - 'div' => [ - [ 'p' => [ '@class' => 'a', '@id' => '123', [ 'span' ] ] ], - [ 'h1' => [ '@class' => 'b' ] ], - [ 'shape' => [ '@class' => 'c' ] ], - [ 'p' => [ '@class' => 'a b' ] ], - [ 'p' => [ '@class' => 'a' ] ], - [ 'span' => [ '@class' => 'b' ] ], - ]]]]); - - $hml->namespace('svg', 'http://svg.org'); - $hml->query('//body') - ->add('svg:svg', true) - ->add('svg:shape') - ->add('svg:shape'); - - it('should support the CSS selector A', function () use ($hml) { - $actual = $hml->query('p')->array(); - $expected = $hml->query('//p')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('p')->size(); - $expected = 3; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector ns|A', function () use ($hml) { - $actual = $hml->query('svg|shape')->array(); - $expected = $hml->query('//svg:shape')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('svg|shape')->size(); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector *|A', function () use ($hml) { - $actual = $hml->query('*|shape')->array(); - $expected = $hml->query('[local-name() = "shape"]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('*|shape')->size(); - $expected = 3; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector :root', function () use ($hml) { - $actual = $hml->query(':root')->array(); - $expected = $hml->query('/*')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query(':root')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector #id', function () use ($hml) { - $actual = $hml->query('#123')->array(); - $expected = $hml->query('//*[@id="123"]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('#123')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector .class.class', function () use ($hml) { - $actual = $hml->query('.a')->array(); - $expected = $hml->query('//p')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('.a')->size(); - $expected = 3; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('.a.b')->array(); - $expected = $hml->query('//p[2]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('.a.b')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('h1.b')->array(); - $expected = $hml->query('//h1')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('h1.b')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector [attr]', function () use ($hml) { - $actual = $hml->query('[class]')->array(); - $expected = $hml->query('//div/*')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('[class]')->size(); - $expected = 6; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('[id]')->array(); - $expected = $hml->query('//*[@id]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('[id]')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector [attr="val"]', function () use ($hml) { - $actual = $hml->query('p[id="123"]')->array(); - $expected = $hml->query('//p[@id]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('p[id="123"]')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('[class="a"]')->array(); - $expected = $hml->query('//*[@class="a"]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('[class="a"]')->size(); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector A B', function () use ($hml) { - $actual = $hml->query('div span')->array(); - $expected = $hml->query('//div//span')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('div span')->size(); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector A > B', function () use ($hml) { - $actual = $hml->query('div > p')->array(); - $expected = $hml->query('//div/p')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('div > p')->size(); - $expected = 3; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector A, B', function () use ($hml) { - $actual = $hml->query('p, div')->array(); - $expected = $hml->query('//p|//div')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('p, div')->size(); - $expected = 4; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector A + B', function () use ($hml) { - $actual = $hml->query('p + p')->array(); - $expected = $hml->query('//p[3]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('p + p')->size(); - $expected = 1; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support the CSS selector A ~ B', function () use ($hml) { - $actual = $hml->query('h1 ~ p')->array(); - $expected = $hml->query('//p[2]|//p[3]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query('h1 ~ p')->size(); - $expected = 2; - \assert($actual === $expected, __($actual, $expected)); - }); - - it('should support mixing CSS selectors :root #123 span, div, :root .a', function () use ($hml) { - $actual = $hml->query(':root #123 span, div, :root .a')->array(); - $expected = $hml->query('//p/span|//div|//*[@class="a"]|//*[@class="a b"]')->array(); - \assert($actual === $expected, __($actual, $expected)); - - $actual = $hml->query(':root #123 span, div, :root .a')->size(); - $expected = 5; - \assert($actual === $expected, __($actual, $expected)); - }); - }); -}); diff --git a/source/FluidXml.php b/src/FluidXml.php similarity index 100% rename from source/FluidXml.php rename to src/FluidXml.php diff --git a/source/FluidXml/CssTranslator.php b/src/FluidXml/CssTranslator.php similarity index 100% rename from source/FluidXml/CssTranslator.php rename to src/FluidXml/CssTranslator.php diff --git a/source/FluidXml/FluidAliasesTrait.php b/src/FluidXml/FluidAliasesTrait.php similarity index 100% rename from source/FluidXml/FluidAliasesTrait.php rename to src/FluidXml/FluidAliasesTrait.php diff --git a/source/FluidXml/FluidContext.php b/src/FluidXml/FluidContext.php similarity index 100% rename from source/FluidXml/FluidContext.php rename to src/FluidXml/FluidContext.php diff --git a/source/FluidXml/FluidDocument.php b/src/FluidXml/FluidDocument.php similarity index 100% rename from source/FluidXml/FluidDocument.php rename to src/FluidXml/FluidDocument.php diff --git a/source/FluidXml/FluidHelper.php b/src/FluidXml/FluidHelper.php similarity index 100% rename from source/FluidXml/FluidHelper.php rename to src/FluidXml/FluidHelper.php diff --git a/source/FluidXml/FluidInsertionHandler.php b/src/FluidXml/FluidInsertionHandler.php similarity index 100% rename from source/FluidXml/FluidInsertionHandler.php rename to src/FluidXml/FluidInsertionHandler.php diff --git a/source/FluidXml/FluidInterface.php b/src/FluidXml/FluidInterface.php similarity index 100% rename from source/FluidXml/FluidInterface.php rename to src/FluidXml/FluidInterface.php diff --git a/source/FluidXml/FluidNamespace.php b/src/FluidXml/FluidNamespace.php similarity index 100% rename from source/FluidXml/FluidNamespace.php rename to src/FluidXml/FluidNamespace.php diff --git a/source/FluidXml/FluidRepeater.php b/src/FluidXml/FluidRepeater.php similarity index 100% rename from source/FluidXml/FluidRepeater.php rename to src/FluidXml/FluidRepeater.php diff --git a/source/FluidXml/FluidSaveTrait.php b/src/FluidXml/FluidSaveTrait.php similarity index 100% rename from source/FluidXml/FluidSaveTrait.php rename to src/FluidXml/FluidSaveTrait.php diff --git a/source/FluidXml/FluidXml.php b/src/FluidXml/FluidXml.php similarity index 100% rename from source/FluidXml/FluidXml.php rename to src/FluidXml/FluidXml.php diff --git a/source/FluidXml/NewableTrait.php b/src/FluidXml/NewableTrait.php similarity index 100% rename from source/FluidXml/NewableTrait.php rename to src/FluidXml/NewableTrait.php diff --git a/source/FluidXml/ReservedCallStaticTrait.php b/src/FluidXml/ReservedCallStaticTrait.php similarity index 100% rename from source/FluidXml/ReservedCallStaticTrait.php rename to src/FluidXml/ReservedCallStaticTrait.php diff --git a/source/FluidXml/ReservedCallTrait.php b/src/FluidXml/ReservedCallTrait.php similarity index 100% rename from source/FluidXml/ReservedCallTrait.php rename to src/FluidXml/ReservedCallTrait.php diff --git a/source/FluidXml/fluid.php b/src/FluidXml/fluid.php similarity index 100% rename from source/FluidXml/fluid.php rename to src/FluidXml/fluid.php diff --git a/support/.common.sh b/support/.common.sh deleted file mode 100644 index ae739ff..0000000 --- a/support/.common.sh +++ /dev/null @@ -1,32 +0,0 @@ -set -o errexit -set -o nounset - -chkcmd() -{ - if command -v "$1" > /dev/null 2>&1; then - return 0 - fi - return 1 -} - -watch() -{ - if ! chkcmd 'fswatch'; then - echo ' error: "fswatch" command not found.' - exit 1 - fi - - fswatch --latency 0.1 --print0 "$@" -} - -dsstore_filter() -{ - while read -d '' e; do - local dsstore=$(echo "$e" | grep -o "\.DS_Store") - # We don't use the exit status, because an exit status different from 0 terminates the script. - # Checking the output should be better than setting set +o errexit and then set -o errexit. - if test "$dsstore" != '.DS_Store'; then - echo "$e\0" - fi - done -} diff --git a/support/Codevelox.php b/support/Codevelox.php deleted file mode 100644 index 76be811..0000000 --- a/support/Codevelox.php +++ /dev/null @@ -1,96 +0,0 @@ -data = $data; - - return $this; - } - - protected function time() - { - $microtime = \microtime(); - - [$usec, $sec] = \explode(' ', $microtime); - - return $sec . \substr($usec, 1); - } - - public function add($tag, Closure $task) - { - $this->tasks[$tag] = $task; - - return $this; - } - - public function run() - { - $results = []; - - foreach ($this->tasks as $g => $s) { - $start = $this->time(); - - for ($i = 0; $i < $this->cycles; ++$i) { - $s($this->data); - } - - $end = $this->time(); - - $elapsed = $end - $start; - - $results[$g] = $elapsed; - - yield $g => $elapsed; - } - - // PHP 5.6 doesn't support returning from a generator. - // PHP 7.0 does. - // return $results; - } - - public function message($index, $desc, $time) - { - $time = \sprintf('%.2f', $time); - return "Task {$index} took $time ($desc)\n"; - } - - public function run_and_show() - { - $queue = $this->run(); - - $n = 1; - foreach ($queue as $g => $i) { - echo $this->message($n, $g, $i); - ++$n; - } - - // $return = $queue->getReturn(); - // $this->show($return); - - return $this; - } - - public function show($results) - { - $n = 1; - foreach ($results as $g => $i) { - echo $this->message($n, $g, $i); - ++$n; - } - - return $this; - } -} diff --git a/support/coverage b/support/coverage deleted file mode 100755 index 38e8eb5..0000000 --- a/support/coverage +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -PATH="$PWD/sandbox/composer/bin:$PATH" - -if ! chkcmd 'peridot'; then - echo ' error: "peridot" command not found.' - echo ' Execute "./support/init" first.' - exit 1 -fi - -coverage_index="$PWD/sandbox/code-coverage-report/index.html" - -## It's is not created automatically. -mkdir -p "$(dirname "$coverage_index")" - -reporter=html-code-coverage - -if test $# -eq 1; then - reporter=$1 -fi - -peridot_arguments="-c ./support/peridot.php -r $reporter -g *.php ./specs/" - -if php -m | grep -i 'xdebug' > /dev/null; then - echo ' info: using Xdebug.' - set -- $peridot_arguments - peridot "$@" - -elif chkcmd 'phpdbg'; then - echo ' info: using phpdbg.' - set -- $peridot_arguments - phpdbg -e -rr "$(which peridot)" "$@" -else - echo ' error: no profiling tool found.' - exit 1 -fi - -if test -f "$coverage_index" && chkcmd 'open'; then - open "$coverage_index" -fi diff --git a/support/coveralls b/support/coveralls deleted file mode 100755 index 4af222b..0000000 --- a/support/coveralls +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -if ! chkcmd 'curl'; then - echo ' error: "curl" command not found.' - exit 1 -fi - -travis_job_id='' - -if test $# -eq 1; then - travis_job_id="$1" -fi - -coverage_data="$PWD/sandbox/code-coverage-report/code-coverage.php" -coverage_file="$(dirname "$coverage_data")/code-coverage.json" - -./support/coverage php-code-coverage - -php -f './support/coveralls.php' "$coverage_data" "$travis_job_id" > "$coverage_file" - -curl -v -F json_file="@$coverage_file" "https://coveralls.io/api/v1/jobs" diff --git a/support/coveralls.php b/support/coveralls.php deleted file mode 100644 index 49942af..0000000 --- a/support/coveralls.php +++ /dev/null @@ -1,61 +0,0 @@ -.php\n", \basename($argv[0])); - exit(1); -} - -$travis_job_id = ''; - -if (isset($argv[2])) { - $travis_job_id = $argv[2]; -} - -$data_file = $argv[1]; - -$DS = \DIRECTORY_SEPARATOR; -$root_dir = \realpath(__DIR__ . "{$DS}.."); - -require_once "{$root_dir}{$DS}sandbox{$DS}composer{$DS}autoload.php"; - -$data = require "$data_file"; - -$data = $data->getData(); - -$payload = [ 'service_name' => 'travis-ci', - 'service_job_id' => $travis_job_id, - 'repo_token' => 'c1DEnhEDEsdeHDUepRI24RibVJ6yDw2kN', - 'source_files' => [ ] ]; - -foreach ($data as $file => $c) { - $splfile = new \SplFileObject($file, 'r'); - $splfile->seek(PHP_INT_MAX); - $lines = $splfile->key(); - - $coverage = []; - for ($i = 0; $i < $lines; ++$i) { - // PHP Code Coverage starts from 1, - // Coveralls from 0. - $l = $i + 1; - - $val = 1; - - if (! isset($c[$l])) { - $val = null; - } else if (\is_array($c[$l]) && empty($c[$l])) { - $val = 0; - } - - $coverage[$i] = $val; - } - - $file = [ 'name' => \substr((string) $file, \strlen($root_dir) + 1), - 'source_digest' => \md5_file($file), - 'coverage' => $coverage ]; - - $payload['source_files'][] = $file; -} - -$data = \json_encode($payload, JSON_THROW_ON_ERROR); - -echo $data; diff --git a/support/gendoc b/support/gendoc deleted file mode 100755 index 5771fb1..0000000 --- a/support/gendoc +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -PATH="$(pwd)/sandbox/composer/bin:$PATH" - -if ! chkcmd 'apigen'; then - echo ' error: "apigen" command not found.' - echo ' Execute "./support/init" first.' - exit 1 -fi - -api_dir="doc/api" - -if test -d "$api_dir"; then - rm -rf "$api_dir" -fi - -genapi() { - apigen generate \ - --source "source" \ - --destination "$api_dir" \ - --template-theme bootstrap \ - --template-config "./sandbox/composer/apigen/theme-bootstrap/src/config.neon" \ - --title "FluidXML" \ - --todo \ - --tree \ - --debug -} - -doc_handler() -{ - genapi || true - - while read -d '' e; do - clear - genapi || true - done -} - -genapi - -echo "Open $api_dir/index.html" - -watch "source/" | dsstore_filter | doc_handler diff --git a/support/init b/support/init deleted file mode 100755 index e8002e2..0000000 --- a/support/init +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -if ! chkcmd 'composer'; then - echo ' error: "composer" command not found.' - exit 1 -fi - -mkdir -pv "sandbox" -mkdir -pv "sandbox/composer" - -composer install -d "." --no-interaction -rm -f "./composer.lock" diff --git a/support/peridot.php b/support/peridot.php deleted file mode 100644 index de53b4c..0000000 --- a/support/peridot.php +++ /dev/null @@ -1,29 +0,0 @@ -on('error', function ($errn, $msg, $file, $line) { - printf("$file:$line\n"); - printf(" $msg\n"); - }); - - // $eventEmitter->on('peridot.start', function (\Peridot\Console\Environment $environment) { - // $environment->getDefinition()->getArgument('path')->setDefault(__DIR__ . '/../specs'); - // }); - - (new CodeCoverageReporters($eventEmitter))->register(); - $eventEmitter->on('code-coverage.start', function (AbstractCodeCoverageReporter $reporter) { - $reporter->addDirectoryToWhitelist(__DIR__ . '/../source'); - // $reporter->addFilesToWhitelist([__DIR__ . '/../source/FluidXml.php']); - // $reporter->addDirectoryToWhitelist(__DIR__ . '/../source') - // ->addFilesToBlacklist([__DIR__ . '/../source/FluidXml.php56.php', - // __DIR__ . '/../source/FluidXml.php70.php']); - }); - - // $watcher = new WatcherPlugin($eventEmitter); - // $watcher->track(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'source'); -}; diff --git a/support/speedtest b/support/speedtest deleted file mode 100755 index 206d65b..0000000 --- a/support/speedtest +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -clear - -if ! chkcmd 'git'; then - echo ' error: "git" command not found.' - exit 1 -fi - -versions='ORIG_HEAD main' - -if test $# -gt 0; then - versions="$@" -fi - -test_dir="$(pwd)/sandbox/codevelox" -repo_dir="$test_dir/checkout" -test_php="$test_dir/speedtest.php" - -git_branch=$(git branch | grep "\*" | cut -d ' ' -f 2) - -ggit() -{ - git --work-tree "$repo_dir" "$@" -} - -mkdir -p "$repo_dir" -mkdir -p "$test_dir" - -cp -f './support/Codevelox.php' "$test_dir/" -cp -f './support/speedtest.php' "$test_dir/" - -for v in $versions; do - ggit checkout -f $v - - mkdir -p "$test_dir/$v" - - cp -rf "$repo_dir/source/"* "$test_dir/$v/" -done - -ggit checkout $git_branch - -cd "$test_dir" - -echo "\n Versions to test: $versions\n" - -for v in $versions; do - echo " => Testing version $v"; - - php speedtest.php "$v" -done diff --git a/support/speedtest.php b/support/speedtest.php deleted file mode 100644 index d1e9334..0000000 --- a/support/speedtest.php +++ /dev/null @@ -1,62 +0,0 @@ -add('fluidxml()', function($data) use ($fluidxml) { - $fluidxml(); -}); - -$machine->add('add()', function($data) use ($fluidxml) { - $xml = $fluidxml(); - for ($i = 0; $i < 10; ++$i) { - $xml->add('el'); - } -}); - -$machine->add('add(true)->add()', function($data) use ($fluidxml) { - $xml = $fluidxml(); - for ($i = 0; $i < 10; ++$i) { - $xml->add('el', true)->add('el'); - } -}); - -$machine->add('query()+add()', function($data) use ($fluidxml) { - $xml = $fluidxml(); - for ($i = 0; $i < 10; ++$i) { - $xml->query('/*')->add('el'); - } -}); - -$machine->add('add([...])', function($data) use ($fluidxml) { - $xml = $fluidxml(); - for ($i = 0; $i < 10; ++$i) { - $xml->add([ 'el' => [ 'el' => 'el' ] ]); - } -}); - -$xml = $fluidxml(['doc' => [ 'body' => [ 'div' ] ] ]); - -$machine->add('query(xpath)', function($data) use ($xml) { - $xml->query('//body/div'); -}); - -$machine->add('query(css)', function($data) use ($xml) { - $xml->query('body > div'); -}); - - -//////////////////////////////////////////////////////////////////////////////// - -$machine->run_and_show(); diff --git a/support/test b/support/test deleted file mode 100755 index 078288d..0000000 --- a/support/test +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -PATH="$(pwd)/sandbox/composer/bin:$PATH" - -if ! chkcmd 'peridot'; then - echo ' error: "peridot" command not found.' - echo ' Execute "./support/init" first.' - exit 1 -fi - -phpdbg= - -if (test $# -ge 1) && (test $1 = 'debug') && chkcmd 'phpdbg'; then - phpdbg="phpdbg -e" -fi - -$phpdbg "$(which peridot)" -c "./support/peridot.php" -g "*.php" "./specs/" diff --git a/support/testd b/support/testd deleted file mode 100755 index d1c1713..0000000 --- a/support/testd +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -tester="./support/testv" - -test_handler() -{ - "$tester" || true - - while read -d '' e; do - ## test -f skips a file descriptor written by vim. - test -f "$e" && "$tester" || true - done -} - -watch "specs/" "source/" | dsstore_filter | test_handler diff --git a/support/testdbg b/support/testdbg deleted file mode 100755 index ca951a1..0000000 --- a/support/testdbg +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" - -./test debug diff --git a/support/testv b/support/testv deleted file mode 100755 index d4adf55..0000000 --- a/support/testv +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")" -. "./.common.sh" -cd .. - -tester="./support/test" - -if ! chkcmd 'brew'; then - echo ' "brew" command not found.' - echo ' Skipping version based testing for PHP {8.1, 8.2}.' - - "$tester" -else - brew unlink php@8.1 > /dev/null \ - && brew link php@8.1 > /dev/null \ - && printf "\nTesting against PHP 8.1\n" \ - && "$tester" \ - && clear \ - && brew unlink php@8.1 > /dev/null \ - && brew link php@8.2 > /dev/null \ - && printf "\nTesting against PHP 8.2\n" \ - && "$tester" -fi diff --git a/tests/CssTranslatorTest.php b/tests/CssTranslatorTest.php new file mode 100644 index 0000000..11e5b63 --- /dev/null +++ b/tests/CssTranslatorTest.php @@ -0,0 +1,143 @@ +hml = new FluidXml(['html' => [ + 'body' => [ + 'div' => [ + ['p' => ['@class' => 'a', '@id' => '123', ['span']]], + ['h1' => ['@class' => 'b']], + ['shape' => ['@class' => 'c']], + ['p' => ['@class' => 'a b']], + ['p' => ['@class' => 'a']], + ['span' => ['@class' => 'b']], + ], + ], + ]]); + + $this->hml->namespace('svg', 'http://svg.org'); + $this->hml->query('//body') + ->add('svg:svg', true) + ->add('svg:shape') + ->add('svg:shape'); + } + + // ------------------------------------------------------------------------- + // .xpath() + // ------------------------------------------------------------------------- + + public function testXpathSupportsCssSelectorA(): void + { + self::assertSame($this->hml->query('//p')->array(), $this->hml->query('p')->array()); + self::assertSame(3, $this->hml->query('p')->size()); + } + + public function testXpathSupportsCssSelectorNsA(): void + { + self::assertSame($this->hml->query('//svg:shape')->array(), $this->hml->query('svg|shape')->array()); + self::assertSame(2, $this->hml->query('svg|shape')->size()); + } + + public function testXpathSupportsCssSelectorWildcardNsA(): void + { + self::assertSame( + $this->hml->query('[local-name() = "shape"]')->array(), + $this->hml->query('*|shape')->array() + ); + self::assertSame(3, $this->hml->query('*|shape')->size()); + } + + public function testXpathSupportsCssSelectorRoot(): void + { + self::assertSame($this->hml->query('/*')->array(), $this->hml->query(':root')->array()); + self::assertSame(1, $this->hml->query(':root')->size()); + } + + public function testXpathSupportsCssSelectorId(): void + { + self::assertSame($this->hml->query('//*[@id="123"]')->array(), $this->hml->query('#123')->array()); + self::assertSame(1, $this->hml->query('#123')->size()); + } + + public function testXpathSupportsCssSelectorClassClass(): void + { + self::assertSame($this->hml->query('//p')->array(), $this->hml->query('.a')->array()); + self::assertSame(3, $this->hml->query('.a')->size()); + + self::assertSame($this->hml->query('//p[2]')->array(), $this->hml->query('.a.b')->array()); + self::assertSame(1, $this->hml->query('.a.b')->size()); + + self::assertSame($this->hml->query('//h1')->array(), $this->hml->query('h1.b')->array()); + self::assertSame(1, $this->hml->query('h1.b')->size()); + } + + public function testXpathSupportsCssSelectorAttr(): void + { + self::assertSame($this->hml->query('//div/*')->array(), $this->hml->query('[class]')->array()); + self::assertSame(6, $this->hml->query('[class]')->size()); + + self::assertSame($this->hml->query('//*[@id]')->array(), $this->hml->query('[id]')->array()); + self::assertSame(1, $this->hml->query('[id]')->size()); + } + + public function testXpathSupportsCssSelectorAttrEqVal(): void + { + self::assertSame($this->hml->query('//p[@id]')->array(), $this->hml->query('p[id="123"]')->array()); + self::assertSame(1, $this->hml->query('p[id="123"]')->size()); + + self::assertSame($this->hml->query('//*[@class="a"]')->array(), $this->hml->query('[class="a"]')->array()); + self::assertSame(2, $this->hml->query('[class="a"]')->size()); + } + + public function testXpathSupportsCssSelectorDescendant(): void + { + self::assertSame($this->hml->query('//div//span')->array(), $this->hml->query('div span')->array()); + self::assertSame(2, $this->hml->query('div span')->size()); + } + + public function testXpathSupportsCssSelectorDirectChild(): void + { + self::assertSame($this->hml->query('//div/p')->array(), $this->hml->query('div > p')->array()); + self::assertSame(3, $this->hml->query('div > p')->size()); + } + + public function testXpathSupportsCssSelectorUnion(): void + { + self::assertSame($this->hml->query('//p|//div')->array(), $this->hml->query('p, div')->array()); + self::assertSame(4, $this->hml->query('p, div')->size()); + } + + public function testXpathSupportsCssSelectorAdjacentSibling(): void + { + self::assertSame($this->hml->query('//p[3]')->array(), $this->hml->query('p + p')->array()); + self::assertSame(1, $this->hml->query('p + p')->size()); + } + + public function testXpathSupportsCssSelectorGeneralSibling(): void + { + self::assertSame($this->hml->query('//p[2]|//p[3]')->array(), $this->hml->query('h1 ~ p')->array()); + self::assertSame(2, $this->hml->query('h1 ~ p')->size()); + } + + public function testXpathSupportsMixingCssSelectors(): void + { + $expected = $this->hml->query('//p/span|//div|//*[@class="a"]|//*[@class="a b"]')->array(); + $actual = $this->hml->query(':root #123 span, div, :root .a')->array(); + + self::assertSame($expected, $actual); + self::assertSame(5, $this->hml->query(':root #123 span, div, :root .a')->size()); + } +} diff --git a/tests/FluidContextTest.php b/tests/FluidContextTest.php new file mode 100644 index 0000000..f73e90b --- /dev/null +++ b/tests/FluidContextTest.php @@ -0,0 +1,235 @@ +addChild(['head', 'body'], true); + + self::assertInstanceOf(\Iterator::class, $cx); + + $representation = []; + foreach ($cx as $k => $v) { + self::assertTrue(is_int($k)); + self::assertInstanceOf(\DOMNode::class, $v); + $representation[$k] = $v->nodeName; + } + + self::assertSame([0 => 'head', 1 => 'body'], $representation); + } + + // ------------------------------------------------------------------------- + // .__construct() + // ------------------------------------------------------------------------- + + public function testConstructAcceptsDomDocument(): void + { + $xml = new FluidXml(); + + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $newCx = new FluidContext($doc, $handler, $xml->dom()); + + self::assertSame($xml->dom(), $newCx[0]); + } + + public function testConstructAcceptsDomNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild(['head'], true); + + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $newCx = new FluidContext($doc, $handler, $cx[0]); + + self::assertSame($cx->array(), $newCx->array()); + } + + public function testConstructAcceptsArrayOfDomNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild(['head', 'body'], true); + + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $newCx = new FluidContext($doc, $handler, $cx->array()); + + self::assertSame($cx->array(), $newCx->array()); + } + + public function testConstructAcceptsDomNodeList(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild(['head', 'body'], true); + $dom = $xml->dom(); + + $domxp = new \DOMXPath($dom); + $nodes = $domxp->query('/doc/*'); + + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $newCx = new FluidContext($doc, $handler, $nodes); + + self::assertSame($cx->array(), $newCx->array()); + } + + public function testConstructAcceptsFluidContext(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild(['head', 'body'], true); + + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $newCx = new FluidContext($doc, $handler, $cx); + + self::assertSame($cx->array(), $newCx->array()); + } + + public function testConstructThrowsForNotSupportedDocument(): void + { + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + + try { + new FluidContext($doc, $handler, 'node'); + } catch (\Exception $e) { + $actual = $e; + } + + self::assertInstanceOf(\Exception::class, $actual); + } + + // ------------------------------------------------------------------------- + // [] + // ------------------------------------------------------------------------- + + public function testArrayAccessAccessesNodesInsideContext(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild(['head', 'body'], true); + + self::assertInstanceOf(\DOMElement::class, $cx[0]); + self::assertInstanceOf(\DOMElement::class, $cx[1]); + } + + public function testArrayAccessBehavesLikeArray(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild(['head', 'body', 'extra'], true); + + self::assertTrue(isset($cx[0])); + self::assertFalse(isset($cx[3])); + self::assertNull($cx[3]); + + try { + $cx[] = "value"; + } catch (\Exception $e) { + $actual = $e; + } + self::assertInstanceOf(\Exception::class, $actual); + + unset($cx[1]); + + self::assertSame('head', $cx[0]->nodeName); + self::assertSame('extra', $cx[1]->nodeName); + } + + // ------------------------------------------------------------------------- + // .array() + // ------------------------------------------------------------------------- + + public function testArrayReturnsArrayOfNodesInsideContext(): void + { + $xml = new FluidXml(null); + + $a = $xml->array(); + + self::assertTrue(is_array($a)); + self::assertCount(1, $a); + self::assertSame([$xml->dom()], $a); + + $xml = new FluidXml(); + + $a = $xml->array(); + + self::assertTrue(is_array($a)); + self::assertCount(1, $a); + self::assertSame([$xml->dom()->documentElement], $a); + + $cx = $xml->addChild(['head', 'body'], true); + + $a = $cx->array(); + + self::assertTrue(is_array($a)); + self::assertCount(2, $a); + } + + // ------------------------------------------------------------------------- + // .length() + // ------------------------------------------------------------------------- + + public function testLengthReturnsNumberOfNodesInsideContext(): void + { + $xml = new FluidXml(); + $cx = $xml->query('/*'); + + self::assertSame(1, $xml->length()); + self::assertSame(1, $cx->length()); + + $cx = $xml->addChild(['child1', 'child2'], true); + self::assertSame(2, $cx->length()); + + $cx = $cx->addChild(['subchild1', 'subchild2', 'subchild3']); + self::assertSame(2, $cx->length()); + + $cx = $cx->addChild(['subchild4', 'subchild5', 'subchild6', 'subchild7'], true); + self::assertSame(8, $cx->length()); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .size() + // ------------------------------------------------------------------------- + + public function testSizeBehavesLikeLength(): void + { + $xml = new FluidXml(); + + self::assertSame($xml->length(), $xml->size()); + + $cx = $xml->addChild('parent', true)->addChild(['child1', 'child2']); + + self::assertSame($cx->length(), $cx->size()); + } +} diff --git a/tests/FluidHelperTest.php b/tests/FluidHelperTest.php new file mode 100644 index 0000000..bbd1de5 --- /dev/null +++ b/tests/FluidHelperTest.php @@ -0,0 +1,86 @@ +xml())); + self::assertTrue(FluidHelper::isAnXmlString(" \n \n \t" . $xml->xml())); + self::assertFalse(FluidHelper::isAnXmlString('item')); + } + + // ------------------------------------------------------------------------- + // :domdocumentToHtml() + // ------------------------------------------------------------------------- + + public function testDomdocumentToHtmlConvertsToHtmlStringWithoutRespectingVoidTags(): void + { + // Exercises the condition branch for code coverage. + FluidHelper::domdocumentToHtml((new FluidXml())->dom(), true); + + $this->addToAssertionCount(1); + } + + // ------------------------------------------------------------------------- + // :domdocumentToStringWithoutHeaders() + // ------------------------------------------------------------------------- + + public function testDomdocumentToStringWithoutHeadersConvertsToXmlStringWithoutHeaders(): void + { + $xml = new FluidXml(); + + self::assertSame('', FluidHelper::domdocumentToStringWithoutHeaders($xml->dom())); + + $xml = new FluidXml('doc', ['stylesheet' => 'x.com/style.xsl']); + + self::assertSame('', FluidHelper::domdocumentToStringWithoutHeaders($xml->dom())); + } + + // ------------------------------------------------------------------------- + // :domnodelistToString() + // ------------------------------------------------------------------------- + + public function testDomnodelistToStringConvertsDomNodeListToXmlString(): void + { + $xml = new FluidXml(); + $nodes = $xml->dom()->childNodes; + + self::assertSame('', FluidHelper::domnodelistToString($nodes)); + } + + // ------------------------------------------------------------------------- + // :domnodesToString() + // ------------------------------------------------------------------------- + + public function testDomnodesToStringConvertsArrayOfDomNodesToXmlString(): void + { + $xml = new FluidXml(); + $nodes = [$xml->dom()->documentElement]; + + self::assertSame('', FluidHelper::domnodesToString($nodes)); + } + + // ------------------------------------------------------------------------- + // :simplexmlToStringWithoutHeaders() + // ------------------------------------------------------------------------- + + public function testSimplexmlToStringWithoutHeadersConvertsSimpleXmlElementToXmlStringWithoutHeaders(): void + { + $xml = simplexml_import_dom((new FluidXml())->dom()); + + self::assertSame('', FluidHelper::simplexmlToStringWithoutHeaders($xml)); + } +} diff --git a/tests/FluidNamespaceTest.php b/tests/FluidNamespaceTest.php new file mode 100644 index 0000000..f595f3e --- /dev/null +++ b/tests/FluidNamespaceTest.php @@ -0,0 +1,90 @@ +id()); + self::assertSame($nsUri, $ns->uri()); + self::assertSame($nsMode, $ns->mode()); + + $nsMode = FluidNamespace::MODE_IMPLICIT; + $ns = new FluidNamespace($nsId, $nsUri, $nsMode); + + self::assertSame($nsMode, $ns->mode()); + } + + // ------------------------------------------------------------------------- + // .id() + // ------------------------------------------------------------------------- + + public function testIdReturnsNamespaceId(): void + { + $nsId = 'x'; + $ns = new FluidNamespace($nsId, 'x.com'); + + self::assertSame($nsId, $ns->id()); + } + + // ------------------------------------------------------------------------- + // .uri() + // ------------------------------------------------------------------------- + + public function testUriReturnsNamespaceUri(): void + { + $nsUri = 'x.com'; + $ns = new FluidNamespace('x', $nsUri); + + self::assertSame($nsUri, $ns->uri()); + } + + // ------------------------------------------------------------------------- + // .mode() + // ------------------------------------------------------------------------- + + public function testModeReturnsNamespaceMode(): void + { + $nsId = 'x'; + $nsUri = 'x.com'; + $ns = new FluidNamespace($nsId, $nsUri); + + self::assertSame(FluidNamespace::MODE_EXPLICIT, $ns->mode()); + + $nsMode = FluidNamespace::MODE_IMPLICIT; + $ns = new FluidNamespace($nsId, $nsUri, $nsMode); + + self::assertSame($nsMode, $ns->mode()); + } + + // ------------------------------------------------------------------------- + // .querify() / __invoke + // ------------------------------------------------------------------------- + + public function testQuerifyFormatsXpathQueryToUseNamespaceId(): void + { + $ns = new FluidNamespace('x', 'x.com'); + + self::assertSame('x:current/x:child', $ns('current/child')); + self::assertSame('//x:current/x:child', $ns('//current/child')); + + $ns = new FluidNamespace('x', 'x.com', FluidNamespace::MODE_IMPLICIT); + + self::assertSame('x:current/x:child', $ns('current/child')); + self::assertSame('//x:current/x:child', $ns('//current/child')); + } +} diff --git a/tests/FluidTestCase.php b/tests/FluidTestCase.php new file mode 100644 index 0000000..f05df3f --- /dev/null +++ b/tests/FluidTestCase.php @@ -0,0 +1,33 @@ +outDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR; + } + + protected function assertEqualXml(mixed $xml, string $expected): void + { + $header = "\n"; + self::assertSame(trim($header . $expected), trim($xml->xml())); + } + + protected function assertIsFluid(string $method, mixed ...$args): void + { + $instance = new \FluidXml\FluidXml(); + if (method_exists($instance, $method)) { + self::assertInstanceOf(\FluidXml\FluidInterface::class, call_user_func([$instance, $method], ...$args)); + } + $instance = $instance->query('/*'); + if (method_exists($instance, $method)) { + self::assertInstanceOf(\FluidXml\FluidInterface::class, call_user_func([$instance, $method], ...$args)); + } + } +} diff --git a/tests/FluidXmlTest.php b/tests/FluidXmlTest.php new file mode 100644 index 0000000..f65cc9a --- /dev/null +++ b/tests/FluidXmlTest.php @@ -0,0 +1,2737 @@ +xml(), $alias->xml()); + + $options = [ + 'root' => 'root', + 'version' => '1.2', + 'encoding' => 'UTF-16', + 'stylesheet' => 'stylesheet.xsl', + ]; + + $xml = new FluidXml(null, $options); + $alias = fluidxml(null, $options); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // fluidify() + // ------------------------------------------------------------------------- + + public function testFluidifyBehavesLikeLoad(): void + { + $file = $this->outDir . '.test_fluidify.xml'; + $doc = "\n" + . " content\n" + . ""; + + file_put_contents($file, $doc); + $xml = FluidXml::load($file); + $alias = fluidify($file); + unlink($file); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // fluidns() + // ------------------------------------------------------------------------- + + public function testFluidnsBehavesLikeNamespaceConstructor(): void + { + $ns = new FluidNamespace('x', 'x.com'); + $alias = fluidns('x', 'x.com'); + + self::assertSame($ns->id(), $alias->id()); + self::assertSame($ns->uri(), $alias->uri()); + self::assertSame($ns->mode(), $alias->mode()); + + $ns = new FluidNamespace('x', 'x.com', FluidNamespace::MODE_IMPLICIT); + $alias = fluidns('x', 'x.com', FluidNamespace::MODE_IMPLICIT); + + self::assertSame($ns->id(), $alias->id()); + self::assertSame($ns->uri(), $alias->uri()); + self::assertSame($ns->mode(), $alias->mode()); + } + + // ------------------------------------------------------------------------- + // FluidXml (top-level) + // ------------------------------------------------------------------------- + + public function testThrowsInvokingNotExistingStaticMethod(): void + { + try { + FluidXml::lload(); + } catch (\Exception $e) { + $actual = $e; + } + + self::assertInstanceOf(\Exception::class, $actual); + } + + // ------------------------------------------------------------------------- + // :load() + // ------------------------------------------------------------------------- + + public function testLoadImportsXmlFile(): void + { + $file = $this->outDir . '.test_load.xml'; + $doc = "\n" + . " content\n" + . ""; + + file_put_contents($file, $doc); + $xml = FluidXml::load($file); + unlink($file); + + $this->assertEqualXml($xml, $doc); + } + + public function testLoadThrowsForNotExistingFile(): void + { + set_error_handler(function () {}); + try { + $xml = FluidXml::load('.impossible.xml'); + } catch (\Exception $e) { + $actual = $e; + } finally { + restore_error_handler(); + } + + self::assertInstanceOf(\Exception::class, $actual); + } + + public function testLoadImportsXmlFileContainingDoctype(): void + { + $file = $this->outDir . '.test_load_doctype.xml'; + $doc = "\n" + . "\n" + . ""; + + file_put_contents($file, $doc); + $xml = FluidXml::load($file); + unlink($file); + + $expected = "\n \n"; + $this->assertEqualXml($xml, $expected); + } + + public function testLoadAcceptsLibxmlFlags(): void + { + $file = $this->outDir . '.test_load_flags.xml'; + $doc = "\n" + . " value\n" + . ""; + + file_put_contents($file, $doc); + $xml = FluidXml::load($file, \LIBXML_PARSEHUGE); + unlink($file); + + $this->assertEqualXml($xml, $doc); + } + + // ------------------------------------------------------------------------- + // .addChild() with DOCTYPE + // ------------------------------------------------------------------------- + + public function testAddChildDoesNotThrowWithDoctypeXmlString(): void + { + $xml = new FluidXml(null); + $xmlStr = "\n" + . "\n" + . ""; + + $xml->addChild($xmlStr); + + $expected = "\n \n"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // :new() + // ------------------------------------------------------------------------- + + public function testNewBehavesLikeConstructor(): void + { + $xml = new FluidXml(); + eval('$alias = \FluidXml\FluidXml::new();'); + + self::assertSame($xml->xml(), $alias->xml()); + + $options = [ + 'root' => 'root', + 'version' => '1.2', + 'encoding' => 'UTF-16', + 'stylesheet' => 'stylesheet.xsl', + ]; + + $xml = new FluidXml($options); + eval('$alias = \FluidXml\FluidXml::new($options);'); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .__construct() + // ------------------------------------------------------------------------- + + public function testConstructCreatesUtf8Xml10DocumentWithDefaultRoot(): void + { + $xml = new FluidXml(); + + $this->assertEqualXml($xml, ""); + } + + public function testConstructCreatesDocumentWithCustomRootAsFirstOrSecondArgument(): void + { + $xml = new FluidXml('document'); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(null, ['root' => 'document']); + $this->assertEqualXml($xml, $expected); + } + + public function testConstructCreatesDocumentWithNoRoot(): void + { + $xml = new FluidXml(null); + $expected = ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(null, ['root' => null]); + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml('doc', ['root' => null]); + $this->assertEqualXml($xml, $expected); + } + + public function testConstructCreatesDocumentWithStylesheetAndRoot(): void + { + $stylesheet = ""; + + $xml = new FluidXml('doc', ['stylesheet' => 'http://servo-php.org/fluidxml']); + + $expected = $stylesheet . "\n" . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testConstructCreatesDocumentWithStylesheetAndNoRoot(): void + { + $stylesheet = ""; + + $xml = new FluidXml(null, ['stylesheet' => 'http://servo-php.org/fluidxml']); + + $expected = $stylesheet; + $this->assertEqualXml($xml, $expected); + } + + public function testConstructImportsXmlString(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + $exp = $dom->saveXML(); + + $xml = new FluidXml("\n " . $exp); + $this->assertEqualXml($xml, $doc); + + $xml = new FluidXml("\n " . substr($exp, strpos($exp, "\n") + 1)); + $this->assertEqualXml($xml, $doc); + } + + public function testConstructImportsArrayWithAtSyntax(): void + { + $xml = new FluidXml(['root' => [ + 'child1' => ['@id' => 1], + 'child2' => 'Text 2', + ]]); + + $expected = "\n" + . " \n" + . " Text 2\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testConstructImportsDomDocument(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $xml = new FluidXml($dom); + $this->assertEqualXml($xml, $doc); + } + + public function testConstructImportsDomNode(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $domxp = new \DOMXPath($dom); + $nodes = $domxp->query('/root/parent'); + $xml = new FluidXml($nodes[0]); + + $this->assertEqualXml($xml, "content"); + } + + public function testConstructImportsDomNodeList(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $domxp = new \DOMXPath($dom); + $nodes = $domxp->query('/root/parent'); + $xml = new FluidXml($nodes); + + $this->assertEqualXml($xml, "content"); + } + + public function testConstructImportsSimpleXmlElement(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $xml = new FluidXml(simplexml_import_dom($dom)); + $this->assertEqualXml($xml, $doc); + } + + public function testConstructImportsFluidXml(): void + { + $doc = "\n" + . " content\n" + . ""; + + $xml = new FluidXml(new FluidXml($doc)); + $this->assertEqualXml($xml, $doc); + } + + public function testConstructImportsFluidContext(): void + { + $doc = "\n" + . " content\n" + . ""; + + $cx = (new FluidXml($doc))->query('/root'); + $xml = new FluidXml($cx); + + $this->assertEqualXml($xml, $doc); + } + + public function testConstructThrowsForNotSupportedDocuments(): void + { + try { + $xml = new FluidXml(1); + } catch (\Exception $e) { + $actual = $e; + } + + self::assertInstanceOf(\Exception::class, $actual); + } + + public function testConstructThrowsInvokingNotExistingMethod(): void + { + $xml = new FluidXml(); + try { + $xml->qquery(); + } catch (\Exception $e) { + $actual = $e; + } + + self::assertInstanceOf(\Exception::class, $actual); + } + + // ------------------------------------------------------------------------- + // .namespace() + // ------------------------------------------------------------------------- + + public function testNamespaceIsFluid(): void + { + $xml = new FluidXml(); + self::assertInstanceOf(\FluidXml\FluidInterface::class, $xml->namespace('a', 'b')); + } + + public function testNamespaceAcceptsNamespace(): void + { + $xml = new FluidXml(); + $xNs = new FluidNamespace('x', 'x.com'); + $xxNs = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); + $nss = $xml->namespace($xNs) + ->namespace($xxNs) + ->namespaces(); + + self::assertSame($xNs, $nss[$xNs->id()]); + self::assertSame($xxNs, $nss[$xxNs->id()]); + } + + public function testNamespaceAcceptsIdUriAndOptionalMode(): void + { + $xml = new FluidXml(); + + $nss = $xml->namespace('x', 'x.com') + ->namespace('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT) + ->namespaces(); + + self::assertSame('x.com', $nss['x']->uri()); + self::assertSame(FluidNamespace::MODE_EXPLICIT, $nss['x']->mode()); + self::assertSame('xx.com', $nss['xx']->uri()); + self::assertSame(FluidNamespace::MODE_IMPLICIT, $nss['xx']->mode()); + } + + public function testNamespaceAcceptsVariableNamespacesArguments(): void + { + $xml = new FluidXml(); + $xNs = new FluidNamespace('x', 'x.com'); + $xxNs = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); + + $nss = $xml->namespace($xNs, $xxNs)->namespaces(); + + self::assertSame($xNs, $nss[$xNs->id()]); + self::assertSame($xxNs, $nss[$xxNs->id()]); + } + + // ------------------------------------------------------------------------- + // .query() + // ------------------------------------------------------------------------- + + public function testQueryIsFluid(): void + { + $this->assertIsFluid('query', '.'); + } + + public function testQueryAcceptsQueryReturningRootNodesXpath(): void + { + $xml = new FluidXml(); + $cx = $xml->query('/*'); + + self::assertSame('doc', $cx[0]->nodeName); + + $xml->appendSibling('meta'); + $cx = $xml->query('/*'); + + self::assertSame('doc', $cx[0]->nodeName); + self::assertSame('meta', $cx[1]->nodeName); + } + + public function testQueryAcceptsQueryReturningRootNodesCss(): void + { + $xml = new FluidXml(); + $cx = $xml->query(':root'); + + self::assertSame('doc', $cx[0]->nodeName); + + $xml->appendSibling('meta'); + $cx = $xml->query(':root'); + + self::assertSame('doc', $cx[0]->nodeName); + self::assertSame('meta', $cx[1]->nodeName); + } + + public function testQueryAcceptsArrayOfQueriesXpath(): void + { + $xml = new FluidXml(); + $xml->addChild('html', true) + ->addChild(['head', 'body']) + ->query(['//html', 'head', '//body']) + ->setAttribute('lang', 'en'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testQueryAcceptsArrayOfQueriesXpathAndCss(): void + { + $xml = new FluidXml(); + $xml->addChild('html', true) + ->addChild(['head', 'body']) + ->query(['//html', 'head', '//body']) + ->setAttribute('lang', 'en'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testQueryAcceptsVariableNumberOfQueriesXpathAndCss(): void + { + $xml = new FluidXml(); + $xml->addChild('html', true) + ->addChild(['head', 'body']) + ->query('//html', 'head', '//body') + ->setAttribute('lang', 'en'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testQuerySupportsRelativeQueriesXpath(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('html', true) + ->addChild(['head', 'body']) + ->query('./body'); + + self::assertSame('body', $cx[0]->nodeName); + + $xml = new FluidXml(); + $xml->addChild('html', true)->addChild(['head', 'body']); + $cx = $xml->query('/doc/html')->query('./head'); + + self::assertSame('head', $cx[0]->nodeName); + } + + public function testQueryQueriesRootFromSubQueryXpath(): void + { + $xml = new FluidXml(); + $xml->addChild('html', true)->addChild(['head', 'body']); + $cx = $xml->query('/doc/html/body') + ->addChild('h1') + ->query('/doc/html/head'); + + self::assertSame('head', $cx[0]->nodeName); + } + + public function testQueryQueriesRootFromSubQueryCss(): void + { + $xml = new FluidXml(); + $xml->addChild('html', true)->addChild(['head', 'body']); + $cx = $xml->query('body') + ->addChild('h1') + ->query(':root head'); + + self::assertSame('head', $cx[0]->nodeName); + } + + public function testQueryPerformsRelativeQueriesAscendingDomTree(): void + { + $xml = new FluidXml(); + $xml->addChild('html', true) + ->addChild(['head', 'body'], true) + ->query('../body') + ->addChild('h1') + ->query('../..') + ->addChild('extra'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . "

\n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testQueryQueriesNamespacedNodesXpath(): void + { + $xml = new FluidXml(); + $xNs = new FluidNamespace('x', 'x.com'); + $xxNs = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); + + $xml->namespace($xNs, $xxNs); + + $xml->addChild('x:a', true) + ->addChild('x:b', true) + ->addChild('xx:c', true) + ->addChild('xx:d', true) + ->addChild('e', true) + ->addChild('x:f', true) + ->addChild('g'); + + $r = $xml->query('/doc/a'); + self::assertSame(0, $r->length()); + + $r = $xml->query('/doc/x:a'); + self::assertSame('x:a', $r[0]->nodeName); + + $r = $xml->query('/doc/x:a/x:b'); + self::assertSame('x:b', $r[0]->nodeName); + + $r = $xml->query('/doc/x:a/x:b/c'); + self::assertSame(0, $r->length()); + + $r = $xml->query('/doc/x:a/x:b/xx:c'); + self::assertSame('c', $r[0]->nodeName); + + $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d'); + self::assertSame('d', $r[0]->nodeName); + + $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e'); + self::assertSame('e', $r[0]->nodeName); + + $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e/f'); + self::assertSame(0, $r->length()); + + $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e/x:f'); + self::assertSame('x:f', $r[0]->nodeName); + + $r = $xml->query('/doc/x:a/x:b/xx:c/xx:d/e/x:f/g'); + self::assertSame('g', $r[0]->nodeName); + } + + public function testQueryQueriesNamespacedNodesCss(): void + { + $xml = new FluidXml(); + $xNs = new FluidNamespace('x', 'x.com'); + $xxNs = fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT); + + $xml->namespace($xNs, $xxNs); + + $xml->addChild('x:a', true) + ->addChild('x:b', true) + ->addChild('xx:c', true) + ->addChild('xx:d', true) + ->addChild('e', true) + ->addChild('x:f', true) + ->addChild('g'); + + $r = $xml->query('a'); + self::assertSame(0, $r->length()); + + $r = $xml->query('x|a'); + self::assertSame('x:a', $r[0]->nodeName); + + $r = $xml->query('x|a > x|b'); + self::assertSame('x:b', $r[0]->nodeName); + + $r = $xml->query('x|a > x|b > c'); + self::assertSame(0, $r->length()); + + $r = $xml->query('x|a > x|b > xx|c'); + self::assertSame('c', $r[0]->nodeName); + + $r = $xml->query('x|a > x|b > xx|c > xx|d'); + self::assertSame('d', $r[0]->nodeName); + + $r = $xml->query('x|a > x|b > xx|c > xx|d > e'); + self::assertSame('e', $r[0]->nodeName); + + $r = $xml->query('x|a > x|b > xx|c > xx|d > e > f'); + self::assertSame(0, $r->length()); + + $r = $xml->query('x|a > x|b > xx|c > xx|d > e > x|f'); + self::assertSame('x:f', $r[0]->nodeName); + + $r = $xml->query('x|a > x|b > xx|c > xx|d > e > x|f > g'); + self::assertSame('g', $r[0]->nodeName); + } + + // ------------------------------------------------------------------------- + // .__invoke() + // ------------------------------------------------------------------------- + + public function testInvokeIsFluid(): void + { + $this->assertIsFluid('__invoke', '/*'); + } + + public function testInvokeBehavesLikeQuery(): void + { + $xml = new FluidXml(); + + $actual = $xml('/*'); + $expected = $xml->query('/*'); + self::assertEquals($expected, $actual); + } + + // ------------------------------------------------------------------------- + // .each() + // ------------------------------------------------------------------------- + + public function testEachIsFluid(): void + { + $this->assertIsFluid('each', function () {}); + } + + public function testEachIteratesNodesInsideContext(): void + { + $xml = new FluidXml(); + + $xml->each(function ($i, $n) { + self::assertInstanceOf(FluidContext::class, $this); + self::assertInstanceOf(\DOMNode::class, $n); + self::assertSame(0, $i); + }); + + $xml->each('eachassert_FluidXmlTest'); + + $xml->addChild('child1')->addChild('child2'); + + $nodes = []; + $index = 0; + $xml->query('/doc/*') + ->each(function ($i, $n) use (&$nodes, &$index) { + $idx = $i + 1; + $this->setText($n->nodeName . $idx); + $nodes[] = $n; + self::assertSame($index, $i); + ++$index; + }); + + self::assertSame($xml->query('/doc/*')->array(), $nodes); + + $expected = "\n" + . " child11\n" + . " child22\n" + . ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('child1')->addChild('child2'); + + $xml->query('/doc/*')->each('eachsettext_FluidXmlTest'); + + $expected = "\n" + . " child11\n" + . " child22\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .map() + // ------------------------------------------------------------------------- + + public function testMapMapsOverNodesInsideContext(): void + { + $xml = new FluidXml(); + + $xml->map(function ($i, $n) { + self::assertInstanceOf(FluidContext::class, $this); + self::assertInstanceOf(\DOMNode::class, $n); + self::assertSame(0, $i); + }); + + $xml->map('mapassert_FluidXmlTest'); + + $xml->addChild(['child1' => 'child1'])->addChild(['child2' => 'child2']); + + $actual = $xml->query('/doc/*') + ->map(function ($i, $n) { + $idx = $i + 1; + return $n->nodeValue . $idx; + }); + + $expected = ['child11', 'child22']; + self::assertSame($expected, $actual); + + $actual = $xml->query('/doc/*')->map('mapfn_FluidXmlTest'); + self::assertSame($expected, $actual); + } + + // ------------------------------------------------------------------------- + // .filter() + // ------------------------------------------------------------------------- + + public function testFilterIsFluid(): void + { + $this->assertIsFluid('filter', function () {}); + } + + public function testFilterFiltersNodesInsideContext(): void + { + $xml = new FluidXml(); + + $xml->filter(function ($i, $n) { + self::assertInstanceOf(FluidContext::class, $this); + self::assertInstanceOf(\DOMNode::class, $n); + self::assertSame(0, $i); + }); + + $xml->each('filterassert_FluidXmlTest'); + $xml->times(4)->addChild('child'); + + $index = 0; + $children = $xml->query('//child'); + + $cx = $children->filter(function ($i, $n) use (&$index) { + self::assertSame($index, $i); + ++$index; + + if ($i === 0) { + return true; + } + + if (($i % 2) === 0) { + return false; + } + }); + + self::assertSame([$children[0], $children[1], $children[3]], $cx->array()); + + $cx->setText('not filtered'); + + $expected = "\n" + . " not filtered\n" + . " not filtered\n" + . " \n" + . " not filtered\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .times() + // ------------------------------------------------------------------------- + + public function testTimesIsFluid(): void + { + self::assertInstanceOf(FluidRepeater::class, (new FluidXml())->times(4)); + $this->assertIsFluid('times', 4, function () {}); + } + + public function testTimesRepeatsFollowingMethodCall(): void + { + $xml = new FluidXml(); + + $xml->times(2) + ->add('child') + ->add('lastchild'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testTimesSwitchesContext(): void + { + $xml = new FluidXml(); + + $xml->times(2) + ->add('child', true) + ->add('subchild'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testTimesRepeatsClosureBoundToContext(): void + { + $xml = new FluidXml(); + + $xml->add('parent', true) + ->times(2, function ($i) { + $this->add("child{$i}"); + }); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testTimesRepeatsCallable(): void + { + $xml = new FluidXml(); + + $xml->add('parent', true) + ->times(2, 'addchild_FluidXmlTest'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testTimesWithCallableDoesNotRepeatFollowingMethodCall(): void + { + $xml = new FluidXml(); + + $xml->add('parent', true) + ->times(2, function ($i) { + $this->add("child{$i}"); + }) + ->add('lastchild'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .addChild() + // ------------------------------------------------------------------------- + + public function testAddChildIsFluid(): void + { + $this->assertIsFluid('addChild', 'a'); + } + + public function testAddChildAddsChildUsingArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1') + ->addChild('parent', true) + ->addChild('child2'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildUsingArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1']) + ->addChild(['parent'], true) + ->addChild(['child2']); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithStringValueArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', 'value1') + ->addChild('parent', true) + ->addChild('child2', 'value2'); + + $expected = "\n" + . " value1\n" + . " \n" + . " value2\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithStringValueArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => 'value1']) + ->addChild('parent', true) + ->addChild(['child2' => 'value2']); + + $expected = "\n" + . " value1\n" + . " \n" + . " value2\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithEmptyStringValueArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', '') + ->addChild('parent', true) + ->addChild('child2', ''); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithEmptyStringValueArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => '']) + ->addChild('parent', true) + ->addChild(['child2' => '']); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithNullValueArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', null) + ->addChild('parent', true) + ->addChild('child2', null); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithNullValueArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => null]) + ->addChild('parent', true) + ->addChild(['child2' => null]); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithIntegerValueArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', 1) + ->addChild('parent', true) + ->addChild('child2', 1); + + $expected = "\n" + . " 1\n" + . " \n" + . " 1\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithIntegerValueArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => 1]) + ->addChild('parent', true) + ->addChild(['child2' => 1]); + + $expected = "\n" + . " 1\n" + . " \n" + . " 1\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithZeroValueArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', 0) + ->addChild('parent', true) + ->addChild('child2', 0); + + $expected = "\n" + . " 0\n" + . " \n" + . " 0\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithZeroValueArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => 0]) + ->addChild('parent', true) + ->addChild(['child2' => 0]); + + $expected = "\n" + . " 0\n" + . " \n" + . " 0\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildEscapesXmlSpecialCharsArgumentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', 'a & b') + ->addChild('parent', true) + ->addChild('child2', 'a < b'); + + $expected = "\n" + . " a & b\n" + . " \n" + . " a < b\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildEscapesXmlSpecialCharsArraySyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => 'Hello & World', 'child2' => 'Tom & Jerry']); + + $expected = "\n" + . " Hello & World\n" + . " Tom & Jerry\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsManyChildrenWithAndWithoutValue(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1', 'child2', 'child3' => 'value3', 'child4' => 'value4']) + ->addChild('parent', true) + ->addChild(['child5', 'child6', 'child7' => 'value7', 'child8' => 'value8']); + + $expected = "\n" + . " \n" + . " \n" + . " value3\n" + . " value4\n" + . " \n" + . " \n" + . " \n" + . " value7\n" + . " value8\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsManyChildrenSameNameWithAndWithoutValue(): void + { + $xml = new FluidXml(); + $xml->addChild(['child', ['child'], ['child' => 'value1'], ['child' => 'value2']]) + ->addChild('parent', true) + ->addChild(['child', ['child'], ['child' => 'value3'], ['child' => 'value4']]); + + $expected = "\n" + . " \n" + . " \n" + . " value1\n" + . " value2\n" + . " \n" + . " \n" + . " \n" + . " value3\n" + . " value4\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsManyChildrenWithNestedArrays(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => ['child11' => ['child111', 'child112' => 'value112'], 'child12' => 'value12'], + 'child2' => ['child21', 'child22' => ['child221', 'child222']]]) + ->addChild('parent', true) + ->addChild(['child3' => ['child31' => ['child311', 'child312' => 'value312'], 'child32' => 'value32'], + 'child4' => ['child41', 'child42' => ['child421', 'child422']]]); + + $expected = << + + + + value112 + + value12 + + + + + + + + + + + + + value312 + + value32 + + + + + + + + + + +EOF; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildWithSomeAttributes(): void + { + $xml = new FluidXml(); + $xml->addChild('child1', ['class' => 'Class attr', 'id' => 'Id attr1']) + ->addChild('parent', true) + ->addChild('child2', ['class' => 'Class attr', 'id' => 'Id attr2']); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsManyChildrenWithSomeAttributes(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1', 'child2'], ['class' => 'Class attr', 'id' => 'Id attr1']) + ->addChild('parent', true) + ->addChild(['child3', 'child4'], ['class' => 'Class attr', 'id' => 'Id attr2']); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsChildrenWithAttributesAndTextUsingAtSyntax(): void + { + $xml = new FluidXml(); + $attrs = [ + '@class' => 'Class attr', + '@' => 'Text content', + '@id' => 'Id attr', + ]; + $xml->addChild(['child1' => $attrs]) + ->addChild(['child2' => $attrs], true) + ->addChild(['child3' => $attrs]); + + $expected = "\n" + . " Text content\n" + . " " + . "Text content" + . "Text content" + . "\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsTextContentUsingAtTextAlias(): void + { + $xml = new FluidXml(); + $xml->addChild(['child1' => ['@:text' => 'Hello']]); + + $expected = "\n" + . " Hello\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsCdataContentUsingAtCdataSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['chapter' => [ + '@id' => '1', + '@:cdata' => 'Ideas About Universe', + ]]); + + $expected = "\n" + . " Universe]]>\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsCommentContentUsingAtCommentSyntax(): void + { + $xml = new FluidXml(); + $xml->addChild(['node' => ['@:comment' => 'This is a comment']]); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildSwitchesContext(): void + { + $xml = new FluidXml(); + + self::assertInstanceOf(FluidContext::class, $xml->addChild('child', true)); + self::assertInstanceOf(FluidContext::class, $xml->addChild('child', 'value', true)); + self::assertInstanceOf(FluidContext::class, $xml->addChild(['child1', 'child2'], true)); + self::assertInstanceOf(FluidContext::class, $xml->addChild(['child1' => 'value1', 'child2' => 'value2'], true)); + self::assertInstanceOf(FluidContext::class, $xml->addChild('child', ['attr' => 'value'], true)); + self::assertInstanceOf(FluidContext::class, $xml->addChild(['child1', 'child2'], ['attr' => 'value'], true)); + } + + public function testAddChildAddsNamespacedChildren(): void + { + $xml = new FluidXml(); + $xml->namespace(new FluidNamespace('x', 'x.com')); + $xml->namespace(fluidns('xx', 'xx.com', FluidNamespace::MODE_IMPLICIT)); + $xml->addChild('x:xTag1', true)->addChild('x:xTag2'); + $xml->addChild('xx:xxTag1', true)->addChild('xx:xxTag2')->addChild('tag3'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildFillsDocumentWithXmlString(): void + { + $xml = new FluidXml(null); + $xml->addChild(''); + + $this->assertEqualXml($xml, ""); + } + + public function testAddChildFillsDocumentWithXmlStringWithMultipleRootNodes(): void + { + $xml = new FluidXml(null); + $xml->addChild(''); + + $expected = "\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsXmlStringWithMultipleRootNodes(): void + { + $xml = new FluidXml(); + $xml->addChild(''); + + $expected = "\n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(''); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildAddsDomDocument(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML('content'); + + $xml = new FluidXml(); + $xml->addChild($dom); + + $this->assertEqualXml($xml, $doc); + } + + public function testAddChildAddsDomNode(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $xp = new \DOMXPath($dom); + $nodes = $xp->query('/doc/parent'); + $xml = new FluidXml(); + $xml->addChild($nodes[0]); + + $this->assertEqualXml($xml, $doc); + } + + public function testAddChildAddsDomNodeList(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $xp = new \DOMXPath($dom); + $nodes = $xp->query('/doc/parent'); + $xml = new FluidXml(); + $xml->addChild($nodes); + + $this->assertEqualXml($xml, $doc); + } + + public function testAddChildAddsSimpleXmlElement(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $sxml = simplexml_import_dom($dom); + $xml = new FluidXml(); + $xml->addChild($sxml->children()); + + $this->assertEqualXml($xml, $doc); + } + + public function testAddChildAddsFluidXml(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $nodes = $dom->documentElement->childNodes; + $fxml = new FluidXml($nodes); + $xml = new FluidXml(); + $xml->addChild($fxml); + + $this->assertEqualXml($xml, $doc); + } + + public function testAddChildAddsFluidContext(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $fxml = (new FluidXml($dom))->query('/doc/parent'); + $xml = new FluidXml(); + $xml->addChild($fxml); + + $this->assertEqualXml($xml, $doc); + } + + public function testAddChildAddsManyInstances(): void + { + $doc = "\n" + . " content\n" + . ""; + $dom = new \DOMDocument(); + $dom->loadXML($doc); + + $fxml = (new FluidXml($dom))->query('/doc/parent'); + $xml = new FluidXml(); + $xml->addChild([$fxml, 'imported' => $fxml]); + + $expected = "\n" + . " content\n" + . " \n" + . " content\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddChildThrowsForNotSupportedInput(): void + { + $xml = new FluidXml(); + try { + $xml->addChild(0); + } catch (\Exception $e) { + $actual = $e; + } + + self::assertInstanceOf(\Exception::class, $actual); + } + + // ------------------------------------------------------------------------- + // .add() + // ------------------------------------------------------------------------- + + public function testAddIsFluid(): void + { + $this->assertIsFluid('add', 'a'); + } + + public function testAddBehavesLikeAddChild(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'child']); + + $alias = new FluidXml(); + $alias->add('parent', true) + ->add(['child1', 'child2'], ['class' => 'child']); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .prependSibling() + // ------------------------------------------------------------------------- + + public function testPrependSiblingIsFluid(): void + { + $this->assertIsFluid('prependSibling', 'a'); + } + + public function testPrependSiblingAddsMoreThanOneRootNodeToDocumentWithOneRoot(): void + { + $xml = new FluidXml(); + $xml->prependSibling('meta'); + $xml->prependSibling('extra'); + $cx = $xml->query('/*'); + + self::assertSame('extra', $cx[0]->nodeName); + self::assertSame('meta', $cx[1]->nodeName); + self::assertSame('doc', $cx[2]->nodeName); + } + + public function testPrependSiblingAddsMoreThanOneRootNodeToDocumentWithNoRoot(): void + { + $xml = new FluidXml(null); + $xml->prependSibling('meta'); + $xml->prependSibling('extra'); + $cx = $xml->query('/*'); + + self::assertSame('extra', $cx[0]->nodeName); + self::assertSame('meta', $cx[1]->nodeName); + } + + public function testPrependSiblingAddsSiblingNodeBeforeNode(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->prependSibling('sibling1') + ->prependSibling('sibling2'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testPrependSiblingAddsXmlDocumentInstanceBeforeNode(): void + { + $dom = new \DOMDocument(); + $dom->loadXML('content'); + + $xml = new FluidXml(); + $xml->prependSibling($dom); + + $expected = "content\n" + . ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('sibling', true) + ->prependSibling($dom); + + $expected = "\n" + . " content\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .prepend() + // ------------------------------------------------------------------------- + + public function testPrependIsFluid(): void + { + $this->assertIsFluid('prepend', 'a'); + } + + public function testPrependBehavesLikePrependSibling(): void + { + $xml = new FluidXml(); + $xml->prependSibling('sibling1', true) + ->prependSibling(['sibling2', 'sibling3'], ['class' => 'sibling']); + + $alias = new FluidXml(); + $alias->prepend('sibling1', true) + ->prepend(['sibling2', 'sibling3'], ['class' => 'sibling']); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .appendSibling() + // ------------------------------------------------------------------------- + + public function testAppendSiblingIsFluid(): void + { + $this->assertIsFluid('appendSibling', 'a'); + } + + public function testAppendSiblingAddsMoreThanOneRootNodeToDocumentWithOneRoot(): void + { + $xml = new FluidXml(); + $xml->appendSibling('meta'); + $xml->appendSibling('extra'); + $cx = $xml->query('/*'); + + self::assertSame('doc', $cx[0]->nodeName); + self::assertSame('extra', $cx[1]->nodeName); + self::assertSame('meta', $cx[2]->nodeName); + } + + public function testAppendSiblingAddsMoreThanOneRootNodeToDocumentWithNoRoot(): void + { + $xml = new FluidXml(null); + $xml->appendSibling('meta'); + $xml->appendSibling('extra'); + $cx = $xml->query('/*'); + + self::assertSame('meta', $cx[0]->nodeName); + self::assertSame('extra', $cx[1]->nodeName); + } + + public function testAppendSiblingAddsSiblingNodeAfterNode(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->appendSibling('sibling1') + ->appendSibling('sibling2'); + + $expected = "\n" + . " \n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAppendSiblingAddsXmlDocumentInstanceAfterNode(): void + { + $dom = new \DOMDocument(); + $dom->loadXML('content'); + + $xml = new FluidXml(); + $xml->appendSibling($dom); + + $expected = "\n" + . "content"; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('sibling', true) + ->appendSibling($dom); + + $expected = "\n" + . " \n" + . " content\n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .append() + // ------------------------------------------------------------------------- + + public function testAppendIsFluid(): void + { + $this->assertIsFluid('append', 'a'); + } + + public function testAppendBehavesLikeAppendSibling(): void + { + $xml = new FluidXml(); + $xml->appendSibling('sibling1', true) + ->appendSibling(['sibling2', 'sibling3'], ['class' => 'sibling']); + + $alias = new FluidXml(); + $alias->append('sibling1', true) + ->append(['sibling2', 'sibling3'], ['class' => 'sibling']); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .setAttribute() + // ------------------------------------------------------------------------- + + public function testSetAttributeIsFluid(): void + { + $this->assertIsFluid('setAttribute', 'a', 'b'); + } + + public function testSetAttributeSetsAttributesOfRootNode(): void + { + $xml = new FluidXml(); + $xml->setAttribute('attr1', 'Attr1 Value') + ->setAttribute('attr2', 'Attr2 Value'); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->setAttribute(['attr1' => 'Attr1 Value', 'attr2' => 'Attr2 Value']); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + } + + public function testSetAttributeChangesAttributesOfRootNode(): void + { + $xml = new FluidXml(); + $xml->setAttribute('attr1', 'Attr1 Value') + ->setAttribute('attr2', 'Attr2 Value'); + + $xml->setAttribute('attr2', 'Attr2 New Value'); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + + $xml->setAttribute('attr1', 'Attr1 New Value'); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + } + + public function testSetAttributeSetsAttributesOfNode(): void + { + $xml = new FluidXml(); + $xml->addChild('child', true) + ->setAttribute('attr1', 'Attr1 Value') + ->setAttribute('attr2', 'Attr2 Value'); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('child', true) + ->setAttribute(['attr1' => 'Attr1 Value', 'attr2' => 'Attr2 Value']); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testSetAttributeSetsAttributesWithoutValuesOfNode(): void + { + $xml = new FluidXml(); + $xml->addChild('child', true) + ->setAttribute('attr1') + ->setAttribute('attr2'); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('child', true) + ->setAttribute(['attr1', 'attr2']); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testSetAttributeChangesAttributesOfNode(): void + { + $xml = new FluidXml(); + $xml->addChild('child', true) + ->setAttribute('attr1', 'Attr1 Value') + ->setAttribute('attr2', 'Attr2 Value') + ->setAttribute('attr2', 'Attr2 New Value'); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + + $xml = new FluidXml(); + $xml->addChild('child', true) + ->setAttribute(['attr1' => 'Attr1 Value', 'attr2' => 'Attr2 Value']) + ->setAttribute('attr1', 'Attr1 New Value'); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .attr() + // ------------------------------------------------------------------------- + + public function testAttrIsFluid(): void + { + $this->assertIsFluid('attr', 'a', 'b'); + } + + public function testAttrBehavesLikeSetAttribute(): void + { + $xml = new FluidXml(); + $xml->setAttribute('attr1', 'Value 1') + ->setAttribute('attr2') + ->setAttribute(['attr3' => 'Value 3', 'attr4' => 'Value 4']) + ->setAttribute(['attr5', 'attr6']) + ->addChild('child', true) + ->setAttribute('attr1', 'Value 1') + ->setAttribute('attr2') + ->setAttribute(['attr3' => 'Value 3', 'attr4' => 'Value 4']) + ->setAttribute(['attr5', 'attr6']); + + $alias = new FluidXml(); + $alias->attr('attr1', 'Value 1') + ->attr('attr2') + ->attr(['attr3' => 'Value 3', 'attr4' => 'Value 4']) + ->attr(['attr5', 'attr6']) + ->addChild('child', true) + ->attr('attr1', 'Value 1') + ->attr('attr2') + ->attr(['attr3' => 'Value 3', 'attr4' => 'Value 4']) + ->attr(['attr5', 'attr6']); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .setText() + // ------------------------------------------------------------------------- + + public function testSetTextIsFluid(): void + { + $this->assertIsFluid('setText', 'a'); + } + + public function testSetTextSetsOrChangesTextOfRootNode(): void + { + $xml = new FluidXml(); + $xml->setText('First Text') + ->setText('Second Text'); + + $expected = "Second Text"; + $this->assertEqualXml($xml, $expected); + } + + public function testSetTextSetsOrChangesTextOfNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('p', true); + $cx->setText('First Text') + ->setText('Second Text'); + + $expected = "\n" + . "

Second Text

\n" + . "
"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .text() + // ------------------------------------------------------------------------- + + public function testTextIsFluid(): void + { + $this->assertIsFluid('text', 'a'); + } + + public function testTextBehavesLikeSetText(): void + { + $xml = new FluidXml(); + $xml->setText('Text1') + ->addChild('child', true) + ->setText('Text2'); + + $alias = new FluidXml(); + $alias->text('Text1') + ->addChild('child', true) + ->text('Text2'); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .addText() + // ------------------------------------------------------------------------- + + public function testAddTextIsFluid(): void + { + $this->assertIsFluid('addText', 'a'); + } + + public function testAddTextAddsTextToRootNode(): void + { + $xml = new FluidXml(); + $xml->addText('First Line') + ->addText('Second Line'); + + $expected = "First LineSecond Line"; + $this->assertEqualXml($xml, $expected); + } + + public function testAddTextAddsTextToNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('p', true); + $cx->addText('First Line') + ->addText('Second Line'); + + $expected = "\n" + . "

First LineSecond Line

\n" + . "
"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .setCdata() + // ------------------------------------------------------------------------- + + public function testSetCdataIsFluid(): void + { + $this->assertIsFluid('setCdata', 'a'); + } + + public function testSetCdataSetsOrChangesCdataOfRootNode(): void + { + $xml = new FluidXml(); + $xml->setCdata('First Data') + ->setCdata('Second Data'); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + } + + public function testSetCdataSetsOrChangesCdataOfNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('p', true); + $cx->setCdata('First Data') + ->setCdata('Second Data'); + + $expected = "\n" + . "

\n" + . "
"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .cdata() + // ------------------------------------------------------------------------- + + public function testCdataIsFluid(): void + { + $this->assertIsFluid('cdata', 'a'); + } + + public function testCdataBehavesLikeSetCdata(): void + { + $xml = new FluidXml(); + $xml->setCdata('Text1') + ->addChild('child', true) + ->setCdata('Text2'); + + $alias = new FluidXml(); + $alias->cdata('Text1') + ->addChild('child', true) + ->cdata('Text2'); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .addCdata() + // ------------------------------------------------------------------------- + + public function testAddCdataIsFluid(): void + { + $this->assertIsFluid('addCdata', 'a'); + } + + public function testAddCdataAddsCdataToRootNode(): void + { + $xml = new FluidXml(); + $xml->addCdata('// <, > are characters that should be escaped in a XML context.') + ->addCdata('// Even & is a characters that should be escaped in a XML context.'); + + $expected = "" + . " are characters that should be escaped in a XML context.]]>" + . "" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddCdataAddsCdataToNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('pre', true); + $cx->addCdata('// <, > are characters that should be escaped in a XML context.') + ->addCdata('// Even & is a characters that should be escaped in a XML context.'); + + $expected = "\n" + . "
"
+                  . " are characters that should be escaped in a XML context.]]>"
+                  . ""
+                  .    "
\n" + . "
"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .setComment() + // ------------------------------------------------------------------------- + + public function testSetCommentIsFluid(): void + { + $this->assertIsFluid('setComment', 'a'); + } + + public function testSetCommentSetsOrChangesCommentOfRootNode(): void + { + $xml = new FluidXml(); + $xml->setComment('First') + ->setComment('Second'); + + $expected = ""; + $this->assertEqualXml($xml, $expected); + } + + public function testSetCommentSetsOrChangesCommentOfNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('p', true); + $cx->setComment('First') + ->setComment('Second'); + + $expected = "\n" + . "

\n" + . "
"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .comment() + // ------------------------------------------------------------------------- + + public function testCommentIsFluid(): void + { + $this->assertIsFluid('comment', 'a'); + } + + public function testCommentBehavesLikeSetComment(): void + { + $xml = new FluidXml(); + $xml->setComment('Text1') + ->addChild('child', true) + ->setComment('Text2'); + + $alias = new FluidXml(); + $alias->comment('Text1') + ->addChild('child', true) + ->comment('Text2'); + + self::assertSame($xml->xml(), $alias->xml()); + } + + // ------------------------------------------------------------------------- + // .addComment() + // ------------------------------------------------------------------------- + + public function testAddCommentIsFluid(): void + { + $this->assertIsFluid('addComment', 'a'); + } + + public function testAddCommentAddsCommentsToRootNode(): void + { + $xml = new FluidXml(); + $xml->addComment('First') + ->addComment('Second'); + + $expected = "\n" + . " \n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testAddCommentAddsCommentsToNode(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('pre', true); + $cx->addComment('First') + ->addComment('Second'); + + $expected = "\n" + . "
\n"
+                  . "    \n"
+                  . "    \n"
+                  . "  
\n" + . "
"; + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .remove() + // ------------------------------------------------------------------------- + + public function testRemoveIsFluid(): void + { + $this->assertIsFluid('remove', 'a'); + } + + public function testRemoveRemovesRootNode(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'removable']); + + $xml->remove(); + + $this->assertEqualXml($xml, ''); + } + + public function testRemoveRemovesResultsOfPreviousQuery(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'removable']); + + $xml->query('//*[@class="removable"]')->remove(); + + $expected = "\n" + . " \n" + . ""; + $this->assertEqualXml($xml, $expected); + } + + public function testRemoveRemovesAbsoluteAndRelativeTargetsXpath(): void + { + $expected = "\n" + . " \n" + . ""; + + $newDoc = function () { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'removable']); + return $xml; + }; + + $xml = $newDoc(); + $xml->remove('//*[@class="removable"]'); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc')->remove('//*[@class="removable"]'); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc/parent')->remove('./*[@class="removable"]'); + $this->assertEqualXml($xml, $expected); + } + + public function testRemoveRemovesAbsoluteAndRelativeTargetsCss(): void + { + $expected = "\n" + . " \n" + . ""; + + $newDoc = function () { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'removable']); + return $xml; + }; + + $xml = $newDoc(); + $xml->remove('.removable'); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc')->remove(':root .removable'); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc/parent')->remove('.removable'); + $this->assertEqualXml($xml, $expected); + } + + public function testRemoveRemovesAbsoluteAndRelativeTargetsArrayOfQueriesXpathAndCss(): void + { + $expected = "\n" + . " \n" + . ""; + + $newDoc = function () { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'removable']); + return $xml; + }; + + $xml = $newDoc(); + $xml->remove(['//child1', ':root child2']); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc')->remove(['//child1', ':root child2']); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc/parent')->remove(['./child1', 'child2']); + $this->assertEqualXml($xml, $expected); + } + + public function testRemoveRemovesAbsoluteAndRelativeTargetsVariableListOfQueriesXpathAndCss(): void + { + $expected = "\n" + . " \n" + . ""; + + $newDoc = function () { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild(['child1', 'child2'], ['class' => 'removable']); + return $xml; + }; + + $xml = $newDoc(); + $xml->remove('//child1', ':root child2'); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc')->remove('//child1', ':root child2'); + $this->assertEqualXml($xml, $expected); + + $xml = $newDoc(); + $xml->query('/doc/parent')->remove('./child1', 'child2'); + $this->assertEqualXml($xml, $expected); + } + + // ------------------------------------------------------------------------- + // .dom() + // ------------------------------------------------------------------------- + + public function testDomReturnsAssociatedDomDocumentInstance(): void + { + $xml = new FluidXml(); + + self::assertInstanceOf(\DOMDocument::class, $xml->dom()); + self::assertInstanceOf(\DOMDocument::class, $xml->query('/*')->dom()); + } + + // ------------------------------------------------------------------------- + // .xml() + // ------------------------------------------------------------------------- + + public function testXmlReturnsDocumentAsXmlString(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild('child', 'content'); + + $expected = "\n" + . " \n" + . " content\n" + . " \n" + . ""; + + $this->assertEqualXml($xml, $expected); + } + + public function testXmlReturnsDocumentAsXmlStringWithoutXmlHeaders(): void + { + $xml = new FluidXml('doc', ['stylesheet' => 'x.com/style.xsl']); + $xml->addChild('parent', true) + ->addChild('child', 'content'); + + $actual = $xml->xml(true); + $expected = "\n" + . " \n" + . " content\n" + . " \n" + . ""; + self::assertSame($expected, $actual); + } + + public function testXmlReturnsNodeAndDescendantsAsXmlString(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addText('parent content') + ->addChild('child', 'content'); + + $actual = $xml->query('//parent')->xml(); + $expected = "parent contentcontent"; + self::assertSame($expected, $actual); + + $xml = new FluidXml(); + $xml->addChild('parent', true) + ->addChild('child', 'content1') + ->addChild('child', 'content2'); + + $actual = $xml->query('//child')->xml(); + $expected = "content1\n" + . "content2"; + self::assertSame($expected, $actual); + } + + public function testXmlRendersEmptyElementsAsExplicitClosingTagsWithLibxmlNoemptytag(): void + { + $xml = new FluidXml(); + $xml->addChild(['parent' => ['child1', 'child2']]); + + $actual = $xml->xml(true, \LIBXML_NOEMPTYTAG); + $expected = "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + self::assertSame($expected, $actual); + } + + public function testXmlPassesLibxmlNoemptytagThroughWhenQueryingContext(): void + { + $xml = new FluidXml(); + $xml->addChild(['parent' => ['child1', 'child2']]); + + $actual = $xml->query('//parent')->xml(false, \LIBXML_NOEMPTYTAG); + $expected = "\n" + . " \n" + . " \n" + . ""; + self::assertSame($expected, $actual); + } + + // ------------------------------------------------------------------------- + // .toArray() + // ------------------------------------------------------------------------- + + public function testToArrayExportsSimpleElementWithTextContent(): void + { + $xml = new FluidXml(); + $xml->addChild(['type' => 'Herbivore']); + + $actual = $xml->toArray(); + $expected = ['doc' => ['type' => 'Herbivore']]; + self::assertSame($expected, $actual); + } + + public function testToArrayExportsAttributesUsingAtPrefix(): void + { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['@operation' => 'create', '@' => 'data']]); + + $actual = $xml->toArray(); + $expected = ['doc' => ['animal' => ['@operation' => 'create', '@' => 'data']]]; + self::assertSame($expected, $actual); + } + + public function testToArrayExportsNestedElementsAsNestedArrays(): void + { + $xml = new FluidXml(); + $xml->addChild(['animal' => [ + '@operation' => 'create', + 'type' => 'Herbivore', + 'attribute' => ['legs' => '4', 'head' => '1'], + ]]); + + $actual = $xml->toArray(); + $expected = ['doc' => [ + 'animal' => [ + '@operation' => 'create', + 'type' => 'Herbivore', + 'attribute' => ['legs' => '4', 'head' => '1'], + ], + ]]; + self::assertSame($expected, $actual); + } + + public function testToArrayExportsRepeatedSiblingElementsAsIndexedArrays(): void + { + $xml = new FluidXml(); + $xml->addChild(['chapters' => [ + ['chapter' => 'One'], + ['chapter' => 'Two'], + ]]); + + $actual = $xml->toArray(); + $expected = ['doc' => [ + 'chapters' => [ + ['chapter' => 'One'], + ['chapter' => 'Two'], + ], + ]]; + self::assertSame($expected, $actual); + } + + public function testToArrayExportsEmptyElementAsNull(): void + { + $xml = new FluidXml(); + $xml->addChild('empty'); + + $actual = $xml->toArray(); + $expected = ['doc' => ['empty' => null]]; + self::assertSame($expected, $actual); + } + + public function testToArrayExportsQueriedNodesViaFluidContext(): void + { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['type' => 'Herbivore', 'legs' => '4']]); + + $actual = $xml->query('//type')->toArray(); + $expected = [['type' => 'Herbivore']]; + self::assertSame($expected, $actual); + } + + public function testToArrayExportsNamespaceDeclarationsUsingAtXmlnsPrefix(): void + { + $xml = new FluidXml('animal'); + $xml->namespace(new FluidNamespace('foo', 'http://foo.com')); + $xml->addChild('foo:legs', '4'); + + $actual = $xml->toArray(); + $expected = ['animal' => [ + 'foo:legs' => [ + '@xmlns:foo' => 'http://foo.com', + '@' => '4', + ], + ]]; + self::assertSame($expected, $actual); + } + + // ------------------------------------------------------------------------- + // .toObject() + // ------------------------------------------------------------------------- + + public function testToObjectReturnsstdClassMirroringToArray(): void + { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['@operation' => 'create', 'type' => 'Herbivore']]); + + $obj = $xml->toObject(); + self::assertInstanceOf(\stdClass::class, $obj); + self::assertInstanceOf(\stdClass::class, $obj->doc); + self::assertInstanceOf(\stdClass::class, $obj->doc->animal); + self::assertSame('create', $obj->doc->animal->{'@operation'}); + self::assertSame('Herbivore', $obj->doc->animal->type); + } + + public function testToObjectPreservesIndexedArraysForRepeatedSiblings(): void + { + $xml = new FluidXml(); + $xml->addChild(['chapters' => [ + ['chapter' => 'One'], + ['chapter' => 'Two'], + ]]); + + $obj = $xml->toObject(); + self::assertTrue(is_array($obj->doc->chapters)); + self::assertInstanceOf(\stdClass::class, $obj->doc->chapters[0]); + self::assertSame('One', $obj->doc->chapters[0]->chapter); + } + + public function testToObjectReturnsArrayOfstdClassForFluidContext(): void + { + $xml = new FluidXml(); + $xml->addChild(['animal' => ['type' => 'Herbivore', 'legs' => '4']]); + + $result = $xml->query('//animal')->toObject(); + self::assertTrue(is_array($result)); + self::assertInstanceOf(\stdClass::class, $result[0]); + self::assertInstanceOf(\stdClass::class, $result[0]->animal); + self::assertSame('Herbivore', $result[0]->animal->type); + } + + // ------------------------------------------------------------------------- + // .__toString() + // ------------------------------------------------------------------------- + + public function testToStringBehavesLikeXml(): void + { + $xml = new FluidXml(); + $cx = $xml->addChild('parent', true)->addChild(['child1', 'child2']); + + $actual = trim("$xml"); + $expected = "\n" + . "\n" + . " \n" + . " \n" + . " \n" + . " \n" + . ""; + self::assertSame($expected, $actual); + + $actual = "$cx"; + $expected = "\n" + . " \n" + . " \n" + . ""; + self::assertSame($expected, $actual); + } + + // ------------------------------------------------------------------------- + // .html() + // ------------------------------------------------------------------------- + + public function testHtmlReturnsDocumentAsValidHtml5String(): void + { + $xml = new FluidXml([ + 'html' => ['body' => ['input', 'div']], + ]); + + $actual = $xml->html(); + $expected = "\n" + . "\n" + . " \n" + . " \n" + . "
\n" + . " \n" + . ""; + self::assertSame($expected, $actual); + } + + public function testHtmlReturnsDocumentAsValidHtml5StringWithoutDoctype(): void + { + $xml = new FluidXml([ + 'html' => ['body' => ['input', 'div']], + ]); + + $actual = $xml->html(true); + $expected = "\n" + . " \n" + . " \n" + . "
\n" + . " \n" + . ""; + self::assertSame($expected, $actual); + } + + public function testHtmlReturnsNodeAndDescendantsAsHtmlString(): void + { + $xml = new FluidXml([ + 'html' => ['body' => ['input', 'div']], + ]); + + $actual = $xml->query('//body/*')->html(); + $expected = "\n" + . "
"; + self::assertSame($expected, $actual); + } + + // ------------------------------------------------------------------------- + // .save() + // ------------------------------------------------------------------------- + + public function testSaveIsFluid(): void + { + $file = $this->outDir . '.test_save0.xml'; + $this->assertIsFluid('save', $file); + unlink($file); + } + + public function testSaveStoresEntireXmlDocumentInFile(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true)->addChild('child', 'content'); + + $file = $this->outDir . '.test_save1.xml'; + $xml->save($file); + + $actual = trim(file_get_contents($file)); + $expected = "\n" + . "\n" + . " \n" + . " content\n" + . " \n" + . ""; + + unlink($file); + + self::assertSame($expected, $actual); + } + + public function testSaveStoresFragmentOfXmlDocumentInFile(): void + { + $xml = new FluidXml(); + $xml->addChild('parent', true)->addChild('child', 'content'); + + $file = $this->outDir . '.test_save2.xml'; + $xml->query('//child')->save($file); + + $actual = trim(file_get_contents($file)); + $expected = "content"; + + unlink($file); + + self::assertSame($expected, $actual); + } + + public function testSaveThrowsForNotWritableFile(): void + { + $xml = new FluidXml(); + + set_error_handler(function () {}); + try { + $xml->save('/.impossible/tmp/out.xml'); + } catch (\Exception $e) { + $actual = $e; + } finally { + restore_error_handler(); + } + + self::assertInstanceOf(\Exception::class, $actual); + } +} + +// Free-standing callables used in .each() / .map() / .filter() / .times() tests + +function eachassert_FluidXmlTest(\FluidXml\FluidContext $cx, int $i, \DOMNode $n): void +{ + assert($cx instanceof \FluidXml\FluidContext); + assert($n instanceof \DOMNode); + assert($i === 0); +} + +function eachsettext_FluidXmlTest(\FluidXml\FluidContext $cx, int $i, \DOMNode $n): void +{ + $idx = $i + 1; + $cx->setText($n->nodeName . $idx); +} + +function mapassert_FluidXmlTest(\FluidXml\FluidContext $cx, int $i, \DOMNode $n): void +{ + assert($cx instanceof \FluidXml\FluidContext); + assert($n instanceof \DOMNode); + assert($i === 0); +} + +function mapfn_FluidXmlTest(\FluidXml\FluidContext $cx, int $i, \DOMNode $n): string +{ + $idx = $i + 1; + return $n->nodeValue . $idx; +} + +function filterassert_FluidXmlTest(\FluidXml\FluidContext $cx, int $i, \DOMNode $n): void +{ + assert($cx instanceof \FluidXml\FluidContext); + assert($n instanceof \DOMNode); + assert($i === 0); +} + +function addchild_FluidXmlTest(\FluidXml\FluidContext $parent, int $i): void +{ + $parent->add("child{$i}"); +}