hQuery.php
Node.php
1 <?php
2 namespace duzun\hQuery;
3 // ------------------------------------------------------------------------
12 abstract class Node implements \Iterator, \Countable {
13  // ------------------------------------------------------------------------
14  const VERSION = '2.0.2';
15  // ------------------------------------------------------------------------
16  public static $last_http_result; // Response details of last request
17 
18  // ------------------------------------------------------------------------
19  public static $selected_doc = NULL;
20  // ------------------------------------------------------------------------
21  protected $_prop = array(); // Properties
22  protected $doc; // Parent doc
23  protected $ids; // contained elements' IDs
24  protected $exc; // excluded elements' IDs
25 
26  // ------------------------------------------------------------------------
27  public $tag_map; // map tag names (eg ['b' => 'strong', 'i' => 'em'])
28 
29  // ------------------------------------------------------------------------
30  // Memory efficiency tricks ;-)
31  static $_ar_ = array() ;
32  static $_mi_ = PHP_INT_MAX ;
33  static $_nl_ = NULL ;
34  static $_fl_ = false ;
35  static $_tr_ = true ;
36 
37  // ------------------------------------------------------------------------
38  protected function __construct($doc, $ids, $is_ctx=false) {
39  $this->doc = $doc;
40  if(is_int($ids)) $ids = array($ids => $doc->ids[$ids]);
41  $this->ids = $is_ctx ? $this->_ctx_ids($ids) : $ids;
42  if($doc === $this) { // documents have no $doc property
43  unset($this->doc);
44  self::$selected_doc = $this;
45  }
46  }
47 
48  public function __destruct() {
49  if(self::$selected_doc === $this) self::$selected_doc = self::$_nl_;
50  $this->ids = self::$_nl_; // If any reference exists, destroy its contents! P.S. Might be buggy, but hey, I own this property. Sincerly yours, hQuery_Node class.
51  unset($this->doc, $this->ids);
52  }
53 
54  // ------------------------------------------------------------------------
62  public function attr($attr=NULL, $to_str=false) {
63  $k = key($this->ids);
64  if($k === NULL) {
65  reset($this->ids);
66  $k = key($this->ids);
67  }
68  return isset($k) ? $this->doc()->get_attr_byId($k, $attr, $to_str) : NULL;
69  }
70  // ------------------------------------------------------------------------
71 
72  // Deprecated
73  public function is_empty() { return $this->isEmpty() ;}
74 
75  public function isEmpty() {
76  return empty($this->ids);
77  }
78 
79  public function isDoc() {
80  return !isset($this->doc) || $this === $this->doc;
81  }
82 
88  public function doc() {
89  return isset($this->doc) ? $this->doc : $this;
90  }
91 
101  public function find($sel, $attr=NULL) {
102  return $this->doc()->find($sel, $attr, $this);
103  }
104 
105  public function exclude($sel, $attr=NULL) {
106  $e = $this->find($sel, $attr, $this);
107  if($e) {
108  if(empty($this->exc)) {
109  $this->exc = $e->ids;
110  }
111  else {
112  // foreach($e->ids as $b => $e) $this->exc[$b] = $e;
113  $this->exc = $e->ids + $this->exc;
114  ksort($this->exc);
115  }
116  }
117  return $e;
118  }
119 
120  public function __toString() {
121  // doc
122  if($this->isDoc()) return $this->html;
123 
124  $ret = '';
125  $doc = $this->doc;
126  $ids = $this->ids;
127  if ( !empty($this->exc) ) {
128  $ids = array_diff_key($ids, $this->exc);
129  }
130  foreach($ids as $p => $q) {
131  // if(isset($this->exc, $this->exc[$p])) continue;
132  ++$p;
133  if($p < $q) $ret .= substr($doc->html, $p, $q-$p);
134  }
135  return $ret;
136  }
137 
138  // ------------------------------------------------------------------------
142  public function html($id=NULL) {
143  if($this->isDoc()) return $this->html; // doc
144 
145  $id = $this->_my_ids($id);
146  if($id === false) return self::$_fl_;
147 
148  $doc = $this->doc;
149  if ( !empty($this->exc) ) {
150  $id = array_diff_key($id, $this->exc);
151  }
152 
153  $ret = self::$_nl_;
154  foreach($id as $p => $q) {
155  // if(isset($this->exc, $this->exc[$p])) continue;
156  ++$p;
157  if($p<$q) $ret .= substr($doc->html, $p, $q-$p);
158  }
159  return $ret;
160  }
161 
165  public function outerHtml($id=NULL) {
166  $dm = $this->isDoc() && !isset($id);
167  if($dm) return $this->html; // doc
168 
169  $id = $this->_my_ids($id);
170  if($id === false) return self::$_fl_;
171  $doc = $this->doc();
172  $ret = self::$_nl_;
173  $map = isset($this->tag_map) ? $this->tag_map : (isset($doc->tag_map) ? $doc->tag_map : NULL);
174  foreach($id as $p => $q) {
175  $a = $doc->get_attr_byId($p, NULL, true);
176  $n = $doc->tags[$p];
177  if($map && isset($map[$_n=strtolower($n)])) $n = $map[$_n];
178  $h = $p++ == $q ? false : ($p<$q ? substr($doc->html, $p, $q-$p) : '');
179  $ret .= '<'.$n.($a?' '.$a:'') . ($h === false ? ' />' : '>' . $h . '</'.$n.'>');
180  }
181  return $ret;
182  }
183 
187  public function text($id=NULL) {
188  return html_entity_decode(strip_tags($this->html($id)), ENT_QUOTES);/* ??? */
189  }
190 
194  public function nodeName($caseFolding = NULL, $id=NULL) {
195  if(!isset($caseFolding)) $caseFolding = hQuery_HTML_Parser::$case_folding;
196  $dm = $this->isDoc() && !isset($id);
197  if($dm) $ret = array_unique($this->tags); // doc
198  else {
199  $id = $this->_my_ids($id, true);
200  if($id === false) return self::$_fl_;
201  $ret = self::array_select($this->doc()->tags, $id);
202  }
203  if($caseFolding) {
204  foreach($ret as $i => $n) $ret[$i] = strtolower($n);
205  if($dm) $ret = array_unique($ret);
206  }
207  return count($ret) <= 1 ? reset($ret) : $ret;
208  }
209 
210  // public function firstChild() {
211  // $doc = $this->doc();
212  // $q = reset($this->ids);
213  // $p = key($this->ids);
214  // return new Element($doc, array($p=>$q));
215  // }
216 
217  // public function lastChild() {
218  // $doc = $this->doc();
219  // $q = end($this->ids);
220  // $p = key($this->ids);
221  // return new Element($doc, array($p=>$q));
222  // }
223 
224  // ------------------------------------------------------------------------
237  public function pos($restore=true) {
238  $k = key($this->ids);
239  if($k === NULL) {
240  reset($this->ids);
241  $k = key($this->ids);
242  if($k !== NULL && $restore) {
243  end($this->ids);
244  next($this->ids);
245  }
246  }
247  return $k;
248  }
249  // ------------------------------------------------------------------------
256  protected function _ctx_ids($ids=NULL) {
257  $m = -1;
258  $exc = $this->exc;
259  if(!isset($ids)) $ids = $this->ids;
260  elseif(is_int($ids)) $ids = isset($this->ids[$ids]) ? array($ids => $this->ids[$ids]) : self::$_fl_;
261  else {
262  foreach($ids as $b => $e) {
263  if($b <= $m || $b+1 >= $e and empty($exc[$b])) unset($ids[$b]);
264  else $m = $e;
265  }
266  }
267  return $ids;
268  }
269 
275  protected function _sub_ids($eq=false) {
276  $ret = array();
277  $ce = reset($this->ids);
278  $cb = key($this->ids);
279  $doc = $this->doc();
280  foreach($doc->ids as $b => $e) {
281  if($b < $cb || !$eq && $b == $cb) continue;
282  if($b < $ce) {
283  $ret[$b] = $e;
284  }
285  else {
286  $ce = next($this->ids);
287  if(!$ce) break; // end of context
288  $cb = key($this->ids);
289  }
290  }
291  return $ret;
292  }
293 
299  protected function _doc_ids($el, $force_array=true) {
300  if($el instanceof self) $el = $el->ids;
301  if($force_array) {
302  if(is_int($el)) $el = array($el=>$this->doc()->ids[$el]);
303  if(!is_array($el)) throw new \Exception(__CLASS__ . '->' . __FUNCTION__ . ': not Array!');
304  }
305  return $el;
306  }
307 
308  protected function _my_ids($id=NULL, $keys=false) {
309  if(!isset($id)) $id = $this->ids;
310  elseif(is_int($id)) {
311  if(!isset($this->ids[$id])) return self::$_fl_;
312  if($keys) return $id;
313  $id = array($id => $this->ids[$id]);
314  }
315  elseif(!$id) return self::$_fl_;
316  else ksort($id);
317  return $keys ? array_keys($id) : $id;
318  }
319  // ------------------------------------------------------------------------
320  protected function _parent($ids=NULL, $n=0) {
321  $ret = self::$_ar_;
322  $ids = $this->_my_ids($ids);
323  if(!$ids) return $ret;
324 
325  $lb = $le = -1; // last parent
326  $ie = reset($ids); // current child
327  $ib = key($ids);
328  $dids = &$this->doc()->ids;
329  foreach($dids as $b => $e) { // $b < $ib < $e
330  // if current element is past current child and last parent is parent for current child
331  if($ib <= $b) {
332  if($ib < $le && $lb < $ib) {
333  $ret[$lb] = $le;
334  }
335  // while current element is past current child
336  do {
337  $ie = next($ids);
338  if($ie === false) { $ib = -1; break 2; }
339  $ib = key($ids);
340  } while($ib <= $b);
341  }
342  // here $b < $ib
343  if($ib < $e) { // [$b=>$e] is a parent of $ib
344  $lb = $b;
345  $le = $e;
346  }
347  }
348  if($ib <= $b && $ib < $le && $lb < $ib) { // while current element is past current child and last parent is parent for current child
349  $ret[$lb] = $le;
350  }
351  return $ret;
352  }
353 
354  function _children($ids=NULL, $n=NULL) {
355  $ret = self::$_ar_;
356  $ids = $this->_my_ids($ids);
357  if(!$ids) return $ret;
358 
359  $dids = &$this->doc()->ids;
360  $le = end($ids);
361  if(current($dids) === false) $ie = reset($dids);
362  $ib = key($dids);
363  foreach($ids as $b => $e) {
364  if($b+4 >= $e) continue; // empty tag; min 3 chars are required for a tag - eg. <b>
365 
366  // child of prev element
367  if($b <= $le) {
368  while($b < $ib) if($ie = prev($dids)) $ib = key($dids); else { reset($dids); break; }
369  }
370  else {
371  if($ie === false && $ib < $b) break;
372  }
373  $le = $e;
374 
375  while($ib <= $b) {
376  if($ie = next($dids)) $ib = key($dids);
377  else { end($dids); break; }
378  }
379 
380  if($b < $ib) {
381  $i = 0;
382  while($ib < $e) {
383  if(!isset($n)) $ret[$ib] = $ie;
384  elseif($n == $i) { $ret[$ib] = $ie; break; }
385  else ++$i;
386  $lie = $ie < $e ? $ie : $e;
387  while($ib <= $lie) {
388  if($ie = next($dids)) $ib = key($dids);
389  else { end($dids); continue 3; }
390  }
391  }
392  }
393  }
394  return $ret;
395  }
396 
397  function _next($ids=NULL, $n=0) {
398  $ret = self::$_ar_; // array()
399  $ids = $this->_my_ids($ids); // ids to search siblings for
400  if(!$ids) return $ret;
401 
402  $dids = &$this->doc()->ids; // all elements in the doc
403  $kb = $le = self::$_mi_; // [$lb=>$le] (last parent) now is 100% parent for any element
404  $ke = $lb = -1; // last parent
405  $ie = reset($ids); // current child
406  $ib = key($ids);
407  $e = current($dids); // traverse starting from current position
408  if($e !== false) do { $b = key($dids); } while( ($ib <= $b || $e < $ib) && ($e = prev($dids)) ) ;
409  if(empty($e)) $e = reset($dids); // current element
410 
411  $pt = $st = $ret; // stacks: $pt - parents, $st - siblings limits
412 
413  while($e) {
414  $b = key($dids);
415 /* 4) */ if($ib <= $b) { // if current element is past our child, then its siblings context is found
416  if($kb < $ke) $st[$kb] = $ke;
417  $kb = $ie;
418  $ke = $le;
419 
420  $ib = ($ie = next($ids)) ? key($ids) : self::$_mi_; // $ie < $ib === no more children
421  if($ie < $ib) break; // no more children, empty siblings context, search done!
422 
423  // pop from stack, if not empty
424  while($le < $ib && $pt) { // if past current parent, pop another one from the stack
425  $le = end($pt);
426  unset($pt[$lb = key($pt)]); // there must be something in the stack anyway
427  }
428  }
429 
430 /* 3) */ if($b < $ib && $ib < $e) { // push the parents to their stack
431  $pt[$lb] = $le;
432  $lb = $b;
433  $le = $e;
434  }
435 
436  $e = next($dids);
437  } // while
438 
439  if($ke < $kb) return $ret; // no siblings contexts found!
440  $st[$kb] = $ke;
441  ksort($st);
442 
443  foreach($st as $kb => $ke) {
444  if($e !== false) do { $b = key($dids); } while( $kb < $b && ($e = prev($dids)) ) ;
445  if(empty($e)) $e = reset($dids); // current element
446  do {
447  $b = key($dids);
448  if($kb < $b) {
449  // iterate next siblings
450  $i = 0;
451  while($b < $ke) {
452  if($n == $i) { $ret[$b] = $e; break; } else ++$i;
453  $lie = $e < $ke ? $e : $ke;
454  while($b <= $lie && ($e = next($dids))) $b = key($dids);
455  if(!$e) { $e = end($dids); break; }
456  }
457  break;
458  }
459  } while($e = next($dids));
460  }
461 
462  return $ret;
463  }
464 
465  function _prev($ids=NULL, $n=0) {
466  $ret = self::$_ar_; // array()
467  $ids = $this->_my_ids($ids); // ids to search siblings for
468  if(!$ids) return $ret;
469 
470  $dids = &$this->doc()->ids; // all elements in the doc
471  $kb = $le = self::$_mi_; // [$lb=>$le] (last parent) now is 100% parent for any element
472  $ke = $lb = -1; // last parent
473  $ie = reset($ids); // current child
474  $ib = key($ids);
475  $e = current($dids); // traverse starting from current position
476  if($e !== false) do {
477  $b = key($dids);
478  } while( ($ib <= $b || $e < $ib) && ($e = prev($dids)) ) ;
479  if(empty($e)) $e = reset($dids); // current element
480 
481  $pt = $st = $ret; // stacks: $pt - parents, $st - siblings limits
482  while($e) {
483  $b = key($dids);
484 /* 4) */ if($ib <= $b) { // if current element is past our child, then its siblings context is found
485  if($kb < $ke) $st[$kb] = $ke;
486  $kb = $lb;
487  $ke = $ib;
488 
489  $ib = ($ie = next($ids)) ? key($ids) : self::$_mi_; // $ie < $ib === no more children
490  if($ie < $ib) break; // no more children, empty siblings context, search done!
491 
492  // pop from stack, if not empty
493  while($le < $ib && $pt) { // if past current parent, pop another one from the stack
494  $le = end($pt);
495  unset($pt[$lb = key($pt)]); // there must be something in the stack anyway
496  }
497  }
498 
499 /* 3) */ if($b < $ib && $ib < $e) { // push the parents to their stack
500  $pt[$lb] = $le;
501  $lb = $b;
502  $le = $e;
503  }
504 
505  $e = next($dids);
506  } // while
507 
508  if($ke < $kb) return $ret; // no siblings contexts found!
509 
510  if($e !== false) do { $b = key($dids); } while( $kb < $b && ($e = prev($dids)) ) ;
511  if(empty($e)) $e = reset($dids); // current element
512 
513  $st[$kb] = $ke;
514  ksort($st);
515  $kb = reset($st);
516  $ke = key($st);
517 
518  do {
519  $b = key($dids);
520 
521 /* 1) */ if($kb < $b) {
522  // iterate next siblings
523  $pt = self::$_ar_;
524  $lie = -1;
525  while($b < $ke) {
526  $pt[$b] = $e;
527  $lie = $e < $ke ? $e : $ke;
528  while($b <= $lie && ($e = next($dids))) $b = key($dids);
529  if(!$e) { $e = end($dids); break; }
530  }
531 
532  if($pt) {
533  $c = count($pt);
534  $i = $n < 0 ? 0 : $c;
535  $i -= $n + 1;
536  if(0 <= $i && $i < $c) {
537  $pt = array_slice($pt, $i, 1, true);
538  $ret[key($pt)] = reset($pt);
539  }
540  }
541  $pt = self::$_nl_;
542 
543  if(empty($st)) break; // stack empty, no more children, search done!
544 
545  // pop from stack, if not empty
546  if($kb = reset($st)) unset($st[$ke = key($st)]);
547  else $kb = self::$_mi_; // $ke < $kb === context empy
548 
549  // rewind back
550  while($kb < $b && ($e = prev($dids))) $b = key($dids);
551  if(!$e) $e = reset($dids); // only for wrong context! - error
552  }
553  } while($e = next($dids));
554 
555  return $ret;
556  }
557 
558  function _all($ids=NULL) {
559  $ret = self::$_ar_;
560  $ids = $this->_my_ids($ids);
561  if(!$ids) return $ret;
562 
563  return $this->doc()->_find('*', NULL, NULL, $ids);
564  }
565 
567  function _has($el, $eq=false) {
568  if(is_int($el)) {
569  $e = end($this->ids);
570  if($el >= $e) return self::$_fl_;
571  foreach($this->ids as $b => $e) {
572  if($el < $b) return self::$_fl_;
573  if($el == $b) return $eq;
574  if($el < $e) return self::$_tr_;
575  }
576  return self::$_fl_;
577  }
578  if($el instanceof self) { if($el === $this) return self::$_fl_; $el = $el->ids; } else
579  $el = $this->_ctx_ids($this->_doc_ids($el, true));
580  foreach($el as $b => $e) if(!$this->_has($b)) return self::$_fl_;
581  return self::$_tr_;
582  }
583 
591  function _filter_contains($el, $eq=false) {
592  if($el instanceof self) $o = $el;
593  $el = $this->_doc_ids($el);
594  $ret = self::$_ar_;
595 
596  $lb = $le = -1; // last parent
597  $ie = reset($el); // current child
598  $ib = key($el);
599  foreach($this->ids as $b => $e) {
600  // skip up to first $el in $this
601  while($ib < $b || !$eq && $ib == $b) {
602  $ie = next($el);
603  if($ie === false) { $ib = -1; break 2; }
604  $ib = key($el);
605  }
606  // $b < $ib
607  while($ib < $e) {
608  $ret[$ib] = $ie;
609  $ie = next($el);
610  if($ie === false) { $ib = -1; break 2; }
611  $ib = key($el);
612  }
613  }
614  if(!empty($o)) {
615  $o = clone $o;
616  $o->ids = $ret;
617  $ret = $o;
618  }
619  return $ret;
620  }
621 
622 // - Magic ------------------------------------------------
623  public function __get($name) {
624  if($this->_prop && array_key_exists($name, $this->_prop)) return $this->_prop[$name];
625  return $this->attr($name);
626  }
627  public function __set($name, $value) {
628  if(isset($value)) return $this->_prop[$name] = $value;
629  $this->__unset($name);
630  }
631  public function __isset($name) {
632  return isset($this->_prop[$name]);
633  }
634  public function __unset($name) {
635  unset($this->_prop[$name]);
636  }
637  // ------------------------------------------------------------------------
638  // Countable:
639  public function count() { return isset($this->ids) ? count($this->ids) : 0; }
640  // ------------------------------------------------------------------------
641  // Iterable:
642  public function current() {
643  $k = key($this->ids);
644  if($k === NULL) return false;
645  return array($k => $this->ids[$k]);
646  }
647  public function valid() { return current($this->ids) !== false; }
648  public function key() { return key($this->ids); }
649  public function next() { return next($this->ids) !== false ? $this->current() : false; }
650  public function prev() { return prev($this->ids) !== false ? $this->current() : false; }
651  public function rewind() { reset($this->ids); return $this->current(); }
652 
653 // - Helpers ------------------------------------------------
654 
663  static function html_normal_pseudoClass($p) {
664  if(is_int($p)) return $p;
665  $i = (int)$p;
666  if((string)$i === $p) return $i;
667 
668  static $map = array(
669  'lt' => '<',
670  'gt' => '>',
671  'prev' => '-',
672  'next' => '+',
673  'parent' => '|',
674  'children' => '*',
675  '*' => '*'
676  );
677  $p = explode('(', $p, 2);
678  $p[1] = isset($p[1]) ? trim(rtrim($p[1], ')')) : NULL;
679  switch($p[0]) {
680  case 'first' :
681  case 'first-child': return 0;
682  case 'last' :
683  case 'last-child' : return -1;
684  case 'eq' : return (int)$p[1];
685  default:
686  if(isset($map[$p[0]])) {
687  $p[0] = $map[$p[0]];
688  if(isset($p[1])) $p[1] = (int)$p[1];
689  }
690  else {
691  // ??? unknown ps
692  }
693  }
694  return array($p[0]=>$p[1]);
695  }
696 
697  // ------------------------------------------------------------------------
723  static function html_selector2struc($sel) {
724  $sc = '#.:';
725  $n = NULL;
726  $a = array();
727  $def = array('n'=>$n, 'i'=>$n, 'c'=>$a, 'p'=>$a);
728  $sel = rtrim(trim(preg_replace('/\\s*(>|,)\\s*/', '$1', $sel), " \t\n\r,>"), $sc);
729  $sel = explode(',', $sel);
730  foreach($sel as &$a) {
731  $a = preg_split('|\\s+|', $a);
732  foreach($a as &$b) {
733  $b = explode('>', $b);
734  foreach($b as &$c) {
735  $d = $def;
736  $l = strlen($c);
737  $j = strcspn($c, $sc, 0, $l);
738  if($j) $d['n'] = substr($c, 0, $j);
739  $i = $j;
740  while($i<$l) {
741  $k = $c[$i++];
742  $j = strcspn($c, $sc, $i, $l);
743  if($j) {
744  $e = substr($c, $i, $j);
745  switch($k) {
746  case '.': $d['c'][] = $e; break;
747  case '#': $d['i'] = $e; break;
748  case ':': $d['p'][] = self::html_normal_pseudoClass($e); break;
749  }
750  $i+=$j;
751  }
752  }
753  if(empty($d['c'])) $d['c'] = $n;
754  if(empty($d['p'])) $d['p'] = $n;
755  $c = $d;
756  }
757  }
758  }
759  return $sel;
760  }
761 
762  // ------------------------------------------------------------------------
763  protected static function html_findTagClose($str, $p) {
764  $l = strlen($str);
765  while ( $i = $p < $l ? strpos($str, '>', $p) : $l ) {
766  $e = $p; // save pos
767  $p += strcspn($str, '"\'', $p, $i);
768 
769  // If closest quote is after '>', return pos of '>'
770  if ( $p >= $i ) return $i;
771 
772  // If there is any quote before '>', make sure '>' is outside an attribute string
773  $q = $str[$p]; // " | ' ?
774  ++$p; // next char after the quote
775 
776  $e += strcspn($str, '=', $e, $p); // is there a '=' before first quote?
777 
778  // is this the attr_name (like in "attr_name"="attr_value") ?
779  if ( $e >= $p ) {
780  // Attribute name should not have '>'
781  $p += strcspn($str, '>' . $q, $p, $l);
782  // but if it has '>', it is the tag closing char
783  if ( $str[$p] == '>' ) return $p;
784  }
785  // else, its attr_value
786  else {
787  $p += strcspn($str, $q, $p, $l);
788  }
789 
790  ++$p; // next char after the quote
791  }
792  return $i;
793  }
794  // ------------------------------------------------------------------------
795  static function html_parseAttrStr($str, $case_folding = true, $extended = false) {
796  static $_attrName_firstLet = NULL;
797  if(!$_attrName_firstLet) $_attrName_firstLet = self::str_range('a-zA-Z_');
798 
799  $ret = array();
800  for($i = strspn($str, " \t\n\r"), $len = strlen($str); $i < $len;) {
801  $i += strcspn($str, $_attrName_firstLet, $i);
802  if($i>=$len) break;
803  $b = $i;
804  $i += strcspn($str, " \t\n\r=\"\'", $i);
805  $attrName = rtrim(substr($str, $b, $i-$b));
806  if($case_folding) $attrName = strtolower($attrName);
807  $i += strspn($str, " \t\n\r", $i);
808  $attrValue = NULL;
809  if($i<$len && $str[$i]=='=') {
810  ++$i;
811  $i += strspn($str, " \t\n\r", $i);
812  if($i < $len) {
813  $q = substr($str, $i, 1);
814  if($q=='"' || $q=="'") {
815  $b = ++$i;
816  $e = strpos($str, $q, $i);
817  if($e !== false) {
818  $attrValue = substr($str, $b, $e-$b);
819  $i = $e+1;
820  }
821  else {
822  /*??? no closing quote */
823  }
824  }
825  else {
826  $b = $i;
827  $i += strcspn($str, " \t\n\r\"\'", $i);
828  $attrValue = substr($str, $b, $i-$b);
829  }
830  }
831  }
832  if($extended && $attrValue) switch($case_folding ? $attrName : strtolower($attrName)) {
833  case 'class':
834  $attrValue = preg_split("|\\s+|", trim($attrValue));
835  if(count($attrValue) == 1) $attrValue = reset($attrValue);
836  else sort($attrValue);
837  break;
838 
839  case 'style':
840  $attrValue = self::parseCSStr($attrValue, $case_folding);
841  break;
842  }
843 
844  $ret[$attrName] = $attrValue;
845  }
846  return $ret;
847  }
848  // ------------------------------------------------------------------------
849  static function html_attr2str($attr, $quote='"') {
850  $sq = htmlspecialchars($quote);
851  if($sq == $quote) $sq = false;
852  ksort($attr);
853  if(isset($attr['class']) && is_array($attr['class'])) {
854  sort($attr['class']);
855  $attr['class'] = implode(' ', $attr['class']);
856  }
857  if(isset($attr['style']) && is_array($attr['style'])) {
858  $attr['style'] = self::CSSArr2Str($attr['style']);
859  }
860  $ret = array();
861  foreach($attr as $n => $v) {
862  $ret[] = $n . '=' . $quote . ($sq ? str_replace($quote, $sq, $v) : $v) . $quote;
863  }
864  return implode(' ', $ret);
865  }
866  // ------------------------------------------------------------------------
867  static function parseCSStr($str, $case_folding = true) {
868  $ret = array();
869  $a = explode(';', $str); // ??? what if ; in "" ?
870  foreach($a as $v) {
871  $v = explode(':', $v, 2);
872  $n = trim(reset($v));
873  if($case_folding) $n = strtolower($n);
874  $ret[$n] = count($v) == 2 ? trim(end($v)) : NULL;
875  }
876  unset($ret['']);
877  return $ret;
878  }
879 
880  static function CSSArr2Str($css) {
881  if(is_array($css)) {
882  ksort($css);
883  $ret = array();
884  foreach($css as $n => $v) $ret[] = $n.':'.$v;
885  return implode(';', $ret);
886  }
887  return $css;
888  }
889  // ------------------------------------------------------------------------
890  static function str_range($comp, $pos=0, $len=NULL) {
891  $ret = array();
892  $b = strlen($comp);
893  if(!isset($len) || $len > $b) $len = $b;
894  $b = "\x0";
895  while($pos < $len) {
896  switch($c = $comp[$pos++]) {
897  case '\\': {
898  $b = substr($comp, $pos, 1);
899  $ret[$b] = $pos++;
900  } break;
901 
902  case '-': {
903  $c_ = ord($c=substr($comp, $pos, 1));
904  $b = ord($b);
905  while($b++ < $c_) $ret[chr($b)] = $pos;
906  while($b-- > $c_) $ret[chr($b)] = $pos;
907  } break;
908 
909  default: {
910  $ret[$b=$c] = $pos;
911  }
912  }
913  }
914  return implode('', array_keys($ret));
915  }
916  // ------------------------------------------------------------------------
917  static function array_select($arr, $keys, $force_null=false) {
918  $ret = array();
919  is_array($keys) or is_object($keys) or $keys = array($keys);
920  foreach($keys as $k) {
921  if(isset($arr[$k])) $ret[$k] = $arr[$k];
922  elseif($force_null) $ret[$k] = NULL;
923  }
924  return $ret;
925  }
926  // ------------------------------------------------------------------------
927  static function convert_encoding($a, $to, $from=NULL) {
928  static $meth = NULL;
929  isset($meth) or $meth = function_exists('mb_convert_encoding');
930  isset($from) or $from = $meth ? mb_internal_encoding() : iconv_get_encoding('internal_encoding');
931 
932  if(is_array($a)) {
933  $ret = array();
934  foreach($a as $n => $v) {
935  $ret[is_string($n)?self::convert_encoding($n,$to,$from):$n] = is_string($v) || is_array($v) || $v instanceof stdClass
936  ? self::convert_encoding($v, $to, $from)
937  : $v;
938  }
939  return $ret;
940  }
941  elseif($a instanceof stdClass) {
942  $ret = (object)array();
943  foreach($a as $n => $v) {
944  $ret->{is_string($n)?self::convert_encoding($n,$to,$from):$n} = is_string($v) || is_array($v) || $v instanceof stdClass
945  ? self::convert_encoding($v, $to, $from)
946  : $v;
947  }
948  return $ret;
949  }
950  return is_string($a) ? $meth ? mb_convert_encoding($a, $to, $from) : iconv($from, $to, $a) : $a;
951  }
952  // ------------------------------------------------------------------------
953 }
_sub_ids($eq=false)
Definition: Node.php:275
find($sel, $attr=NULL)
Definition: Node.php:101
_filter_contains($el, $eq=false)
Definition: Node.php:591
_ctx_ids($ids=NULL)
Definition: Node.php:256
nodeName($caseFolding=NULL, $id=NULL)
Definition: Node.php:194
_has($el, $eq=false)
$el < $this, with $eq == true -> $el <= $this
Definition: Node.php:567
_doc_ids($el, $force_array=true)
Definition: Node.php:299
text($id=NULL)
Definition: Node.php:187
static html_normal_pseudoClass($p)
Definition: Node.php:663
html($id=NULL)
Definition: Node.php:142
static html_selector2struc($sel)
Definition: Node.php:723
outerHtml($id=NULL)
Definition: Node.php:165
pos($restore=true)
Definition: Node.php:237
attr($attr=NULL, $to_str=false)
Definition: Node.php:62