1: <?php
2: namespace Pharborist;
3:
4: 5: 6: 7: 8:
9: class NodeCollection implements \IteratorAggregate, \Countable, \ArrayAccess {
10: 11: 12:
13: protected $nodes;
14:
15: 16: 17: 18: 19:
20: protected static function sortUnique($nodes) {
21: $sort = [];
22: $detached = [];
23: foreach ($nodes as $node) {
24: $key = $node->sortKey();
25: if ($key[0] === '~') {
26: $detached[] = $node;
27: }
28: else {
29: $sort[$key] = $node;
30: }
31: }
32: ksort($sort, SORT_NATURAL);
33: return array_merge(array_values($sort), $detached);
34: }
35:
36: public function __construct($nodes = [], $sort = TRUE) {
37: $this->nodes = $sort ? static::sortUnique($nodes) : $nodes;
38: }
39:
40: 41: 42:
43: public function getIterator() {
44: return new \ArrayIterator($this->nodes);
45: }
46:
47: 48: 49:
50: public function count() {
51: return count($this->nodes);
52: }
53:
54: 55: 56: 57: 58: 59: 60:
61: public function offsetExists($offset) {
62: return isset($this->nodes[$offset]);
63: }
64:
65: 66: 67: 68: 69: 70: 71:
72: public function offsetGet($offset) {
73: return $this->nodes[$offset];
74: }
75:
76: 77: 78: 79: 80: 81: 82: 83:
84: public function offsetSet($offset, $value) {
85: throw new \BadMethodCallException('NodeCollection offsetSet not supported');
86: }
87:
88: 89: 90: 91: 92: 93: 94:
95: public function offsetUnset($offset) {
96: throw new \BadMethodCallException('NodeCollection offsetUnset not supported');
97: }
98:
99:
100: 101: 102: 103: 104:
105: public function isEmpty() {
106: return $this->count() == 0;
107: }
108:
109: 110: 111: 112: 113:
114: public function isNotEmpty() {
115: return $this->count() > 0;
116: }
117:
118: 119: 120: 121:
122: public function reverse() {
123: return array_reverse($this->nodes);
124: }
125:
126: 127: 128: 129: 130: 131:
132: public function slice($start_index, $end_index = NULL) {
133: if ($start_index < 0) {
134: $start_index = $this->count() + $start_index;
135: }
136: if ($end_index < 0) {
137: $end_index = $this->count() + $end_index;
138: }
139: $last_index = $this->count() - 1;
140: if ($start_index > $last_index) {
141: $start_index = $last_index;
142: }
143: if ($end_index !== NULL) {
144: if ($end_index > $last_index) {
145: $end_index = $last_index;
146: }
147: if ($start_index > $end_index) {
148: $start_index = $end_index;
149: }
150: $length = $end_index - $start_index;
151: }
152: else {
153: $length = $this->count() - $start_index;
154: }
155: return new NodeCollection(array_slice($this->nodes, $start_index, $length));
156: }
157:
158: 159: 160: 161: 162: 163: 164: 165: 166:
167: public function map(callable $callback) {
168: return new NodeCollection(array_map($callback, $this->nodes));
169: }
170:
171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183:
184: public function reduce(callable $callback, $initial = NULL) {
185: return array_reduce($this->nodes, $callback, $initial);
186: }
187:
188: 189: 190: 191: 192: 193:
194: public function toArray() {
195: return $this->nodes;
196: }
197:
198: 199: 200: 201: 202: 203: 204: 205:
206: public function get($index) {
207: return $this->nodes[$index];
208: }
209:
210: 211: 212: 213: 214: 215: 216: 217: 218:
219: public function indexOf(callable $callback) {
220: foreach ($this->nodes as $i => $node) {
221: if ($callback($node)) {
222: return $i;
223: }
224: }
225: return -1;
226: }
227:
228: 229: 230: 231: 232: 233: 234: 235: 236:
237: public function is(callable $callback) {
238: foreach ($this->nodes as $node) {
239: if ($callback($node)) {
240: return TRUE;
241: }
242: }
243: return FALSE;
244: }
245:
246: 247: 248: 249: 250: 251:
252: public function parent(callable $callback = NULL) {
253: $matches = [];
254: foreach ($this->nodes as $node) {
255: if ($match = $node->parent($callback)) {
256: $matches[] = $match;
257: }
258: }
259: return new NodeCollection($matches);
260: }
261:
262: 263: 264: 265: 266: 267:
268: public function parents(callable $callback = NULL) {
269: $matches = [];
270: foreach ($this->nodes as $node) {
271: $matches = array_merge($matches, $node->parents($callback)->nodes);
272: }
273: return new NodeCollection($matches);
274: }
275:
276: 277: 278: 279: 280: 281: 282:
283: public function parentsUntil(callable $callback, $inclusive = FALSE) {
284: $matches = [];
285: foreach ($this->nodes as $node) {
286: $matches = array_merge($matches, $node->parentsUntil($callback, $inclusive)->nodes);
287: }
288: return new NodeCollection($matches);
289: }
290:
291: 292: 293: 294: 295: 296: 297:
298: public function closest(callable $callback) {
299: $matches = [];
300: foreach ($this->nodes as $node) {
301: if ($match = $node->closest($callback)) {
302: $matches[] = $match;
303: }
304: }
305: return new NodeCollection($matches);
306: }
307:
308: 309: 310: 311: 312:
313: public function children(callable $callback = NULL) {
314: $matches = [];
315: foreach ($this->nodes as $node) {
316: if ($node instanceof ParentNode) {
317: $matches = array_merge($matches, $node->children($callback)->nodes);
318: }
319: }
320: return new NodeCollection($matches);
321: }
322:
323: 324: 325: 326: 327:
328: public function clear() {
329: foreach ($this->nodes as $node) {
330: if ($node instanceof ParentNode) {
331: $node->clear();
332: }
333: }
334: return $this;
335: }
336:
337: 338: 339: 340: 341: 342: 343:
344: public function previous(callable $callback = NULL) {
345: $matches = [];
346: foreach ($this->nodes as $node) {
347: if ($match = $node->previous($callback)) {
348: $matches[] = $match;
349: }
350: }
351: return new NodeCollection($matches);
352: }
353:
354: 355: 356: 357: 358: 359:
360: public function previousAll(callable $callback = NULL) {
361: $matches = [];
362: foreach ($this->nodes as $node) {
363: $matches = array_merge($matches, $node->previousAll($callback)->nodes);
364: }
365: return new NodeCollection($matches);
366: }
367:
368: 369: 370: 371: 372: 373: 374:
375: public function previousUntil(callable $callback, $inclusive = FALSE) {
376: $matches = [];
377: foreach ($this->nodes as $node) {
378: $matches = array_merge($matches, $node->previousUntil($callback, $inclusive)->nodes);
379: }
380: return new NodeCollection($matches);
381: }
382:
383: 384: 385: 386: 387: 388: 389:
390: public function next(callable $callback = NULL) {
391: $matches = [];
392: foreach ($this->nodes as $node) {
393: if ($match = $node->next($callback)) {
394: $matches[] = $match;
395: }
396: }
397: return new NodeCollection($matches);
398: }
399:
400: 401: 402: 403: 404: 405:
406: public function nextAll(callable $callback = NULL) {
407: $matches = [];
408: foreach ($this->nodes as $node) {
409: $matches = array_merge($matches, $node->nextAll($callback)->nodes);
410: }
411: return new NodeCollection($matches);
412: }
413:
414: 415: 416: 417: 418: 419: 420:
421: public function nextUntil(callable $callback, $inclusive = FALSE) {
422: $matches = [];
423: foreach ($this->nodes as $node) {
424: $matches = array_merge($matches, $node->nextUntil($callback, $inclusive)->nodes);
425: }
426: return new NodeCollection($matches);
427: }
428:
429: 430: 431: 432: 433: 434:
435: public function siblings(callable $callback = NULL) {
436: $matches = [];
437: foreach ($this->nodes as $node) {
438: $matches = array_merge($matches, $node->siblings($callback)->nodes);
439: }
440: return new NodeCollection($matches);
441: }
442:
443: 444: 445: 446: 447: 448:
449: public function find(callable $callback) {
450: $matches = [];
451: foreach ($this->nodes as $node) {
452: if ($node instanceof ParentNode) {
453: $matches = array_merge($matches, $node->find($callback)->nodes);
454: }
455: }
456: return new NodeCollection($matches);
457: }
458:
459: 460: 461: 462: 463:
464: public function filter(callable $callback) {
465: $matches = [];
466: foreach ($this->nodes as $index => $node) {
467: if ($callback($node, $index)) {
468: $matches[] = $node;
469: }
470: }
471: return new NodeCollection($matches, FALSE);
472: }
473:
474: 475: 476: 477: 478:
479: public function not(callable $callback) {
480: $matches = [];
481: foreach ($this->nodes as $index => $node) {
482: if (!$callback($node, $index)) {
483: $matches[] = $node;
484: }
485: }
486: return new NodeCollection($matches, FALSE);
487: }
488:
489: 490: 491: 492: 493: 494:
495: public function has(callable $callback) {
496: $matches = [];
497: foreach ($this->nodes as $node) {
498: if ($node instanceof ParentNode && $node->has($callback)) {
499: $matches[] = $node;
500: }
501: }
502: return new NodeCollection($matches, FALSE);
503: }
504:
505: 506: 507:
508: public function first() {
509: $matches = [];
510: if (!empty($this->nodes)) {
511: $matches[] = $this->nodes[0];
512: }
513: return new NodeCollection($matches, FALSE);
514: }
515:
516: 517: 518:
519: public function last() {
520: $matches = [];
521: if (!empty($this->nodes)) {
522: $matches[] = $this->nodes[count($this->nodes) - 1];
523: }
524: return new NodeCollection($matches, FALSE);
525: }
526:
527: 528: 529: 530: 531:
532: public function insertBefore($targets) {
533: foreach ($this->nodes as $node) {
534: $node->insertBefore($targets);
535: }
536: return $this;
537: }
538:
539: 540: 541: 542: 543:
544: public function before($nodes) {
545: foreach ($this->nodes as $i => $node) {
546: $node->before($i === 0 ? $nodes : clone $nodes);
547: }
548: return $this;
549: }
550:
551: 552: 553: 554: 555:
556: public function insertAfter($targets) {
557: foreach ($this->nodes as $node) {
558: $node->insertAfter($targets);
559: }
560: return $this;
561: }
562:
563: 564: 565: 566: 567:
568: public function after($nodes) {
569: foreach ($this->nodes as $i => $node) {
570: $node->after($i === 0 ? $nodes : clone $nodes);
571: }
572: return $this;
573: }
574:
575: 576: 577: 578:
579: public function remove() {
580: foreach ($this->nodes as $node) {
581: $node->remove();
582: }
583: return $this;
584: }
585:
586: 587: 588: 589: 590: 591:
592: public function replaceWith($nodes) {
593: $first = TRUE;
594: foreach ($this->nodes as $node) {
595: if (!$first) {
596: if (is_array($nodes)) {
597: $nodes = new NodeCollection($nodes, FALSE);
598: }
599: $nodes = clone $nodes;
600: }
601: $node->replaceWith($nodes);
602: $first = FALSE;
603: }
604: return $this;
605: }
606:
607: 608: 609: 610: 611:
612: public function replaceAll($targets) {
613: if ($targets instanceof Node) {
614: $targets->replaceWith($this->nodes);
615: }
616: elseif ($targets instanceof NodeCollection || is_array($targets)) {
617: $first = TRUE;
618:
619: foreach ($targets as $target) {
620: $target->replaceWith($first ? $this->nodes : clone $this);
621: $first = FALSE;
622: }
623: }
624: return $this;
625: }
626:
627: 628: 629: 630: 631: 632:
633: public function add($nodes) {
634: if ($nodes instanceof Node) {
635: $this->nodes[] = $nodes;
636: }
637: elseif ($nodes instanceof NodeCollection) {
638: $this->nodes = array_merge($this->nodes, $nodes->nodes);
639: }
640: elseif (is_array($nodes)) {
641: $this->nodes = array_merge($this->nodes, $nodes);
642: }
643: else {
644: throw new \InvalidArgumentException();
645: }
646: $this->nodes = static::sortUnique($this->nodes);
647: return $this;
648: }
649:
650: 651: 652: 653: 654: 655: 656: 657:
658: public function addTo(NodeCollection $collection) {
659: return $collection->add($this);
660: }
661:
662: 663: 664: 665: 666: 667:
668: public function each(callable $callback) {
669: array_walk($this->nodes, $callback);
670: return $this;
671: }
672:
673: public function __clone() {
674: $copy = [];
675: foreach ($this->nodes as $node) {
676: $copy[] = clone $node;
677: }
678: $this->nodes = $copy;
679: }
680: }
681: