meyfa/php-svg

View on GitHub
src/Nodes/SVGNodeContainer.php

Summary

Maintainability
A
1 hr
Test Coverage
B
85%
<?php

namespace SVG\Nodes;

use SVG\Nodes\Structures\SVGStyle;
use SVG\Rasterization\SVGRasterizer;
use SVG\Rasterization\Transform\TransformParser;
use SVG\Shims\Str;
use SVG\Utilities\SVGStyleParser;

/**
 * Represents an SVG image element that contains child elements.
 */
abstract class SVGNodeContainer extends SVGNode
{
    /**
     * @var SVGNode[] $children This node's child nodes.
     */
    protected array $children;

    /**
     * @var string[] $globalStyles A 2D array mapping CSS selectors to values.
     */
    protected array $containerStyles;

    public function __construct()
    {
        parent::__construct();

        $this->containerStyles = [];
        $this->children = [];
    }

    /**
     * Inserts an SVGNode instance at the given index, or, if no index is given,
     * at the end of the child list.
     * Does nothing if the node already exists in this container.
     *
     * @param SVGNode  $node  The node to add to this container's children.
     * @param int|null $index The position to insert at (optional).
     *
     * @return $this This node instance, for call chaining.
     */
    public function addChild(SVGNode $node, int $index = null): SVGNodeContainer
    {
        if ($node === $this || $node->parent === $this) {
            return $this;
        }

        if (isset($node->parent)) {
            $node->parent->removeChild($node);
        }

        $index ??= count($this->children);

        // insert and set new parent
        array_splice($this->children, $index, 0, [$node]);
        $node->parent = $this;

        if ($node instanceof SVGStyle) {
            // if node is SVGStyle then add rules to container's style
            $this->addContainerStyle($node);
        }

        return $this;
    }

    /**
     * Removes a child node, given either as its instance or as the index it's
     * located at, from this container.
     *
     * @param SVGNode|int $child The node (or respective index) to remove.
     *
     * @return $this This node instance, for call chaining.
     */
    public function removeChild($child): SVGNodeContainer
    {
        $index = $this->resolveChildIndex($child);
        if ($index === false) {
            return $this;
        }

        $node         = $this->children[$index];
        $node->parent = null;

        array_splice($this->children, $index, 1);

        return $this;
    }

    /**
     * Replaces a child node with another node.
     *
     * @param SVGNode|int $child The node (or respective index) to replace.
     * @param SVGNode     $node  The replacement node.
     *
     * @return $this This node instance, for call chaining.
     */
    public function setChild($child, SVGNode $node): SVGNodeContainer
    {
        $index = $this->resolveChildIndex($child);
        if ($index === false) {
            return $this;
        }

        $this->removeChild($index);
        $this->addChild($node, $index);

        return $this;
    }

    /**
     * Resolves a child node to its index. If an index is given, it is returned
     * without modification.
     *
     * @param SVGNode|int $nodeOrIndex The node (or respective index).
     *
     * @return int|false The index, or false if argument invalid or not a child.
     */
    private function resolveChildIndex($nodeOrIndex)
    {
        if (is_int($nodeOrIndex)) {
            return $nodeOrIndex;
        } elseif ($nodeOrIndex instanceof SVGNode) {
            return array_search($nodeOrIndex, $this->children, true);
        }

        return false;
    }

    /**
     * @return int The amount of children in this container.
     */
    public function countChildren(): int
    {
        return count($this->children);
    }

    /**
     * @param int $index The index of the child to get.
     * @return SVGNode The child node at the given index.
     */
    public function getChild(int $index): SVGNode
    {
        return $this->children[$index];
    }

    /**
     * Adds the SVGStyle element rules to container's styles.
     *
     * @param SVGStyle $styleNode The style node to add rules from.
     *
     * @return $this This node instance, for call chaining.
     */
    public function addContainerStyle(SVGStyle $styleNode): SVGNodeContainer
    {
        $newStyles = SVGStyleParser::parseCss($styleNode->getValue());
        $this->containerStyles = array_merge($this->containerStyles, $newStyles);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function rasterize(SVGRasterizer $rasterizer): void
    {
        if ($this->getComputedStyle('display') === 'none') {
            return;
        }

        // 'visibility' can be overridden -> only applied in shape nodes.

        TransformParser::parseTransformString($this->getAttribute('transform'), $rasterizer->pushTransform());

        foreach ($this->children as $child) {
            $child->rasterize($rasterizer);
        }

        $rasterizer->popTransform();
    }

    /**
     * Returns a node's 'global' style rules.
     *
     * @param SVGNode $node The node for which we need to obtain.
     * its container style rules.
     *
     * @return string[] The style rules to be applied.
     */
    public function getContainerStyleForNode(SVGNode $node): array
    {
        $pattern = $node->getIdAndClassPattern();

        return $this->getContainerStyleByPattern($pattern);
    }

    /**
     * Returns style rules for the given node id + class pattern.
     *
     * @param string|null $pattern The node's pattern.
     *
     * @return string[] The style rules to be applied.
     */
    public function getContainerStyleByPattern(?string $pattern): array
    {
        if ($pattern === null) {
            return [];
        }

        $nodeStyles = [];
        if (!empty($this->parent)) {
            $nodeStyles = $this->parent->getContainerStyleByPattern($pattern);
        }

        $keys = $this->pregGrepStyle($pattern);
        foreach ($keys as $key) {
            $nodeStyles = array_merge($nodeStyles, $this->containerStyles[$key]);
        }

        return $nodeStyles;
    }

    /**
     * Returns the array consisting of the keys of the style rules that match
     * the given pattern.
     *
     * @param string $pattern The pattern to search for.
     *
     * @return string[] The matches array
     */
    private function pregGrepStyle(string $pattern): array
    {
        return preg_grep($pattern, array_keys($this->containerStyles));
    }

    /**
     * @inheritdoc
     */
    public function getElementsByTagName(string $tagName, array &$result = []): array
    {
        foreach ($this->children as $child) {
            if ($tagName === '*' || $child->getName() === $tagName) {
                $result[] = $child;
            }
            $child->getElementsByTagName($tagName, $result);
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function getElementsByClassName($className, array &$result = []): array
    {
        if (!is_array($className)) {
            $className = preg_split('/\s+/', Str::trim($className));
        }
        // shortcut if empty
        if (empty($className) || $className[0] === '') {
            return $result;
        }

        foreach ($this->children as $child) {
            $class = ' ' . $child->getAttribute('class') . ' ';
            $allMatch = true;
            foreach ($className as $cn) {
                if (strpos($class, ' ' . $cn . ' ') === false) {
                    $allMatch = false;
                    break;
                }
            }
            if ($allMatch) {
                $result[] = $child;
            }
            $child->getElementsByClassName($className, $result);
        }

        return $result;
    }
}