فهرست منبع

ZF-9764, ZF-9765: fix CSS -> XPath transforms

- Find all attribute identity selectors
- Find all attribute word selectors
- Remove double asterix

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@22044 44c647ce-9c0f-0410-b52a-842ac1e357ba
matthew 15 سال پیش
والد
کامیت
563262bed7
3فایلهای تغییر یافته به همراه118 افزوده شده و 88 حذف شده
  1. 62 35
      library/Zend/Dom/Query/Css2Xpath.php
  2. 28 53
      tests/Zend/Dom/Query/Css2XpathTest.php
  3. 28 0
      tests/Zend/Dom/QueryTest.php

+ 62 - 35
library/Zend/Dom/Query/Css2Xpath.php

@@ -58,16 +58,16 @@ class Zend_Dom_Query_Css2Xpath
         foreach ($segments as $key => $segment) {
             $pathSegment = self::_tokenize($segment);
             if (0 == $key) {
-                if (0 === strpos($pathSegment, '[contains(@class')) {
-                    $paths[0] .= '*' . $pathSegment;
+                if (0 === strpos($pathSegment, '[contains(')) {
+                    $paths[0] .= '*' . ltrim($pathSegment, '*');
                 } else {
                     $paths[0] .= $pathSegment;
                 }
                 continue;
             }
-            if (0 === strpos($pathSegment, '[contains(@class')) {
+            if (0 === strpos($pathSegment, '[contains(')) {
                 foreach ($paths as $key => $xpath) {
-                    $paths[$key] .= '//*' . $pathSegment;
+                    $paths[$key] .= '//*' . ltrim($pathSegment, '*');
                     $paths[]      = $xpath . $pathSegment;
                 }
             } else {
@@ -99,44 +99,71 @@ class Zend_Dom_Query_Css2Xpath
         $expression = preg_replace('|(?<![a-z0-9_-])(\[@id=)|i', '*$1', $expression);
 
         // arbitrary attribute strict equality
-        if (preg_match('|([a-z]+)\[([a-z0-9_-]+)=[\'"]([^\'"]+)[\'"]\]|i', $expression)) {
-            $expression = preg_replace_callback(
-                '|([a-z]+)\[([a-z0-9_-]+)=[\'"]([^\'"]+)[\'"]\]|i',
-                create_function(
-                    '$matches',
-                    'return $matches[1] . "[@" . strtolower($matches[2]) . "=\'" . $matches[3] . "\']";'
-                ),
-                $expression
-            );
-        }
+        $expression = preg_replace_callback(
+            '|\[([a-z0-9_-]+)=[\'"]([^\'"]+)[\'"]\]|i',
+            array(__CLASS__, '_createEqualityExpression'),
+            $expression
+        );
 
         // arbitrary attribute contains full word
-        if (preg_match('|([a-z]+)\[([a-z0-9_-]+)~=[\'"]([^\'"]+)[\'"]\]|i', $expression)) {
-            $expression = preg_replace_callback(
-                '|([a-z]+)\[([a-z0-9_-]+)~=[\'"]([^\'"]+)[\'"]\]|i',
-                create_function(
-                    '$matches',
-                    'return $matches[1] . "[contains(@" . strtolower($matches[2]) . ", \' $matches[3] \')]";'
-                ),
-                $expression
-            );
-        }
+        $expression = preg_replace_callback(
+            '|\[([a-z0-9_-]+)~=[\'"]([^\'"]+)[\'"]\]|i',
+            array(__CLASS__, '_normalizeSpaceAttribute'),
+            $expression
+        );
 
         // arbitrary attribute contains specified content
-        if (preg_match('|([a-z]+)\[([a-z0-9_-]+)\*=[\'"]([^\'"]+)[\'"]\]|i', $expression)) {
-            $expression = preg_replace_callback(
-                '|([a-z]+)\[([a-z0-9_-]+)\*=[\'"]([^\'"]+)[\'"]\]|i',
-                create_function(
-                    '$matches',
-                    'return $matches[1] . "[contains(@" . strtolower($matches[2]) . ", \'" . $matches[3] . "\')]";'
-                ),
-                $expression
-            );
-        }
+        $expression = preg_replace_callback(
+            '|\[([a-z0-9_-]+)\*=[\'"]([^\'"]+)[\'"]\]|i',
+            array(__CLASS__, '_createContainsExpression'),
+            $expression
+        );
 
         // Classes
-        $expression = preg_replace('|\.([a-z][a-z0-9_-]*)|i', "[contains(@class, ' \$1 ')]", $expression);
+        $expression = preg_replace(
+            '|\.([a-z][a-z0-9_-]*)|i', 
+            "[contains(concat(' ', normalize-space(@class), ' '), ' \$1 ')]", 
+            $expression
+        );
+
+        /** ZF-9764 -- remove double asterix */
+        $expression = str_replace('**', '*', $expression);
 
         return $expression;
     }
+
+    /**
+     * Callback for creating equality expressions
+     * 
+     * @param  array $matches 
+     * @return string
+     */
+    protected static function _createEqualityExpression($matches)
+    {
+        return '[@' . strtolower($matches[1]) . "='" . $matches[2] . "']";
+    }
+
+    /**
+     * Callback for creating expressions to match one or more attribute values
+     * 
+     * @param  array $matches 
+     * @return string
+     */
+    protected static function _normalizeSpaceAttribute($matches)
+    {
+        return "[contains(concat(' ', normalize-space(@" . strtolower($matches[1]) . "), ' '), ' " 
+             . $matches[2] . " ')]";
+    }
+
+    /**
+     * Callback for creating a strict "contains" expression
+     * 
+     * @param  array $matches 
+     * @return string
+     */
+    protected static function _createContainsExpression($matches)
+    {
+        return "[contains(@" . strtolower($matches[1]) . ", '" 
+             . $matches[2] . "')]";
+    }
 }

+ 28 - 53
tests/Zend/Dom/Query/Css2XpathTest.php

@@ -20,18 +20,11 @@
  * @version    $Id$
  */
 
-// Call Zend_Dom_Query_Css2XpathTest::main() if this source file is executed directly.
-if (!defined("PHPUnit_MAIN_METHOD")) {
-    define("PHPUnit_MAIN_METHOD", "Zend_Dom_Query_Css2XpathTest::main");
-}
-
 require_once dirname(__FILE__) . '/../../../TestHelper.php';
-
-/** Zend_Dom_Query_Css2Xpath */
 require_once 'Zend/Dom/Query/Css2Xpath.php';
 
 /**
- * Test class for Zend_Dom_Query_Css2Xpath.
+ * Test class for Css2Xpath.
  *
  * @category   Zend
  * @package    Zend_Dom
@@ -42,37 +35,6 @@ require_once 'Zend/Dom/Query/Css2Xpath.php';
  */
 class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
 {
-    /**
-     * Runs the test methods of this class.
-     *
-     * @return void
-     */
-    public static function main()
-    {
-        $suite  = new PHPUnit_Framework_TestSuite("Zend_Dom_Query_Css2XpathTest");
-        $result = PHPUnit_TextUI_TestRunner::run($suite);
-    }
-
-    /**
-     * Sets up the fixture, for example, open a network connection.
-     * This method is called before a test is executed.
-     *
-     * @return void
-     */
-    public function setUp()
-    {
-    }
-
-    /**
-     * Tears down the fixture, for example, close a network connection.
-     * This method is called after a test is executed.
-     *
-     * @return void
-     */
-    public function tearDown()
-    {
-    }
-
     public function testTransformShouldBeCalledStatically()
     {
         Zend_Dom_Query_Css2Xpath::transform('');
@@ -104,7 +66,7 @@ class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
     public function testTransformShouldRecognizeDotSymbolAsClass()
     {
         $test = Zend_Dom_Query_Css2Xpath::transform('.foo');
-        $this->assertEquals("//*[contains(@class, ' foo ')]", $test);
+        $this->assertEquals("//*[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", $test);
     }
 
     public function testTransformShouldAssumeSpacesToIndicateRelativeXpathQueries()
@@ -112,8 +74,8 @@ class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
         $test = Zend_Dom_Query_Css2Xpath::transform('div#foo .bar');
         $this->assertContains('|', $test);
         $expected = array(
-            "//div[@id='foo']//*[contains(@class, ' bar ')]",
-            "//div[@id='foo'][contains(@class, ' bar ')]",
+            "//div[@id='foo']//*[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]",
+            "//div[@id='foo'][contains(concat(' ', normalize-space(@class), ' '), ' bar ')]",
         );
         foreach ($expected as $path) {
             $this->assertContains($path, $test);
@@ -136,8 +98,8 @@ class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
         $this->assertContains('|', $test);
         $actual   = explode('|', $test);
         $expected = array(
-            "//div[@id='foo']//span[contains(@class, ' bar ')]",
-            "//*[@id='bar']//li[contains(@class, ' baz ')]//a",
+            "//div[@id='foo']//span[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]",
+            "//*[@id='bar']//li[contains(concat(' ', normalize-space(@class), ' '), ' baz ')]//a",
         );
         $this->assertEquals(count($expected), count($actual));
         foreach ($actual as $path) {
@@ -150,10 +112,10 @@ class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
         $test = Zend_Dom_Query_Css2Xpath::transform('div.foo .bar a .baz span');
         $this->assertContains('|', $test);
         $segments = array(
-            "//div[contains(@class, ' foo ')]//*[contains(@class, ' bar ')]//a//*[contains(@class, ' baz ')]//span",
-            "//div[contains(@class, ' foo ')]//*[contains(@class, ' bar ')]//a[contains(@class, ' baz ')]//span",
-            "//div[contains(@class, ' foo ')][contains(@class, ' bar ')]//a//*[contains(@class, ' baz ')]//span",
-            "//div[contains(@class, ' foo ')][contains(@class, ' bar ')]//a[contains(@class, ' baz ')]//span",
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]//*[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]//a//*[contains(concat(' ', normalize-space(@class), ' '), ' baz ')]//span",
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]//*[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]//a[contains(concat(' ', normalize-space(@class), ' '), ' baz ')]//span",
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' foo ')][contains(concat(' ', normalize-space(@class), ' '), ' bar ')]//a//*[contains(concat(' ', normalize-space(@class), ' '), ' baz ')]//span",
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' foo ')][contains(concat(' ', normalize-space(@class), ' '), ' bar ')]//a[contains(concat(' ', normalize-space(@class), ' '), ' baz ')]//span",
         );
         foreach ($segments as $xpath) {
             $this->assertContains($xpath, $test);
@@ -175,7 +137,7 @@ class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
     public function testShouldAllowContentSubSelectionOfArbitraryAttributes()
     {
         $test = Zend_Dom_Query_Css2Xpath::transform('div[foo~="bar"]');
-        $this->assertEquals("//div[contains(@foo, ' bar ')]", $test);
+        $this->assertEquals("//div[contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]", $test);
     }
 
     public function testShouldAllowContentMatchingOfArbitraryAttributes()
@@ -201,9 +163,22 @@ class Zend_Dom_Query_Css2XpathTest extends PHPUnit_Framework_TestCase
         $test = Zend_Dom_Query_Css2Xpath::transform('child > leaf');
         $this->assertEquals("//child/leaf", $test);
     }
-}
 
-// Call Zend_Dom_Query_Css2XpathTest::main() if this source file is executed directly.
-if (PHPUnit_MAIN_METHOD == "Zend_Dom_Query_Css2XpathTest::main") {
-    Zend_Dom_Query_Css2XpathTest::main();
+    /**
+     * @group ZF-9764
+     */
+    public function testIdSelectorWithAttribute()
+    {
+        $test = Zend_Dom_Query_Css2Xpath::transform('#id[attribute="value"]');
+        $this->assertEquals("//*[@id='id'][@attribute='value']", $test);
+    }
+
+    /**
+     * @group ZF-9764
+     */
+    public function testIdSelectorWithLeadingAsterix()
+    {
+        $test = Zend_Dom_Query_Css2Xpath::transform('*#id');
+        $this->assertEquals("//*[@id='id']", $test);
+    }
 }

+ 28 - 0
tests/Zend/Dom/QueryTest.php

@@ -243,6 +243,34 @@ class Zend_Dom_QueryTest extends PHPUnit_Framework_TestCase
         $this->assertTrue(is_array($errors));
         $this->assertTrue(0 < count($errors));
     }
+    
+    /**
+     * @group ZF-9765
+     */
+    public function testCssSelectorShouldFindNodesWhenMatchingMultipleAttributes()
+    {
+        $html = <<<EOF
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<body>
+  <form action="#" method="get">
+    <input type="hidden" name="foo" value="1" id="foo"/>
+    <input type="hidden" name="bar" value="0" id="bar"/>
+    <input type="hidden" name="baz" value="1" id="baz"/>
+  </form>
+</body>
+</html>
+EOF;
+
+        $this->query->setDocument($html);
+        $results = $this->query->query('input[type="hidden"][value="1"]');
+        $this->assertEquals(2, count($results), $results->getXpathQuery());
+        $results = $this->query->query('input[value="1"][type~="hidden"]');
+        $this->assertEquals(2, count($results), $results->getXpathQuery());
+        $results = $this->query->query('input[type="hidden"][value="0"]');
+        $this->assertEquals(1, count($results));
+    }
 }
 
 // Call Zend_Dom_QueryTest::main() if this source file is executed directly.