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->getMainPallete($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('/.*<\/title>/', '', $markup); } private function getMainPallete(string $palleteName): array { if (array_key_exists($palleteName, $this->palletes)) return $this->palletes[$palleteName]; throw new PalleteNotFound("The pallete '$palleteName' was not found!"); } private function isNonEmptyString(mixed $title): bool { if (!is_string($title) && null !== $title) throw new \TypeError('Title must be a string!'); if ('' === $title) throw new \InvalidArgumentException('Title string must not be empty!'); return true; } private function addTitleToMarkup(string $markup, ?string $title): string { if (null === $title) return $markup; return preg_replace('/(<svg(.|\n)*?>\n?)/', "$1<title>$title", $markup); } private function setSize(string $content, int $size): string { $svgAsXmlElement = new \SimpleXMLElement($content); $svgAsXmlElement = $this->addAttributeToXmlElement($svgAsXmlElement, 'width', $size); $svgAsXmlElement = $this->addAttributeToXmlElement($svgAsXmlElement, 'height', $size); return $this->removeXMLDeclaration($svgAsXmlElement->saveXML()); } private function removeXMLDeclaration(string $content): string { return trim(preg_replace('/<\?xml.*\?>/', '', $content)); } private function addAttributeToXmlElement(\SimpleXMLElement $xml, string $attrName, mixed $attrValue): \SimpleXMLElement { if (isset($xml->attributes()->$attrName)) { $xml->attributes()->$attrName = strval($attrValue); } else { $xml->addAttribute($attrName, strval($attrValue)); } return $xml; } private function removeBlackStrokeAttributes(string $content): string { return preg_replace('/stroke="\s*(#0{6}|#000|rgb\(\s*0,\s*0,\s*0\s*\)|black)\s*"/', '', $content); } private function removeBlackFillAttributes(string $content): string { return preg_replace('/fill="\s*(#0{6}|#000|rgb\(\s*0,\s*0,\s*0\s*\)|black)\s*"/', '', $content); } } class IconNotFound extends \Exception {}; class PalleteNotFound extends \Exception {}; // class="stroke-neutral-400 fill-neutral-400" // hover:stroke-ses-highlight // group-hover:stroke-ses-highlight // hover:fill-ses-highlight // group-hover:fill-ses-highlight // cursor-pointer