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}"); +}