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