diff --git a/CHANGELOG.md b/CHANGELOG.md index 09dc393..5f965dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,19 +22,21 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed -- Nothing. +- [#21](https://github.com/zendframework/zend-dom/pull/21) fixes an issue with + matching against nested attribute selectors (e.g., `div[class="foo"] div + [class="bar"]`), ensuring such syntax will transform to expected XPath. ## 2.7.0 - 2018-03-27 ### Added -- [#20](https://github.com/zendframework/zend-dom/pull/4) adds support for +- [#20](https://github.com/zendframework/zend-dom/pull/20) adds support for attribute selectors that contain spaces, such as `input[value="Marty McFly"]`. Previously, spaces within the selector value would result in a query per space-separated word; they now, correctly, result in a single query for the exact value. -- [#19](https://github.com/zendframework/zend-dom/pull/4) adds support for PHP +- [#19](https://github.com/zendframework/zend-dom/pull/19) adds support for PHP versions 7.1 and 7.2. - Adds documentation and publishes it to https://docs.zendframework.com/zend-dom/ @@ -45,12 +47,12 @@ All notable changes to this project will be documented in this file, in reverse ### Removed -- [#13](https://github.com/zendframework/zend-dom/pull/4) and - [#19](https://github.com/zendframework/zend-dom/pull/4) remove support for PHP +- [#13](https://github.com/zendframework/zend-dom/pull/13) and + [#19](https://github.com/zendframework/zend-dom/pull/19) remove support for PHP versions prior to 5.6. -- [#13](https://github.com/zendframework/zend-dom/pull/4) and - [#19](https://github.com/zendframework/zend-dom/pull/4) remove support for HHVM. +- [#13](https://github.com/zendframework/zend-dom/pull/13) and + [#19](https://github.com/zendframework/zend-dom/pull/19) remove support for HHVM. ### Fixed @@ -60,7 +62,7 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- [#2](https://github.com/zendframework/zend-dom/pull/4) ads context node +- [#2](https://github.com/zendframework/zend-dom/pull/2) adds context node support for DOMXPath->query that supports querying in the context of a specific node. diff --git a/src/Document/Query.php b/src/Document/Query.php index 7b40a8e..7b2fc8c 100644 --- a/src/Document/Query.php +++ b/src/Document/Query.php @@ -88,9 +88,9 @@ public static function cssToXpath($path) // Arbitrary attribute value contains whitespace $path = preg_replace_callback( - '/\[\S+["\'](.+)["\']\]/', + '/\[\S+?([\'"])((?:(?!\1)[^\\\]|\\.)*)\1\]/', function ($matches) { - return str_replace($matches[1], preg_replace('/\s+/', '\s', $matches[1]), $matches[0]); + return str_replace($matches[2], preg_replace('/\s+/', '\s', $matches[2]), $matches[0]); }, $path ); @@ -147,29 +147,29 @@ protected static function _tokenize($expression) // arbitrary attribute strict equality $expression = preg_replace_callback( - '|\[@?([a-z0-9_-]+)=[\'"]([^\'"]+)[\'"]\]|i', + '/\[@?([a-z0-9_-]+)=([\'"])((?:(?!\2)[^\\\]|\\.)*)\2\]/i', function ($matches) { - return '[@' . strtolower($matches[1]) . "='" . $matches[2] . "']"; + return sprintf("[@%s='%s']", strtolower($matches[1]), str_replace("'", "\\'", $matches[3])); }, $expression ); // arbitrary attribute contains full word $expression = preg_replace_callback( - '|\[([a-z0-9_-]+)~=[\'"]([^\'"]+)[\'"]\]|i', + '/\[([a-z0-9_-]+)~=([\'"])((?:(?!\2)[^\\\]|\\.)*)\2\]/i', function ($matches) { return "[contains(concat(' ', normalize-space(@" . strtolower($matches[1]) . "), ' '), ' " - . $matches[2] . " ')]"; + . $matches[3] . " ')]"; }, $expression ); // arbitrary attribute contains specified content $expression = preg_replace_callback( - '|\[([a-z0-9_-]+)\*=[\'"]([^\'"]+)[\'"]\]|i', + '/\[([a-z0-9_-]+)\*=([\'"])((?:(?!\2)[^\\\]|\\.)*)\2\]/i', function ($matches) { return "[contains(@" . strtolower($matches[1]) . ", '" - . $matches[2] . "')]"; + . $matches[3] . "')]"; }, $expression ); diff --git a/test/Document/QueryTest.php b/test/Document/QueryTest.php index afcc7ca..7683bf4 100644 --- a/test/Document/QueryTest.php +++ b/test/Document/QueryTest.php @@ -172,4 +172,34 @@ public function testCanTransformWithAttributeAndDot() $test = Query::cssToXpath('a[@href="http://example.com"]'); $this->assertEquals("//a[@href='http://example.com']", $test); } + + public function nestedAttributeSelectors() + { + return [ + 'with-double-quotes' => [ + 'select[name="foo"] option[selected="selected"]', + "//select[@name='foo']//option[@selected='selected']", + ], + 'with-single-quotes' => [ + "select[name='foo'] option[selected='selected']", + "//select[@name='foo']//option[@selected='selected']", + ], + 'double-quotes-containing-single-quotes' => [ + "select[name=\"f'oo\"] option[selected=\"sel'ected\"]", + "//select[@name='f\'oo']//option[@selected='sel\'ected']", + ], + 'single-quotes-containing-double-quotes' => [ + "select[name='f\"oo'] option[selected='sel\"ected']", + "//select[@name='f\"oo']//option[@selected='sel\"ected']", + ], + ]; + } + + /** + * @dataProvider nestedAttributeSelectors + */ + public function testTransformNestedAttributeSelectors($selector, $expectedXpath) + { + $this->assertEquals($expectedXpath, Query::cssToXpath($selector)); + } }