4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
7 namespace Wikimedia\CSS\Objects;
9 use Wikimedia\CSS\Util;
12 * Represent a list of CSS objects
14 class CSSObjectList implements \Countable, \SeekableIterator, \ArrayAccess, CSSObject {
16 /** @var string The specific class of object contained */
17 protected static $objectType;
19 /** @var CSSObject[] The objects contained */
23 protected $offset = 0;
26 * Additional validation for objects
27 * @param CSSObject[] $objects
29 protected static function testObjects( array $objects ) {
33 * @param CSSObject[] $objects
35 public function __construct( array $objects = [] ) {
36 Util::assertAllInstanceOf( $objects, static::$objectType, static::class );
37 static::testObjects( $objects );
38 $this->objects = array_values( $objects );
42 * Insert one or more objects into the list
43 * @param CSSObject|CSSObject[]|CSSObjectList $objects An object to add, or an array of objects.
44 * @param int $index Insert the objects at this index. If omitted, the
45 * objects are added at the end.
47 public function add( $objects, $index = null ) {
48 if ( $objects instanceof static ) {
49 $objects = $objects->objects;
50 } elseif ( is_array( $objects ) ) {
51 Util::assertAllInstanceOf( $objects, static::$objectType, static::class );
52 $objects = array_values( $objects );
53 static::testObjects( $objects );
55 if ( !$objects instanceof static::$objectType ) {
56 throw new \InvalidArgumentException(
57 static::class . ' may only contain instances of ' . static::$objectType . '.'
60 $objects = [ $objects ];
61 static::testObjects( $objects );
64 if ( $index === null ) {
65 $index = count( $this->objects );
66 } elseif ( $index < 0 || $index > count( $this->objects ) ) {
67 throw new \OutOfBoundsException( 'Index is out of range.' );
70 array_splice( $this->objects, $index, 0, $objects );
71 if ( $this->offset > $index ) {
72 $this->offset += count( $objects );
77 * Remove an object from the list
79 * @return CSSObject The removed object
81 public function remove( $index ) {
82 if ( $index < 0 || $index >= count( $this->objects ) ) {
83 throw new \OutOfBoundsException( 'Index is out of range.' );
85 $ret = $this->objects[$index];
86 array_splice( $this->objects, $index, 1 );
88 // This works most sanely with foreach() and removing the current index
89 if ( $this->offset >= $index ) {
97 * Extract a slice of the list
99 * @param int|null $length
100 * @return CSSObject[] The objects in the slice
102 public function slice( $offset, $length = null ) {
103 return array_slice( $this->objects, $offset, $length );
109 public function clear() {
114 // \Countable interface
116 public function count() {
117 return count( $this->objects );
120 // \SeekableIterator interface
122 public function seek( $offset ) {
123 if ( $offset < 0 || $offset >= count( $this->objects ) ) {
124 throw new \OutOfBoundsException( 'Offset is out of range.' );
126 $this->offset = $offset;
129 public function current() {
130 return isset( $this->objects[$this->offset] ) ? $this->objects[$this->offset] : null;
133 public function key() {
134 return $this->offset;
137 public function next() {
141 public function rewind() {
145 public function valid() {
146 return isset( $this->objects[$this->offset] );
149 // \ArrayAccess interface
151 public function offsetExists( $offset ) {
152 return isset( $this->objects[$offset] );
155 public function offsetGet( $offset ) {
156 if ( !is_numeric( $offset ) || (float)(int)$offset !== (float)$offset ) {
157 throw new \InvalidArgumentException( 'Offset must be an integer.' );
159 if ( $offset < 0 || $offset > count( $this->objects ) ) {
160 throw new \OutOfBoundsException( 'Offset is out of range.' );
162 return $this->objects[$offset];
165 public function offsetSet( $offset, $value ) {
166 if ( !$value instanceof static::$objectType ) {
167 throw new \InvalidArgumentException(
168 static::class . ' may only contain instances of ' . static::$objectType . '.'
171 static::testObjects( [ $value ] );
172 if ( !is_numeric( $offset ) || (float)(int)$offset !== (float)$offset ) {
173 throw new \InvalidArgumentException( 'Offset must be an integer.' );
175 if ( $offset < 0 || $offset > count( $this->objects ) ) {
176 throw new \OutOfBoundsException( 'Offset is out of range.' );
178 $this->objects[$offset] = $value;
181 public function offsetUnset( $offset ) {
182 if ( isset( $this->objects[$offset] ) && $offset !== count( $this->objects ) - 1 ) {
183 throw new \OutOfBoundsException( 'Cannot leave holes in the list.' );
185 unset( $this->objects[$offset] );
188 // CSSObject interface
190 public function getPosition() {
192 foreach ( $this->objects as $obj ) {
193 $pos = $obj->getPosition();
194 if ( $pos[0] >= 0 && (
195 !$ret || $pos[0] < $ret[0] || $pos[0] === $ret[0] && $pos[1] < $ret[1]
200 return $ret ?: [ -1, -1 ];
204 * Return the tokens to use to separate list items
205 * @param CSSObject $left
206 * @param CSSObject|null $right
209 protected function getSeparator( CSSObject $left, CSSObject $right = null ) {
214 * @param string $function Function to call, toTokenArray() or toComponentValueArray()
216 private function toTokenOrCVArray( $function ) {
218 $l = count( $this->objects );
219 for ( $i = 0; $i < $l; $i++ ) {
220 // Manually looping and appending turns out to be noticably faster than array_merge.
221 foreach ( $this->objects[$i]->$function() as $v ) {
224 $sep = $this->getSeparator( $this->objects[$i], $i + 1 < $l ? $this->objects[$i + 1] : null );
225 foreach ( $sep as $v ) {
232 public function toTokenArray() {
233 return $this->toTokenOrCVArray( __FUNCTION__ );
236 public function toComponentValueArray() {
237 return $this->toTokenOrCVArray( __FUNCTION__ );
240 public function __toString() {
241 return Util::stringify( $this );