null, 'title' => null, 'size' => self::DEFAULT_SIZE, 'colour' => 'primary' ]; public function __construct(private array $directories, private array $palletes) { if (empty($this->directories)) throw new InvalidArgumentException('Directories array must contain at least one path!'); $dirsContainNonString = array_reduce($this->directories, fn($notString, $path) => $notString || !is_string($path)); if ($dirsContainNonString) throw new \TypeError('Directories array must only contain strings!'); if (empty($this->palletes)) throw new InvalidArgumentException('Palletes array must contain at least one pallet!'); $pelletesContainNonarray = array_reduce($this->palletes, fn($notArray, $path) => $notArray || !is_array($path)); if ($pelletesContainNonarray) throw new \TypeError('Palletes array must only contain arrays!'); foreach ($this->palletes as $pallete) { if (!(array_key_exists('stroke', $pallete) && array_key_exists('fill', $pallete))) { throw new \Exception('Palletes must contain a "stroke" and "fill" key!'); } } } /** * @inheritDoc */ public function getFunctions(): array { return [ new TwigFunction('icon', [$this, 'renderIcon'], [ 'is_safe' => ['html'] ]) ]; } /** * @param array $options * ``` * $options = [ * 'icon' => (string) Which icon to use * 'title' => (?string) Text to appear on mouse hover * 'size' => (int) Height and width in px * 'colour' => (string) Main colour pallete * ] * ``` */ public function renderIcon(array $userOptions): string { $options = $this->mergeUserOptionsWithDefaults($userOptions); $iconFilepath = $this->findSvgFilepath($options['icon']); $rawSvgMarkup = $this->getSvgMarkup($iconFilepath); $cleanSvgMarkup = $this->cleanSvgMarkup($rawSvgMarkup); $mainPallete = $this->getPallete($options['colour']); $xml = new \SimpleXMLElement($cleanSvgMarkup); $xml = $this->addAttributeToXmlElement($xml, 'class', $mainPallete['stroke'] . ' ' . $mainPallete['fill']); $markup = $this->removeXMLDeclaration($xml->saveXML()); if ($this->isNonEmptyString($options['title'])) $markup = $this->addTitleToMarkup($markup, $options['title']); if ($options['size'] < 0) throw new \InvalidArgumentException('Size must not be negative'); if (!is_int($options['size'])) throw new \TypeError('Size value must be an integer'); $markup = $this->setSize($markup, $options['size']); $markup = $this->removeBlackStrokeAttributes($markup); $markup = $this->removeBlackFillAttributes($markup); return $markup; } private function mergeUserOptionsWithDefaults(array $userOptions): array { return array_merge(self::DEFAULT_OPTIONS, $userOptions); } private function findSvgFilepath(string $iconName): string { foreach ($this->directories as $directory) { $potentialFilepath = sprintf('%s/%s.svg', $directory, $iconName); if (file_exists($potentialFilepath)) return $potentialFilepath; } throw new IconNotFound(sprintf('File "%s.svg" not found in %s', $iconName, implode(', ', $this->directories))); } private function getSvgMarkup(string $filepath): string { return file_get_contents($filepath); } private function cleanSvgMarkup(string $markup): string { return preg_replace('/