├── .gitignore ├── LICENSE ├── README.md └── src ├── jsonselect.php └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | JSONSelectTests/ 2 | assets/ 3 | protected/runtime/ 4 | themes/classic/views/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michiel van Vlaardingen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSONselect-php 2 | ============== 3 | 4 | Port of JSONselect to PHP, for more information check http://jsonselect.org/ 5 | 6 | 7 | This implementation is heavily based on https://github.com/lloyd/JSONSelect/blob/master/src/jsonselect.js 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/jsonselect.php: -------------------------------------------------------------------------------- 1 | sel = $this->parse($expr); 19 | 20 | } 21 | 22 | // emitted error codes. 23 | var $errorCodes = array( 24 | "bop" => "binary operator expected", 25 | "ee" => "expression expected", 26 | "epex"=> "closing paren expected ')'", 27 | "ijs" => "invalid json string", 28 | "mcp" => "missing closing paren", 29 | "mepf"=> "malformed expression in pseudo-function", 30 | "mexp"=> "multiple expressions not allowed", 31 | "mpc" => "multiple pseudo classes (:xxx) not allowed", 32 | "nmi" => "multiple ids not allowed", 33 | "pex" => "opening paren expected '('", 34 | "se" => "selector expected", 35 | "sex" => "string expected", 36 | "sra" => "string required after '.'", 37 | "uc" => "unrecognized char", 38 | "ucp" => "unexpected closing paren", 39 | "ujs" => "unclosed json string", 40 | "upc" => "unrecognized pseudo class" 41 | ); 42 | 43 | // throw an error message 44 | function te($ec, $context) { 45 | throw new Exception($this->errorCodes[$ec] . (" in '" . $context . "'")); 46 | } 47 | 48 | // THE LEXER 49 | var $toks = array( 50 | 'psc' => 1, // pseudo class 51 | 'psf' => 2, // pseudo class function 52 | 'typ' => 3, // type 53 | 'str' => 4, // string 54 | 'ide' => 5 // identifiers (or "classes", stuff after a dot) 55 | ); 56 | 57 | // The primary lexing regular expression in jsonselect 58 | function pat(){ 59 | return "/^(?:" . 60 | // (1) whitespace 61 | "([\\r\\n\\t\\ ]+)|" . 62 | // (2) one-char ops 63 | "([~*,>\\)\\(])|" . 64 | // (3) types names 65 | "(string|boolean|null|array|object|number)|" . 66 | // (4) pseudo classes 67 | "(:(?:root|first-child|last-child|only-child))|" . 68 | // (5) pseudo functions 69 | "(:(?:nth-child|nth-last-child|has|expr|val|contains))|" . 70 | // (6) bogusly named pseudo something or others 71 | "(:\\w+)|" . 72 | // (7 & 8) identifiers and JSON strings 73 | "(?:(\\.)?(\\\"(?:[^\\\\\\\"]|\\\\[^\\\"])*\\\"))|" . 74 | // (8) bogus JSON strings missing a trailing quote 75 | "(\\\")|" . 76 | // (9) identifiers (unquoted) 77 | "\\.((?:[_a-zA-Z]|[^\\0-\\0177]|\\\\[^\\r\\n\\f0-9a-fA-F])(?:[\$_a-zA-Z0-9\\-]|[^\\x{0000}-\\x{0177}]|(?:\\\\[^\\r\\n\\f0-9a-fA-F]))*)" . 78 | ")/u"; 79 | } 80 | 81 | // A regular expression for matching "nth expressions" (see grammar, what :nth-child() eats) 82 | var $nthPat = '/^\s*\(\s*(?:([+\-]?)([0-9]*)n\s*(?:([+\-])\s*([0-9]))?|(odd|even)|([+\-]?[0-9]+))\s*\)/'; 83 | 84 | function lex($str, $off) { 85 | if (!$off) $off = 0; 86 | //var m = pat.exec(str.substr(off)); 87 | preg_match($this->pat(), substr($str, $off), $m); 88 | 89 | //echo "lex from $off ".print_r($m,true)."\n"; 90 | 91 | if (!$m) return null; 92 | $off+=strlen($m[0]); 93 | 94 | $a = null; 95 | if (($m[1])) $a = array($off, " "); 96 | else if (($m[2])) $a = array($off, $m[0]); 97 | else if (($m[3])) $a = array($off, $this->toks['typ'], $m[0]); 98 | else if (($m[4])) $a = array($off, $this->toks['psc'], $m[0]); 99 | else if (($m[5])) $a = array($off, $this->toks['psf'], $m[0]); 100 | else if (($m[6])) $this->te("upc", $str); 101 | else if (($m[8])) $a = array($off, $m[7] ? $this->toks['ide'] : $this->toks['str'], json_decode($m[8])); 102 | else if (($m[9])) $this->te("ujs", $str); 103 | else if (($m[10])) $a = array($off, $this->toks['ide'], preg_replace('/\\\\([^\r\n\f0-9a-fA-F])/','$1',$m[10])); 104 | return $a; 105 | } 106 | 107 | // THE EXPRESSION SUBSYSTEM 108 | 109 | function exprPat() { 110 | return 111 | // skip and don't capture leading whitespace 112 | "/^\\s*(?:" . 113 | // (1) simple vals 114 | "(true|false|null)|" . 115 | // (2) numbers 116 | "(-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)|" . 117 | // (3) strings 118 | "(\"(?:[^\\]|\\[^\"])*\")|" . 119 | // (4) the 'x' value placeholder 120 | "(x)|" . 121 | // (5) binops 122 | "(&&|\\|\\||[\\$\\^<>!\\*]=|[=+\\-*\\/%<>])|" . 123 | // (6) parens 124 | "([\\(\\)])" . 125 | ")/"; 126 | } 127 | 128 | function operator($op,$ix){ 129 | 130 | $operators = array( 131 | '*' => array( 9, function($lhs, $rhs) { return $lhs * $rhs; } ), 132 | '/' => array( 9, function($lhs, $rhs) { return $lhs / $rhs; } ), 133 | '%' => array( 9, function($lhs, $rhs) { return $lhs % $rhs; } ), 134 | '+' => array( 7, function($lhs, $rhs) { return $lhs + $rhs; } ), 135 | '-' => array( 7, function($lhs, $rhs) { return $lhs - $rhs; } ), 136 | '<=' => array( 5, function($lhs, $rhs) { return is_numeric($lhs) && is_numeric($rhs) && $lhs <= $rhs || is_string($lhs) && is_string($rhs) && strcmp($lhs, $rhs) <= 0; } ), 137 | '>=' => array( 5, function($lhs, $rhs) { return is_numeric($lhs) && is_numeric($rhs) && $lhs >= $rhs || is_string($lhs) && is_string($rhs) && strcmp($lhs,$rhs) >= 0; } ), 138 | '$=' => array( 5, function($lhs, $rhs) { return is_string($lhs) && is_string($rhs) && strrpos($lhs, $rhs) === strlen($lhs) - strlen($rhs); } ), 139 | '^=' => array( 5, function($lhs, $rhs) { return is_string($lhs) && is_string($rhs) && strpos($lhs, $rhs) === 0; } ), 140 | '*=' => array( 5, function($lhs, $rhs) { return is_string($lhs) && is_string($rhs) && strpos($lhs, $rhs) !== false; } ), 141 | '>' => array( 5, function($lhs, $rhs) { return is_numeric($lhs) && is_numeric($rhs) && $lhs > $rhs || is_string($lhs) && is_string($rhs) && strcmp($lhs,$rhs) > 0; } ), 142 | '<' => array( 5, function($lhs, $rhs) { return is_numeric($lhs) && is_numeric($rhs) && $lhs < $rhs || is_string($lhs) && is_string($rhs) && strcmp($lhs,$rhs) < 0; } ), 143 | '=' => array( 3, function($lhs, $rhs) { return $lhs === $rhs; } ), 144 | '!=' => array( 3, function($lhs, $rhs) { return $lhs !== $rhs; } ), 145 | '&&' => array( 2, function($lhs, $rhs) { return $lhs && $rhs; } ), 146 | '||' => array( 1, function($lhs, $rhs) { return $lhs || $rhs; }) 147 | ); 148 | return $operators[$op][$ix]; 149 | } 150 | 151 | function exprLex($str, $off) { 152 | //var v, m = exprPat.exec(str.substr(off)); 153 | $v = null; 154 | preg_match($this->exprPat(), substr($str, $off), $m); 155 | if ($m) { 156 | $off += strlen($m[0]); 157 | //$v = $m[1] || $m[2] || $m[3] || $m[5] || $m[6]; 158 | foreach(array(1,2,3,5,6) as $k){ 159 | if(strlen($m[$k])>0){ 160 | $v = $m[$k]; 161 | break; 162 | } 163 | } 164 | 165 | if (strlen($m[1]) || strlen($m[2]) || strlen($m[3])) return array($off, 0, json_decode($v)); 166 | else if (strlen($m[4])) return array($off, 0, VALUE_PLACEHOLDER); 167 | return array($off, $v); 168 | } 169 | } 170 | 171 | function exprParse2($str, $off) { 172 | if (!$off) $off = 0; 173 | // first we expect a value or a '(' 174 | $l = $this->exprLex($str, $off); 175 | //echo "exprLex ".print_r($l,true); 176 | $lhs=null; 177 | if ($l && $l[1] === '(') { 178 | $lhs = $this->exprParse2($str, $l[0]); 179 | $p = $this->exprLex($str, $lhs[0]); 180 | 181 | //echo "exprLex2 ".print_r($p,true); 182 | 183 | if (!$p || $p[1] !== ')') $this->te('epex', $str); 184 | $off = $p[0]; 185 | $lhs = [ '(', $lhs[1] ]; 186 | } else if (!$l || ($l[1] && $l[1] != 'x')) { 187 | $this->te("ee", $str . " - " . ( $l[1] && $l[1] )); 188 | } else { 189 | $lhs = (($l[1] === 'x') ? VALUE_PLACEHOLDER : $l[2]); 190 | $off = $l[0]; 191 | } 192 | 193 | // now we expect a binary operator or a ')' 194 | $op = $this->exprLex($str, $off); 195 | 196 | //echo "exprLex3 ".print_r($op,true); 197 | if (!$op || $op[1] == ')') return array($off, $lhs); 198 | else if ($op[1] == 'x' || !$op[1]) { 199 | $this->te('bop', $str . " - " . ( $op[1] && $op[1] )); 200 | } 201 | 202 | // tail recursion to fetch the rhs expression 203 | $rhs = $this->exprParse2($str, $op[0]); 204 | $off = $rhs[0]; 205 | $rhs = $rhs[1]; 206 | 207 | // and now precedence! how shall we put everything together? 208 | $v = null; 209 | if ((!is_object($rhs) && !is_array($rhs)) || $rhs[0] === '(' || $this->operator($op[1],0) < $this->operator($rhs[1],0) ) { 210 | $v = array($lhs, $op[1], $rhs); 211 | } 212 | else { 213 | // TODO: fix this, prob related due to $v copieeing $rhs instead of referencing 214 | //echo "re-arrange lhs:".print_r($lhs,true).' rhs: '.print_r($rhs,true); 215 | //print_r($rhs); 216 | 217 | $v = &$rhs; 218 | while (is_array($rhs[0]) && $rhs[0][0] != '(' && $this->operator($op[1],0) >= $this->operator($rhs[0][1],0)) { 219 | $rhs = &$rhs[0]; 220 | } 221 | $rhs[0] = array($lhs, $op[1], $rhs[0]); 222 | } 223 | return array($off, $v); 224 | } 225 | 226 | 227 | function deparen($v) { 228 | if ( (!is_object($v) && !is_array($v)) || $v === null) return $v; 229 | else if ($v[0] === '(') return $this->deparen($v[1]); 230 | else return array($this->deparen($v[0]), $v[1], $this->deparen($v[2])); 231 | } 232 | 233 | 234 | function exprParse($str, $off) { 235 | $e = $this->exprParse2($str, $off ? $off : 0); 236 | 237 | return array($e[0], $this->deparen($e[1])); 238 | } 239 | 240 | function exprEval($expr, $x) { 241 | if ($expr === VALUE_PLACEHOLDER) return $x; 242 | else if ($expr === null || (!is_object($expr) && !is_array($expr))) { 243 | return $expr; 244 | } 245 | $lhs = $this->exprEval($expr[0], $x); 246 | $rhs = $this->exprEval($expr[2], $x); 247 | $op = $this->operator($expr[1],1); 248 | 249 | return $op($lhs, $rhs); 250 | } 251 | 252 | // THE PARSER 253 | 254 | function parse($str, $off=0, $nested=null, $hints=null) { 255 | if (!$nested) $hints = array(); 256 | 257 | $a = array(); 258 | $am=null; 259 | $readParen=null; 260 | if (!$off) $off = 0; 261 | 262 | while (true) { 263 | //echo "parse round @$off\n"; 264 | $s = $this->parse_selector($str, $off, $hints); 265 | $a [] = $s[1]; 266 | $s = $this->lex($str, $off = $s[0]); 267 | //echo "next lex @$off "; 268 | //print_r($s); 269 | if ($s && $s[1] === " ") $s = $this->lex($str, $off = $s[0]); 270 | //echo "next lex @$off "; 271 | if (!$s) break; 272 | // now we've parsed a selector, and have something else... 273 | if ($s[1] === ">" || $s[1] === "~") { 274 | if ($s[1] === "~") $hints['usesSiblingOp'] = true; 275 | $a []= $s[1]; 276 | $off = $s[0]; 277 | } else if ($s[1] === ",") { 278 | if ($am === null) $am = [ ",", $a ]; 279 | else $am []= $a; 280 | $a = []; 281 | $off = $s[0]; 282 | } else if ($s[1] === ")") { 283 | if (!$nested) $this->te("ucp", $s[1]); 284 | $readParen = 1; 285 | $off = $s[0]; 286 | break; 287 | } 288 | } 289 | if ($nested && !$readParen) $this->te("mcp", $str); 290 | if ($am) $am []= $a; 291 | $rv; 292 | if (!$nested && isset($hints['usesSiblingOp'])) { 293 | $rv = $this->normalize($am ? $am : $a); 294 | } else { 295 | $rv = $am ? $am : $a; 296 | } 297 | 298 | 299 | return array($off, $rv); 300 | } 301 | 302 | function normalizeOne($sel) { 303 | $sels = array(); 304 | $s=null; 305 | for ($i = 0; $i < sizeof($sel); $i++) { 306 | if ($sel[$i] === '~') { 307 | // `A ~ B` maps to `:has(:root > A) > B` 308 | // `Z A ~ B` maps to `Z :has(:root > A) > B, Z:has(:root > A) > B` 309 | // This first clause, takes care of the first case, and the first half of the latter case. 310 | if ($i < 2 || $sel[$i-2] != '>') { 311 | $s = array_slice($sel,0,$i-1); 312 | $s []= array('has'=>array(array(array('pc'=> ":root"), ">", $sel[$i-1] ))); 313 | $s []= ">"; 314 | $s = array_merge($s, array_slice($sel, $i+1)); 315 | $sels []= $s; 316 | } 317 | // here we take care of the second half of above: 318 | // (`Z A ~ B` maps to `Z :has(:root > A) > B, Z :has(:root > A) > B`) 319 | // and a new case: 320 | // Z > A ~ B maps to Z:has(:root > A) > B 321 | if ($i > 1) { 322 | $at = $sel[$i-2] === '>' ? $i-3 : $i-2; 323 | $s = array_slice($sel,0,$at); 324 | $z = array(); 325 | foreach($sel[$at] as $k => $v){ $z[$k] = $v; } 326 | if (!isset($z['has'])) $z['has'] = array(); 327 | $z['has'] []= array( array('pc'=> ":root"), ">", $sel[$i-1]); 328 | 329 | $s = array_merge($s, array($z, '>'), array_slice($sel, $i+1) ); 330 | $sels []= $s; 331 | } 332 | break; 333 | } 334 | } 335 | if ($i == sizeof($sel)) return $sel; 336 | return sizeof($sels) > 1 ? array_merge(array(','), $sels) : $sels[0]; 337 | } 338 | 339 | function normalize($sels) { 340 | if ($sels[0] === ',') { 341 | $r = array(","); 342 | for ($i = 0; $i < sizeof($sels); $i++) { 343 | $s = $this->normalizeOne($s[$i]); 344 | $r = array_merge($r, $s[0] === "," ? array_slice($s,1) : $s); 345 | } 346 | return $r; 347 | } else { 348 | return $this->normalizeOne($sels); 349 | } 350 | } 351 | 352 | function parse_selector($str, $off, $hints) { 353 | $soff = $off; 354 | $s = array(); 355 | $l = $this->lex($str, $off); 356 | 357 | //echo "parse_selector:1 @$off ".print_r($l,true)."\n"; 358 | 359 | 360 | // skip space 361 | if ($l && $l[1] === " ") { $soff = $off = $l[0]; $l = $this->lex($str, $off); } 362 | if ($l && $l[1] === $this->toks['typ']) { 363 | $s['type'] = $l[2]; 364 | $l = $this->lex($str, ($off = $l[0])); 365 | } else if ($l && $l[1] === "*") { 366 | // don't bother representing the universal sel, '*' in the 367 | // parse tree, cause it's the default 368 | $l = $this->lex($str, ($off = $l[0])); 369 | } 370 | 371 | 372 | 373 | // now support either an id or a pc 374 | while (true) { 375 | //echo "parse_selector:1 @$off ".print_r($l,true)."\n"; 376 | if ($l === null) { 377 | break; 378 | } else if ($l[1] === $this->toks['ide']) { 379 | if ($s['id']) $this->te("nmi", $l[1]); 380 | $s[ 'id'] = $l[2]; 381 | } else if ($l[1] === $this->toks['psc']) { 382 | if ($s['pc'] || $s['pf']) $this->te("mpc", $l[1]); 383 | // collapse first-child and last-child into nth-child expressions 384 | if ($l[2] === ":first-child") { 385 | $s['pf'] = ":nth-child"; 386 | $s['a'] = 0; 387 | $s['b'] = 1; 388 | } else if ($l[2] === ":last-child") { 389 | $s['pf'] = ":nth-last-child"; 390 | $s['a'] = 0; 391 | $s['b'] = 1; 392 | } else { 393 | $s['pc'] = $l[2]; 394 | } 395 | } else if ($l[1] === $this->toks['psf']) { 396 | if ($l[2] === ":val" || $l[2] === ":contains") { 397 | $s['expr'] = array(VALUE_PLACEHOLDER, $l[2] === ":val" ? "=" : "*=", null); 398 | // any amount of whitespace, followed by paren, string, paren 399 | $l = $this->lex($str, ($off = $l[0])); 400 | if ($l && $l[1] === " ") $l = $this->lex($str, $off = $l[0]); 401 | if (!$l || $l[1] !== "(") $this->te("pex", $str); 402 | $l = $this->lex($str, ($off = $l[0])); 403 | if ($l && $l[1] === " ") $l = $this->lex($str, $off = $l[0]); 404 | if (!$l || $l[1] !== $this->toks['str']) $this->te("sex", $str); 405 | $s['expr'][2] = $l[2]; 406 | $l = $this->lex($str, ($off = $l[0])); 407 | if ($l && $l[1] === " ") $l = $this->lex($str, $off = $l[0]); 408 | if (!$l || $l[1] !== ")") $this->te("epex", $str); 409 | } else if ($l[2] === ":has") { 410 | // any amount of whitespace, followed by paren 411 | $l = $this->lex($str, ($off = $l[0])); 412 | if ($l && $l[1] === " ") $l = $this->lex($str, $off = $l[0]); 413 | if (!$l || $l[1] !== "(") $this->te("pex", $str); 414 | $h = $this->parse($str, $l[0], true); 415 | $l[0] = $h[0]; 416 | if (!isset($s['has'])) $s['has'] = array(); 417 | $s['has'] []= $h[1]; 418 | } else if ($l[2] === ":expr") { 419 | if (isset($s['expr'])) $this->te("mexp", $str); 420 | $e = $this->exprParse($str, $l[0]); 421 | $l[0] = $e[0]; 422 | $s['expr'] = $e[1]; 423 | } else { 424 | if (isset($s['pc']) || isset($s['pf']) ) $this->te("mpc", $str); 425 | $s['pf'] = $l[2]; 426 | //m = nthPat.exec(str.substr(l[0])); 427 | preg_match($this->nthPat, substr($str, $l[0]), $m); 428 | 429 | 430 | if (!$m) $this->te("mepf", $str); 431 | if (strlen($m[5])>0) { 432 | $s['a'] = 2; 433 | $s['b'] = ($m[5] === "odd") ? 1 : 0; 434 | } else if (strlen($m[6])>0) { 435 | $s['a'] = 0; 436 | $s['b'] = (int)$m[6]; 437 | } else { 438 | $s['a'] = (int)(($m[1] ? $m[1] : "+") . ($m[2] ? $m[2] : "1")); 439 | $s['b'] = $m[3] ? (int)($m[3] + $m[4]) : 0; 440 | } 441 | $l[0] += strlen($m[0]); 442 | } 443 | } else { 444 | break; 445 | } 446 | $l = $this->lex($str, ($off = $l[0])); 447 | } 448 | 449 | // now if we didn't actually parse anything it's an error 450 | if ($soff === $off) $this->te("se", $str); 451 | //echo "parsed "; 452 | //print_r($s); 453 | return array($off, $s); 454 | } 455 | 456 | // THE EVALUATOR 457 | 458 | 459 | function mytypeof($o) { 460 | if ($o === null) return "null"; 461 | if (is_object($o)) return "object"; 462 | if(is_array($o)) return "array"; 463 | if(is_numeric($o)) return "number"; 464 | if($o===true || $o==false) return "boolean"; 465 | return "string"; 466 | } 467 | 468 | function mn($node, $sel, $id, $num, $tot) { 469 | //echo "match on $num/$tot\n"; 470 | $sels = array(); 471 | $cs = ($sel[0] === ">") ? $sel[1] : $sel[0]; 472 | $m = true; 473 | $mod = null; 474 | if (isset($cs['type'])) $m = $m && ($cs['type'] === $this->mytypeof($node)); 475 | if (isset($cs['id'])) $m = $m && ($cs['id'] === $id); 476 | if ($m && isset($cs['pf'])) { 477 | if($num===null) $num = null; 478 | else if ($cs['pf'] === ":nth-last-child") $num = $tot - $num; 479 | else $num++; 480 | 481 | if ($cs['a'] === 0) { 482 | $m = $cs['b'] === $num; 483 | } else if($num!==null){ 484 | $mod = (($num - $cs['b']) % $cs['a']); 485 | 486 | $m = (!$mod && (($num*$cs['a'] + $cs['b']) >= 0)); 487 | 488 | }else { 489 | $m = false; 490 | } 491 | } 492 | if ($m && isset($cs['has'])) { 493 | // perhaps we should augment forEach to handle a return value 494 | // that indicates "client cancels traversal"? 495 | //var bail = function() { throw 42; }; 496 | for ($i = 0; $i < sizeof($cs['has']); $i++) { 497 | //echo "select for has ".print_r($cs['has'],true); 498 | $res = $this->collect($cs['has'][$i], $node, null, null, null, true ); 499 | if(sizeof($res)>0){ 500 | //echo " => ".print_r($res, true); 501 | //echo " on ".print_r($node, true); 502 | continue; 503 | } 504 | //echo "blaaaa \n"; 505 | $m = false; 506 | break; 507 | } 508 | } 509 | if ($m && isset($cs['expr'])) { 510 | $m = $this->exprEval($cs['expr'], $node); 511 | } 512 | // should we repeat this selector for descendants? 513 | if ($sel[0] !== ">" && $sel[0]['pc'] !== ":root") $sels []= $sel; 514 | 515 | if ($m) { 516 | // is there a fragment that we should pass down? 517 | if ($sel[0] === ">") { 518 | if (sizeof($sel) > 2) { $m = false; $sels []= array_slice($sel,2); } 519 | } 520 | else if (sizeof($sel) > 1) { $m = false; $sels []= array_slice($sel,1); } 521 | } 522 | //echo "MATCH? "; 523 | //echo print_r($node,true); 524 | //echo $m ? "YES":"NO"; 525 | //echo "\n"; 526 | return array($m, $sels); 527 | } 528 | 529 | function collect($sel, $obj, $collector=null, $id=null, $num=null, $tot=null, $returnFirst=false) { 530 | if(!$collector) $collector = array(); 531 | 532 | $a = ($sel[0] === ",") ? array_slice($sel, 1) : array($sel); 533 | $a0 = array(); 534 | $call = false; 535 | $i = 0; 536 | $j = 0; 537 | $k = 0; 538 | $x = 0; 539 | for ($i = 0; $i < sizeof($a); $i++) { 540 | $x = $this->mn($obj, $a[$i], $id, $num, $tot); 541 | if ($x[0]) { 542 | $call = true; 543 | if($returnFirst) return array($obj); 544 | } 545 | for ($j = 0; $j < sizeof($x[1]); $j++) { 546 | $a0 []= $x[1][$j]; 547 | } 548 | } 549 | if (sizeof($a0)>0 && ( is_array($obj) || is_object($obj) ) ) { 550 | if (sizeof($a0) >= 1) { 551 | array_unshift($a0, ","); 552 | } 553 | if(is_array($obj)){ 554 | $_tot = sizeof($obj); 555 | //echo "iterate $_tot\n"; 556 | foreach ($obj as $k=>$v) { 557 | $collector = $this->collect($a0, $v, $collector, null, $k, $_tot, $returnFirst); 558 | if($returnFirst && sizeof($collector)>0) return $collector; 559 | } 560 | }else{ 561 | foreach ($obj as $k=>$v) { 562 | $collector = $this->collect($a0, $v, $collector, $k, null, null, $returnFirst); 563 | if($returnFirst && sizeof($collector)>0) return $collector; 564 | } 565 | 566 | } 567 | } 568 | 569 | if($call){ 570 | $collector []= $obj; 571 | } 572 | 573 | return $collector; 574 | } 575 | 576 | function match($obj){ 577 | return $this->collect($this->sel[1], $obj); 578 | } 579 | 580 | 581 | } 582 | ?> 583 | -------------------------------------------------------------------------------- /src/test.php: -------------------------------------------------------------------------------- 1 | match($testdataStruct) as $r){ 29 | $real_output .= json_encode($r)."\n"; 30 | } 31 | }catch(Exception $e){ 32 | $real_output .= "Error: ".$e->getMessage(); 33 | } 34 | 35 | $ws = array(' ',"\n","\r"); 36 | if(str_replace($ws,'', $expected_output)==str_replace($ws,'',$real_output)){ 37 | echo "SUCCESS\n"; 38 | }else{ 39 | echo "FAIL\n"; 40 | print_r($parser->sel); 41 | echo "-expected:\n\n"; 42 | echo $expected_output; 43 | echo "\n\n-actual:\n\n"; 44 | echo $real_output."\n"; 45 | } 46 | 47 | } 48 | 49 | 50 | } 51 | 52 | 53 | 54 | } 55 | 56 | 57 | 58 | 59 | ?> 60 | 61 | 62 | --------------------------------------------------------------------------------