99
1010namespace Inphinit \Experimental \Dom ;
1111
12- class Query extends \DOMXPath
12+ class Selector extends \DOMXPath
1313{
1414 private $ prevent ;
15- private $ restore ;
1615 private $ rules ;
1716 private $ qxs = array (
1817 array ( '/^([^a-z*])/ ' , '* \\1 ' ),
19- array ( '/([>\+~])([^a-z])/ ' , '\\1* \\2 ' ),
18+ array ( '/([>\+~])([^a-z* ])/ ' , '\\1* \\2 ' ),
2019 array ( '/\[(.*?)\]/ ' , '[@ \\1] ' ),
2120 array ( '/#(\w+)/ ' , '[@id=" \\1"] ' ),
2221 array ( '/\:empty/ ' , '[count(*)=0] ' ),
2322 array ( '/\:last-child/ ' , '[last()] ' ),
2423 array ( '/^(.*?)\:first-child $/ ' , 'descendant:: \\1 ' ),
25- array ( '/\:nth-child\(n\)/ ' , '[position() mod n = 1] ' ),
26- array ( '/\:nth-child\(odd\)/ ' , '[(position() mod 2)=1] ' ),
27- array ( '/\:nth-child\(even\)/ ' , '[(position() mod 2)=0] ' ),
28- array ( '/\:nth-child\((\d+)n\)/ ' , '[(position() mod \\1)=0] ' ),
29- array ( '/\:nth-child\((\d+)n\+(\d+)\)/ ' , '[(position() mod \\1)= \\2] ' ),
30- array ( '/\:lang\(([\w\-]+)\)/ ' , '[starts-with(concat(@lang,"-"),concat(" \\1","-"))] ' ),
31- array ( '/\[(@\w+)\^=([^" \'])(.*?) \\1\]/ ' , '[starts-with( \\1, \\2 \\3 \\2)] ' ),
32- array ( '/\[(@\w+)\*=([^" \'])(.*?) \\1\]/ ' , '[contains( \\1, \\2 \\3 \\2)] ' ),
33- array ( '/\[(@\w+)\~=([^" \'])(.*?) \\1\]/ ' , '[contains(concat(" ", \\1," "), \\2 \\3 \\2)] ' ),
34- array ( '/\[(@\w+)\|=([^" \'])(.*?) \\1\]/ ' , '[starts-with(concat( \\1,"-"),concat( \\2 \\3 \\2,"-"))] ' ),
35- array ( '/\[(@\w+)\$=([^" \'])(.*?) \\1\]/ ' , '[substring( \\1,string-length( \\1)-2)= \\2 \\3 \\2] ' ),
36- array ( '/\.(\w+)/ ' , '[contains(concat(" ",@class," ")," \\1 ")] ' ),
24+ array ( '/\:nth-(last-)?(child|of-type)\(n\)/ ' , '' ),
25+ array ( '/\:nth-child\(odd\)/ ' , ':nth-child(2n+1) ' ),
26+ array ( '/\:nth-child\(even\)/ ' , ':nth-child(2n) ' ),
27+ array ( '/\:nth-child\((\d+)\)/ ' , '[position() mod \\1 = 1] ' ),
28+ array ( '/\:nth-child\((\d+)n\)/ ' , '[position() mod \\1 = 0] ' ),
29+ array ( '/\:nth-child\((\d+)n\+(\d+)\)/ ' , '[position() mod \\1 = \\2] ' ),
30+ array ( '/\:nth-last-child\(odd\)/ ' , ':nth-last-child(2n+1) ' ),
31+ array ( '/\:nth-last-child\(even\)/ ' , ':nth-last-child(2n) ' ),
32+ array ( '/\:nth-last-child\((\d+)\)/ ' , '[(count() - position()) mod \\1 = 1] ' ),
33+ array ( '/\:nth-last-child\((\d+)n\)/ ' , '[(count() - position()) mod \\1 = 0] ' ),
34+ array ( '/\:nth-last-child\((\d+)n\+(\d+)\)/ ' , '[(count() - position()) mod \\1 = \\2] ' ),
35+ array ( '/\.(\w+)/ ' , '[@class~=" \\1"] ' ),
36+ array ( '/\:lang\(([\w\-]+)\)/ ' , '[@lang|=" \\1"] ' ),
37+ array ( '/\[(@\w+)\^=([" \'])(.*?) \\2\]/ ' , '[starts-with( \\1, \\2 \\3 \\2)] ' ),
38+ array ( '/\[(@\w+)\*=([" \'])(.*?) \\2\]/ ' , '[contains( \\1, \\2 \\3 \\2)] ' ),
39+ array ( '/\[(@\w+)\~=([" \'])(.*?) \\2\]/ ' , '[contains(concat(" ", \\1," "), \\2 \\3 \\2)] ' ),
40+ array ( '/\[(@\w+)\|=([" \'])(.*?) \\2\]/ ' , '[starts-with(concat( \\1,"-"),concat( \\2 \\3 \\2,"-"))] ' ),
41+ array ( '/\[(@\w+)\$=([" \'])(.*?) \\2\]/ ' , '[substring( \\1,string-length( \\1)-2)= \\2 \\3 \\2] ' ),
3742 array ( '/\:contains\((.*?)\)/ ' , '[contains(text(), \\1)] ' ),
43+ array ( '/[~]([*\w]+)/ ' , '/following-sibling::*[count( \\1)] ' ),
3844 array ( '/\+/ ' , '/following-sibling:: ' ),
39- array ( '/[~]([*\w]{1,})/ ' , '/following-sibling::*[count( \\1)] ' ),
4045 array ( '/[>]/ ' , '/ ' )
4146 );
4247
43- /**
44- * Count all nodes matching the given XPath expression
45- *
46- * @param string $selector
47- * @param \DOMNode $context
48- * @param bool $registerNodeNS
49- * @throws \Inphinit\Experimental\Exception
50- * @return \DOMNodeList
51- */
52- public function evaluate ($ selector , \DOMNode $ context = null , $ registerNodeNS = true )
53- {
54- if (PHP_VERSION_ID >= 50303 ) {
55- return parent ::evaluate ($ selector , $ context , $ registerNodeNS );
56- } else {
57- return parent ::evaluate ($ selector , $ context );
58- }
59- }
60-
61- /**
62- * Returns a DOMNodeList containing all nodes matching the given XPath expression
63- *
64- * @param string $selector
65- * @param \DOMNode $context
66- * @param bool $registerNodeNS
67- * @return \DOMNodeList
68- */
69- public function query ($ selector , \DOMNode $ context = null , $ registerNodeNS = true )
70- {
71- if (PHP_VERSION_ID >= 50303 ) {
72- return parent ::query ($ selector , $ context , $ registerNodeNS );
73- } else {
74- return parent ::query ($ selector , $ context );
75- }
76- }
77-
7848 /**
7949 * Count all nodes matching the given CSS selector
8050 *
@@ -85,11 +55,7 @@ public function query($selector, \DOMNode $context = null, $registerNodeNS = tru
8555 */
8656 public function count ($ selector , \DOMNode $ context = null , $ registerNodeNS = true )
8757 {
88- $ this ->boot ($ selector );
89-
90- $ selector = $ this ->toXPath ($ selector );
91-
92- return self ::evaluate ($ selector , $ context , $ registerNodeNS );
58+ return $ this ->exec ('evaluate ' , $ selector , $ context , $ registerNodeNS );
9359 }
9460
9561 /**
@@ -98,19 +64,14 @@ public function count($selector, \DOMNode $context = null, $registerNodeNS = tru
9864 * @param string $selector
9965 * @param \DOMNode $context
10066 * @param bool $registerNodeNS
101- * @throws \Inphinit\Experimental\Exception
10267 * @return \DOMNodeList
10368 */
10469 public function get ($ selector , \DOMNode $ context = null , $ registerNodeNS = true )
10570 {
106- $ this ->boot ($ selector );
107-
108- $ selector = $ this ->toXPath ($ selector );
109-
110- return self ::query ($ selector , $ context , $ registerNodeNS );
71+ return $ this ->exec ('query ' , $ selector , $ context , $ registerNodeNS );
11172 }
11273
113- private function boot (& $ query )
74+ private function exec ( $ method , $ query, $ context , $ registerNodeNS )
11475 {
11576 $ dot = self ::uniqueToken ($ query , 'dot ' );
11677 $ hash = self ::uniqueToken ($ query , 'hash ' );
@@ -138,26 +99,36 @@ private function boot(&$query)
13899 ') ' => $ rparenthesis
139100 );
140101
141- $ this ->restore = array_combine (array_values ($ this ->prevent ), array_keys ($ this ->prevent ));
142-
143102 $ this ->rules = array (
144103 ' ' => $ spaces ,
145104 ', ' => $ commas
146105 );
147106
148- $ query = preg_replace ('#\[(\w+)(.)?[=]([^" \'])(.*?)\]# ' , '[$1$2="$4"] ' , $ query );
149-
150- $ query = preg_replace_callback ('#([" \'])(?: \\.|[^ \\\\])*?\1# ' , array ($ this , 'preventInQuotes ' ), $ query );
107+ $ query = $ this ->toXPath ($ query );
151108
152- $ query = preg_replace_callback ('#:contains\(((?: \\.|[^ \\\\])*?)\)# ' , array ($ this , 'preventInContains ' ), $ query );
109+ if (PHP_VERSION_ID >= 50303 ) {
110+ return $ this ->$ method ($ query , $ context , $ registerNodeNS );
111+ } else if ($ context !== null ) {
112+ return $ this ->$ method ($ query , $ context );
113+ }
153114
154- $ query = preg_replace ( ' #(\s+)?([>+~\s])(\s+)?# ' , '\\ 2 ' , $ query );
115+ return $ this -> $ method ( $ query );
155116 }
156117
157118 private function toXPath ($ query )
158119 {
120+ $ query = preg_replace_callback ('#\[(\w+)(.)?[=]([^" \'])(.*?)\]# ' , array ($ this , 'putQuotes ' ), $ query );
121+
122+ $ query = preg_replace_callback ('#\:contains\(([^" \'])(.*?)\)# ' , array ($ this , 'putQuotes ' ), $ query );
123+
124+ $ query = preg_replace_callback ('#([" \'])(?: \\\\.|[^ \\\\])*? \\1# ' , array ($ this , 'preventInQuotes ' ), $ query );
125+
126+ $ query = preg_replace ('#(\s+)?([>+~\s])(\s+)?# ' , '\\2 ' , $ query );
127+
159128 $ queries = explode (', ' , $ query );
160129
130+ $ restore = array_combine (array_values ($ this ->prevent ), array_keys ($ this ->prevent ));
131+
161132 foreach ($ queries as &$ descendants ) {
162133
163134 $ descendants = explode (' ' , trim ($ descendants ));
@@ -167,6 +138,8 @@ private function toXPath($query)
167138 $ r = strtr ($ qx [1 ], $ this ->rules );
168139
169140 $ descendant = trim (preg_replace ($ qx [0 ], $ r , $ descendant ));
141+
142+ var_dump ( strtr ($ descendant , $ restore ) . ' => ' . $ qx [0 ] );
170143 }
171144 }
172145
@@ -177,21 +150,38 @@ private function toXPath($query)
177150
178151 $ queries = null ;
179152
180- return '// ' . strtr ($ query , $ this ->restore );
153+ $ restore = array_combine (array_values ($ this ->prevent ), array_keys ($ this ->prevent ));
154+
155+ return '// ' . strtr ($ query , $ restore );
181156 }
182157
183- private function preventInQuotes ($ arg )
158+ private function putQuotes ($ arg )
184159 {
185- return strtr ($ arg [0 ], $ this ->prevent );
160+ if (stripos ($ arg [0 ], ':contains ' ) === 0 ) {
161+ return ':contains(" ' . addcslashes ($ arg [1 ] . $ arg [2 ], '" ' ) . '") ' ;
162+ }
163+
164+ return sprintf ('[%s%s="%s"] ' , $ arg [1 ], $ arg [2 ], addcslashes ($ arg [3 ] . $ arg [4 ], '" ' ));
186165 }
187166
188- private function preventInContains ($ arg )
167+ private function preventInQuotes ($ arg )
189168 {
190- if ($ arg [1 ]{0 } === '\'' || $ arg [1 ]{0 } === '" ' ) {
191- return strtr (addcslashes ($ arg [0 ], '\\" ' ), $ this ->prevent );
169+ $ quote = $ arg [1 ];
170+ $ inValue = substr ($ arg [0 ], 1 , -1 );
171+
172+ if (strpos ($ inValue , $ quote ) === false ) {
173+ return strtr ($ arg [0 ], $ this ->prevent );
192174 }
193175
194- return ':contains(" ' . strtr ($ arg [1 ], $ this ->prevent ) . '") ' ;
176+ $ quotec = $ quote === '" ' ? '\'' : '" ' ;
177+ $ unes = '\\' . $ quote ;
178+ $ glue = $ quote . ', ' . $ quotec . $ quote . $ quotec . ', ' . $ quote ;
179+
180+ $ inValue = str_replace ($ unes , $ quote , $ inValue );
181+
182+ $ inValue = 'concat( ' . $ quote . implode ($ glue , explode ($ quote , $ inValue )) . $ quote . ') ' ;
183+
184+ return strtr ($ inValue , $ this ->prevent );
195185 }
196186
197187 private static function uniqueToken ($ query , $ key , $ token = null )
0 commit comments