Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
87 / 87
Wikimedia\CSS\Objects\CSSObjectList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
23 / 23
57
100.00% covered (success)
100.00%
87 / 87
 testObjects
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 add
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
19 / 19
 remove
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 slice
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 clear
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 count
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 seek
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
4 / 4
 current
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 key
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 next
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 rewind
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 valid
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
5 / 5
 offsetSet
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
10 / 10
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
4 / 4
 getPosition
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
7 / 7
 getSeparator
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 toTokenOrCVArray
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
9 / 9
 toTokenArray
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 toComponentValueArray
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 __toString
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
<?php
/**
 * @file
 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
 */
namespace Wikimedia\CSS\Objects;
use Wikimedia\CSS\Util;
/**
 * Represent a list of CSS objects
 */
class CSSObjectList implements \Countable, \SeekableIterator, \ArrayAccess, CSSObject {
    /** @var string The specific class of object contained */
    protected static $objectType;
    /** @var CSSObject[] The objects contained */
    protected $objects;
    /** @var int */
    protected $offset = 0;
    /**
     * Additional validation for objects
     * @param CSSObject[] $objects
     */
    protected static function testObjects( array $objects ) {
    }
    /**
     * @param CSSObject[] $objects
     */
    public function __construct( array $objects = [] ) {
        Util::assertAllInstanceOf( $objects, static::$objectType, static::class );
        static::testObjects( $objects );
        $this->objects = array_values( $objects );
    }
    /**
     * Insert one or more objects into the list
     * @param CSSObject|CSSObject[]|CSSObjectList $objects An object to add, or an array of objects.
     * @param int|null $index Insert the objects at this index. If omitted, the
     *  objects are added at the end.
     */
    public function add( $objects, $index = null ) {
        if ( $objects instanceof static ) {
            $objects = $objects->objects;
        } elseif ( is_array( $objects ) ) {
            Util::assertAllInstanceOf( $objects, static::$objectType, static::class );
            $objects = array_values( $objects );
            static::testObjects( $objects );
        } else {
            if ( !$objects instanceof static::$objectType ) {
                throw new \InvalidArgumentException(
                    static::class . ' may only contain instances of ' . static::$objectType . '.'
                );
            }
            $objects = [ $objects ];
            static::testObjects( $objects );
        }
        if ( $index === null ) {
            $index = count( $this->objects );
        } elseif ( $index < 0 || $index > count( $this->objects ) ) {
            throw new \OutOfBoundsException( 'Index is out of range.' );
        }
        array_splice( $this->objects, $index, 0, $objects );
        if ( $this->offset > $index ) {
            $this->offset += count( $objects );
        }
    }
    /**
     * Remove an object from the list
     * @param int $index
     * @return CSSObject The removed object
     */
    public function remove( $index ) {
        if ( $index < 0 || $index >= count( $this->objects ) ) {
            throw new \OutOfBoundsException( 'Index is out of range.' );
        }
        $ret = $this->objects[$index];
        array_splice( $this->objects, $index, 1 );
        // This works most sanely with foreach() and removing the current index
        if ( $this->offset >= $index ) {
            $this->offset--;
        }
        return $ret;
    }
    /**
     * Extract a slice of the list
     * @param int $offset
     * @param int|null $length
     * @return CSSObject[] The objects in the slice
     */
    public function slice( $offset, $length = null ) {
        return array_slice( $this->objects, $offset, $length );
    }
    /**
     * Clear the list
     */
    public function clear() {
        $this->objects = [];
        $this->offset = 0;
    }
    // \Countable interface
    /** @inheritDoc */
    public function count() {
        return count( $this->objects );
    }
    // \SeekableIterator interface
    /** @inheritDoc */
    public function seek( $offset ) {
        if ( $offset < 0 || $offset >= count( $this->objects ) ) {
            throw new \OutOfBoundsException( 'Offset is out of range.' );
        }
        $this->offset = $offset;
    }
    /** @inheritDoc */
    public function current() {
        return $this->objects[$this->offset] ?? null;
    }
    /** @inheritDoc */
    public function key() {
        return $this->offset;
    }
    /** @inheritDoc */
    public function next() {
        $this->offset++;
    }
    /** @inheritDoc */
    public function rewind() {
        $this->offset = 0;
    }
    /** @inheritDoc */
    public function valid() {
        return isset( $this->objects[$this->offset] );
    }
    // \ArrayAccess interface
    /** @inheritDoc */
    public function offsetExists( $offset ) {
        return isset( $this->objects[$offset] );
    }
    /** @inheritDoc */
    public function offsetGet( $offset ) {
        if ( !is_numeric( $offset ) || (float)(int)$offset !== (float)$offset ) {
            throw new \InvalidArgumentException( 'Offset must be an integer.' );
        }
        if ( $offset < 0 || $offset > count( $this->objects ) ) {
            throw new \OutOfBoundsException( 'Offset is out of range.' );
        }
        return $this->objects[$offset];
    }
    /** @inheritDoc */
    public function offsetSet( $offset, $value ) {
        if ( !$value instanceof static::$objectType ) {
            throw new \InvalidArgumentException(
                static::class . ' may only contain instances of ' . static::$objectType . '.'
            );
        }
        static::testObjects( [ $value ] );
        if ( !is_numeric( $offset ) || (float)(int)$offset !== (float)$offset ) {
            throw new \InvalidArgumentException( 'Offset must be an integer.' );
        }
        if ( $offset < 0 || $offset > count( $this->objects ) ) {
            throw new \OutOfBoundsException( 'Offset is out of range.' );
        }
        $this->objects[$offset] = $value;
    }
    /** @inheritDoc */
    public function offsetUnset( $offset ) {
        if ( isset( $this->objects[$offset] ) && $offset !== count( $this->objects ) - 1 ) {
            throw new \OutOfBoundsException( 'Cannot leave holes in the list.' );
        }
        unset( $this->objects[$offset] );
    }
    // CSSObject interface
    /** @inheritDoc */
    public function getPosition() {
        $ret = null;
        foreach ( $this->objects as $obj ) {
            $pos = $obj->getPosition();
            if ( $pos[0] >= 0 && (
                !$ret || $pos[0] < $ret[0] || $pos[0] === $ret[0] && $pos[1] < $ret[1]
            ) ) {
                $ret = $pos;
            }
        }
        return $ret ?: [ -1, -1 ];
    }
    /**
     * Return the tokens to use to separate list items
     * @param CSSObject $left
     * @param CSSObject|null $right
     * @return Token[]
     */
    protected function getSeparator( CSSObject $left, CSSObject $right = null ) {
        return [];
    }
    /**
     * @param string $function Function to call, toTokenArray() or toComponentValueArray()
     */
    private function toTokenOrCVArray( $function ) {
        $ret = [];
        $l = count( $this->objects );
        for ( $i = 0; $i < $l; $i++ ) {
            // Manually looping and appending turns out to be noticably faster than array_merge.
            foreach ( $this->objects[$i]->$function() as $v ) {
                $ret[] = $v;
            }
            $sep = $this->getSeparator( $this->objects[$i], $i + 1 < $l ? $this->objects[$i + 1] : null );
            foreach ( $sep as $v ) {
                $ret[] = $v;
            }
        }
        return $ret;
    }
    /** @inheritDoc */
    public function toTokenArray() {
        return $this->toTokenOrCVArray( __FUNCTION__ );
    }
    /** @inheritDoc */
    public function toComponentValueArray() {
        return $this->toTokenOrCVArray( __FUNCTION__ );
    }
    public function __toString() {
        return Util::stringify( $this );
    }
}