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/source/FluidXml/FluidHelper.php b/source/FluidXml/FluidHelper.php
deleted file mode 100644
index 25b0b71..0000000
--- a/source/FluidXml/FluidHelper.php
+++ /dev/null
@@ -1,123 +0,0 @@
-$delegate($node);
-
- if ($html) {
- return static::domnodeToHtml($node);
- }
-
- return $dom->saveXML($node);
- }
-
- public static function domdocumentToStringWithoutHeaders(\DOMDocument $dom, $html = false): bool|array|string|null
- {
- return static::exportNode($dom, $dom->documentElement, $html);
- }
-
- public static function domnodelistToString(\DOMNodeList $nodelist, $html = false): string
- {
- $nodes = [];
-
- // Algorithm 1:
- foreach ($nodelist as $n) {
- $nodes[] = $n;
- }
-
- // Algorithm 2:
- // $nodes = \iterator_to_array($nodelist);
-
- // Algorithm 1 is faster than Algorithm 2.
-
- return static::domnodesToString($nodes, $html);
- }
-
- public static function domnodesToString(array $nodes, $html = false): string
- {
- $dom = $nodes[0]->ownerDocument;
- $xml = '';
-
- foreach ($nodes as $n) {
- $xml .= static::exportNode($dom, $n, $html) . PHP_EOL;
- }
-
- return \rtrim($xml);
- }
-
- public static function simplexmlToStringWithoutHeaders(\SimpleXMLElement $element, $html = false): bool|array|string|null
- {
- $dom = \dom_import_simplexml($element);
-
- return static::exportNode($dom->ownerDocument, $dom, $html);
- }
-
- public static function domdocumentToHtml($dom, $clone = true): array|string|null
- {
- if ($clone) {
- $dom = $dom->cloneNode(true);
- }
-
- $voids = ['area',
- 'base',
- 'br',
- 'col',
- 'colgroup',
- 'command',
- 'embed',
- 'hr',
- 'img',
- 'input',
- 'keygen',
- 'link',
- 'meta',
- 'param',
- 'source',
- 'track',
- 'wbr'];
-
- // Every empty node. There is no reason to match nodes with content inside.
- $query = '//*[not(node())]';
- $nodes = (new \DOMXPath($dom))->query($query);
-
- foreach ($nodes as $n) {
- if (! \in_array($n->nodeName, $voids)) {
- // If it is not a void/empty tag,
- // we need to leave the tag open.
- $n->appendChild(new \DOMProcessingInstruction('X-NOT-VOID'));
- }
- }
-
- $html = static::domdocumentToStringWithoutHeaders($dom);
-
- // Let's remove the placeholder.
- $html = \preg_replace('/\s*<\?X-NOT-VOID\?>\s*/', '', (string) $html);
-
- return $html;
- }
-
- public static function domnodeToHtml(\DOMNode $node): array|string|null
- {
- $dom = new \DOMDocument();
- $dom->formatOutput = true;
- $dom->preserveWhiteSpace = false;
- $node = $dom->importNode($node, true);
- $dom->appendChild($node);
-
- return static::domdocumentToHtml($dom, false);
- }
-}
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 6cac1ef..0000000
--- a/specs/FluidXml.php
+++ /dev/null
@@ -1,3018 +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);
- });
- });
-
- 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 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 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));
- });
- });
-
- 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 95%
rename from source/FluidXml/FluidContext.php
rename to src/FluidXml/FluidContext.php
index 6f76e41..275baa4 100644
--- a/source/FluidXml/FluidContext.php
+++ b/src/FluidXml/FluidContext.php
@@ -397,9 +397,30 @@ 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 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
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/src/FluidXml/FluidHelper.php b/src/FluidXml/FluidHelper.php
new file mode 100644
index 0000000..d1c1609
--- /dev/null
+++ b/src/FluidXml/FluidHelper.php
@@ -0,0 +1,205 @@
+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,
+ // otherwise the first character check may fail.
+ $string = \ltrim((string) $string);
+
+ return $string && $string[0] === '<';
+ }
+
+ 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);
+
+ if ($html) {
+ return static::domnodeToHtml($node);
+ }
+
+ return $dom->saveXML($node, $options);
+ }
+
+ public static function domdocumentToStringWithoutHeaders(\DOMDocument $dom, $html = false, int $options = 0): bool|array|string|null
+ {
+ return static::exportNode($dom, $dom->documentElement, $html, $options);
+ }
+
+ public static function domnodelistToString(\DOMNodeList $nodelist, $html = false, int $options = 0): string
+ {
+ $nodes = [];
+
+ // Algorithm 1:
+ foreach ($nodelist as $n) {
+ $nodes[] = $n;
+ }
+
+ // Algorithm 2:
+ // $nodes = \iterator_to_array($nodelist);
+
+ // Algorithm 1 is faster than Algorithm 2.
+
+ return static::domnodesToString($nodes, $html, $options);
+ }
+
+ 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, $options) . PHP_EOL;
+ }
+
+ return \rtrim($xml);
+ }
+
+ public static function simplexmlToStringWithoutHeaders(\SimpleXMLElement $element, $html = false): bool|array|string|null
+ {
+ $dom = \dom_import_simplexml($element);
+
+ return static::exportNode($dom->ownerDocument, $dom, $html);
+ }
+
+ public static function domdocumentToHtml($dom, $clone = true): array|string|null
+ {
+ if ($clone) {
+ $dom = $dom->cloneNode(true);
+ }
+
+ $voids = ['area',
+ 'base',
+ 'br',
+ 'col',
+ 'colgroup',
+ 'command',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'keygen',
+ 'link',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr'];
+
+ // Every empty node. There is no reason to match nodes with content inside.
+ $query = '//*[not(node())]';
+ $nodes = (new \DOMXPath($dom))->query($query);
+
+ foreach ($nodes as $n) {
+ if (! \in_array($n->nodeName, $voids)) {
+ // If it is not a void/empty tag,
+ // we need to leave the tag open.
+ $n->appendChild(new \DOMProcessingInstruction('X-NOT-VOID'));
+ }
+ }
+
+ $html = static::domdocumentToStringWithoutHeaders($dom);
+
+ // Let's remove the placeholder.
+ $html = \preg_replace('/\s*<\?X-NOT-VOID\?>\s*/', '', (string) $html);
+
+ return $html;
+ }
+
+ public static function domnodeToHtml(\DOMNode $node): array|string|null
+ {
+ $dom = new \DOMDocument();
+ $dom->formatOutput = true;
+ $dom->preserveWhiteSpace = false;
+ $node = $dom->importNode($node, true);
+ $dom->appendChild($node);
+
+ return static::domdocumentToHtml($dom, false);
+ }
+}
diff --git a/source/FluidXml/FluidInsertionHandler.php b/src/FluidXml/FluidInsertionHandler.php
similarity index 89%
rename from source/FluidXml/FluidInsertionHandler.php
rename to src/FluidXml/FluidInsertionHandler.php
index f47fd46..ad472d7 100644
--- a/source/FluidXml/FluidInsertionHandler.php
+++ b/src/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';
}
@@ -226,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);
}
@@ -250,6 +261,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:
@@ -269,9 +300,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/source/FluidXml/FluidInterface.php b/src/FluidXml/FluidInterface.php
similarity index 86%
rename from source/FluidXml/FluidInterface.php
rename to src/FluidXml/FluidInterface.php
index 0bfc6e4..760e65e 100644
--- a/source/FluidXml/FluidInterface.php
+++ b/src/FluidXml/FluidInterface.php
@@ -9,9 +9,11 @@ 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 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/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 67%
rename from source/FluidXml/FluidSaveTrait.php
rename to src/FluidXml/FluidSaveTrait.php
index c0af9d2..20e9be3 100644
--- a/source/FluidXml/FluidSaveTrait.php
+++ b/src/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/src/FluidXml/FluidXml.php
similarity index 91%
rename from source/FluidXml/FluidXml.php
rename to src/FluidXml/FluidXml.php
index 1f8b11d..400fe5d 100644
--- a/source/FluidXml/FluidXml.php
+++ b/src/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)
@@ -152,13 +152,29 @@ 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 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
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}");
+}