Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
1 / 1
CSSObjectList
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
23 / 23
57
100.00% covered (success)
100.00%
1 / 1
 testObjects
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
8
 remove
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 slice
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clear
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 seek
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 current
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 next
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 offsetSet
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 offsetUnset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getPosition
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
8
 getSeparator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toTokenOrCVArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 toTokenArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toComponentValueArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare( strict_types = 1 );
3
4/**
5 * @file
6 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
7 */
8
9namespace Wikimedia\CSS\Objects;
10
11use ArrayAccess;
12use Countable;
13use InvalidArgumentException;
14use OutOfBoundsException;
15use SeekableIterator;
16use Wikimedia\CSS\Util;
17
18/**
19 * Represent a list of CSS objects
20 * @template T of CSSObject
21 * @implements SeekableIterator<T>
22 * @implements ArrayAccess<T>
23 */
24class CSSObjectList implements Countable, SeekableIterator, ArrayAccess, CSSObject {
25
26    /** @var class-string The specific class of object contained */
27    protected static $objectType;
28
29    /** @var T[] The objects contained */
30    protected $objects;
31
32    /** @var int */
33    protected $offset = 0;
34
35    /**
36     * Additional validation for objects
37     * @param CSSObject[] $objects
38     */
39    protected static function testObjects( array $objects ) {
40    }
41
42    /**
43     * @param T[] $objects
44     */
45    public function __construct( array $objects = [] ) {
46        Util::assertAllInstanceOf( $objects, static::$objectType, static::class );
47        // @phan-suppress-next-line PhanTypeMismatchArgument
48        static::testObjects( $objects );
49        $this->objects = array_values( $objects );
50    }
51
52    /**
53     * Insert one or more objects into the list
54     * @param T|T[]|CSSObjectList<T> $objects An object to add, or an array of objects.
55     * @param int|null $index Insert the objects at this index. If omitted, the
56     *  objects are added at the end.
57     */
58    public function add( $objects, $index = null ) {
59        if ( $objects instanceof static ) {
60            $objects = $objects->objects;
61        } elseif ( is_array( $objects ) ) {
62            Util::assertAllInstanceOf( $objects, static::$objectType, static::class );
63            $objects = array_values( $objects );
64            // @phan-suppress-next-line PhanTypeMismatchArgument
65            static::testObjects( $objects );
66        } else {
67            if ( !$objects instanceof static::$objectType ) {
68                throw new InvalidArgumentException(
69                    static::class . ' may only contain instances of ' . static::$objectType . '.'
70                );
71            }
72            $objects = [ $objects ];
73            static::testObjects( $objects );
74        }
75
76        if ( $index === null ) {
77            $index = count( $this->objects );
78        } elseif ( $index < 0 || $index > count( $this->objects ) ) {
79            throw new OutOfBoundsException( 'Index is out of range.' );
80        }
81
82        array_splice( $this->objects, $index, 0, $objects );
83        if ( $this->offset > $index ) {
84            $this->offset += count( $objects );
85        }
86    }
87
88    /**
89     * Remove an object from the list
90     * @param int $index
91     * @return T The removed object
92     */
93    public function remove( $index ) {
94        if ( $index < 0 || $index >= count( $this->objects ) ) {
95            throw new OutOfBoundsException( 'Index is out of range.' );
96        }
97        $ret = $this->objects[$index];
98        array_splice( $this->objects, $index, 1 );
99
100        // This works most sanely with foreach() and removing the current index
101        if ( $this->offset >= $index ) {
102            $this->offset--;
103        }
104
105        return $ret;
106    }
107
108    /**
109     * Extract a slice of the list
110     * @param int $offset
111     * @param int|null $length
112     * @return T[] The objects in the slice
113     */
114    public function slice( $offset, $length = null ) {
115        return array_slice( $this->objects, $offset, $length );
116    }
117
118    /**
119     * Clear the list
120     */
121    public function clear() {
122        $this->objects = [];
123        $this->offset = 0;
124    }
125
126    // Countable interface
127
128    /** @inheritDoc */
129    public function count(): int {
130        return count( $this->objects );
131    }
132
133    // SeekableIterator interface
134
135    /** @inheritDoc */
136    public function seek( int $offset ): void {
137        if ( $offset < 0 || $offset >= count( $this->objects ) ) {
138            throw new OutOfBoundsException( 'Offset is out of range.' );
139        }
140        $this->offset = $offset;
141    }
142
143    /** @inheritDoc */
144    public function current(): mixed {
145        return $this->objects[$this->offset] ?? null;
146    }
147
148    /** @inheritDoc */
149    public function key(): int {
150        return $this->offset;
151    }
152
153    /** @inheritDoc */
154    public function next(): void {
155        $this->offset++;
156    }
157
158    /** @inheritDoc */
159    public function rewind(): void {
160        $this->offset = 0;
161    }
162
163    /** @inheritDoc */
164    public function valid(): bool {
165        return isset( $this->objects[$this->offset] );
166    }
167
168    // ArrayAccess interface
169
170    /** @inheritDoc */
171    public function offsetExists( $offset ): bool {
172        return isset( $this->objects[$offset] );
173    }
174
175    /**
176     * @param mixed $offset
177     * @return T
178     */
179    public function offsetGet( $offset ): CSSObject {
180        if ( !is_numeric( $offset ) || (float)(int)$offset !== (float)$offset ) {
181            throw new InvalidArgumentException( 'Offset must be an integer.' );
182        }
183        if ( $offset < 0 || $offset > count( $this->objects ) ) {
184            throw new OutOfBoundsException( 'Offset is out of range.' );
185        }
186        return $this->objects[$offset];
187    }
188
189    /** @inheritDoc */
190    public function offsetSet( $offset, $value ): void {
191        if ( !$value instanceof static::$objectType ) {
192            throw new InvalidArgumentException(
193                static::class . ' may only contain instances of ' . static::$objectType . '.'
194            );
195        }
196        static::testObjects( [ $value ] );
197        if ( !is_numeric( $offset ) || (float)(int)$offset !== (float)$offset ) {
198            throw new InvalidArgumentException( 'Offset must be an integer.' );
199        }
200        if ( $offset < 0 || $offset > count( $this->objects ) ) {
201            throw new OutOfBoundsException( 'Offset is out of range.' );
202        }
203        $this->objects[$offset] = $value;
204    }
205
206    /** @inheritDoc */
207    public function offsetUnset( $offset ): void {
208        if ( isset( $this->objects[$offset] ) && $offset !== count( $this->objects ) - 1 ) {
209            throw new OutOfBoundsException( 'Cannot leave holes in the list.' );
210        }
211        unset( $this->objects[$offset] );
212    }
213
214    // CSSObject interface
215
216    /** @inheritDoc */
217    public function getPosition() {
218        $ret = null;
219        foreach ( $this->objects as $obj ) {
220            $pos = $obj->getPosition();
221            if ( $pos[0] >= 0 && (
222                !$ret || $pos[0] < $ret[0] || ( $pos[0] === $ret[0] && $pos[1] < $ret[1] )
223            ) ) {
224                $ret = $pos;
225            }
226        }
227        return $ret ?: [ -1, -1 ];
228    }
229
230    /**
231     * Return the tokens to use to separate list items
232     * @param T $left
233     * @param ?T $right
234     * @return Token[]
235     */
236    protected function getSeparator( CSSObject $left, ?CSSObject $right = null ) {
237        return [];
238    }
239
240    /**
241     * @param string $function Function to call, toTokenArray() or toComponentValueArray()
242     * @return Token[]|ComponentValue[]
243     */
244    private function toTokenOrCVArray( $function ) {
245        $ret = [];
246        $l = count( $this->objects );
247        foreach ( $this->objects as $i => $iValue ) {
248            // Manually looping and appending turns out to be noticeably faster than array_merge.
249            foreach ( $iValue->$function() as $v ) {
250                $ret[] = $v;
251            }
252            // @phan-suppress-next-line PhanTemplateTypeConstraintViolation
253            $sep = $this->getSeparator( $iValue, $i + 1 < $l ? $this->objects[$i + 1] : null );
254            foreach ( $sep as $v ) {
255                $ret[] = $v;
256            }
257        }
258
259        return $ret;
260    }
261
262    /** @inheritDoc */
263    public function toTokenArray() {
264        return $this->toTokenOrCVArray( __FUNCTION__ );
265    }
266
267    /** @inheritDoc */
268    public function toComponentValueArray() {
269        return $this->toTokenOrCVArray( __FUNCTION__ );
270    }
271
272    public function __toString() {
273        return Util::stringify( $this );
274    }
275}