diff --git a/config/services.yml b/config/services.yml index 618b7c3..7e447ae 100644 --- a/config/services.yml +++ b/config/services.yml @@ -7,8 +7,10 @@ services: alias: Pcm\IconBundle\Twig\Functions\IconExtension public: true + Pcm\IconBundle\Twig\Functions\IconExtension: + tags: ['twig.extension'] public: false arguments: - # $dir: - # - '%kernel.project_dir%/public/icons' + $directories: + - '%kernel.project_dir%/public/icons' diff --git a/src/Twig/Functions/IconExtension.php b/src/Twig/Functions/IconExtension.php index fea66b6..0cb5c8e 100644 --- a/src/Twig/Functions/IconExtension.php +++ b/src/Twig/Functions/IconExtension.php @@ -4,22 +4,98 @@ declare(strict_types=1); namespace Pcm\IconBundle\Twig\Functions; +use Exception; +use InvalidArgumentException; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; +use TypeError; final class IconExtension extends AbstractExtension { + public function __construct(private array $directories) {} + + private const DEFAULT_OPTIONS = [ + 'icon' => null, + 'title' => null, + ]; + /** * @inheritDoc */ public function getFunctions(): array { return [ - new TwigFunction('icon', [$this, 'render'], [ + new TwigFunction('icon', [$this, 'renderIcon'], [ 'is_safe' => ['html'] ]) ]; } + + /** + * @param array $options + */ + public function renderIcon(array $userOptions): string + { + $options = $this->mergeUserOptionsWithDefaults($userOptions); + + $iconFilepath = $this->findSvgFilepath($options['icon']); + $rawSvgMarkup = $this->getSvgMarkup($iconFilepath); + $cleanSvgMarkup = $this->cleanSvgMarkup($rawSvgMarkup); + + if ($this->titleIsANonEmptyString($options['title'])) { + $markup = $this->addTitleToMarkup($cleanSvgMarkup, $options['title']); + } + + 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 titleIsANonEmptyString(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); + } } - +class IconNotFound extends Exception {}; diff --git a/tests/AppKernel.php b/tests/AppKernel.php new file mode 100644 index 0000000..432fa8b --- /dev/null +++ b/tests/AppKernel.php @@ -0,0 +1,32 @@ +load(__DIR__.'/services_'.$this->getEnvironment().'.yml'); + } +} diff --git a/tests/Twig/Functions/IconExtensionTest.php b/tests/Twig/Functions/IconExtensionTest.php index a45d2fb..50a48d4 100644 --- a/tests/Twig/Functions/IconExtensionTest.php +++ b/tests/Twig/Functions/IconExtensionTest.php @@ -4,11 +4,15 @@ declare(strict_types=1); namespace Pcm\IconBundle\Tests\Twig\Functions; +use Pcm\IconBundle\Tests\AppKernel; use Pcm\IconBundle\Twig\Functions\IconExtension; +use Pcm\IconBundle\Twig\Functions\IconNotFound; use PHPUnit\Framework\TestCase; class IconExtensionTest extends TestCase { + private const ICON = 'test'; + /** * @var IconExtension */ @@ -16,7 +20,9 @@ class IconExtensionTest extends TestCase protected function setUp(): void { - $this->icon = new IconExtension(); + $kernel = new AppKernel('test', false); + $kernel->boot(); + $this->icon = $kernel->getContainer()->get('pcm_icon.icon_extension'); } public function testInstanceOf(): void @@ -24,4 +30,45 @@ class IconExtensionTest extends TestCase $this->assertInstanceOf(IconExtension::class, $this->icon); } + public function testDirectoriesArrayGetsInjected(): void + { + $reflection = new \ReflectionClass($this->icon); + $property = $reflection->getProperty('directories'); + $directories = $property->getValue($this->icon); + $this->assertIsArray($directories); + $this->assertCount(1, $directories); + $this->assertStringContainsString('tests/icons', $directories[0]); + } + + public function testThrowsWhenPassedAnInvalidIconName(): void + { + $this->expectException(IconNotFound::class); + $this->icon->renderIcon(['icon' => random_bytes(8)]); + } + + public function testNoTitleExistsIfNotPassedIn(): void + { + $content = $this->icon->renderIcon(['icon' => self::ICON]); + $this->assertStringNotContainsString('', $content); + $this->assertStringNotContainsString('', $content); + } + + public function testTitleGetsAddedIfSpecified(): void + { + $title = 'test_title'; + $content = $this->icon->renderIcon(['icon' => self::ICON, 'title' => $title]); + $this->assertMatchesRegularExpression("/{$title}<\/title>/", $content); + } + + public function testTitleThrowsIfNotPassedAString(): void + { + $this->expectException(\TypeError::class); + $this->icon->renderIcon(['icon' => self::ICON, 'title' => 99]); + } + + public function testTitleThrowsIfPassedAnEmptyString():void + { + $this->expectException(\InvalidArgumentException::class); + $this->icon->renderIcon(['icon' => self::ICON, 'title' => '']); + } } diff --git a/tests/icons/test.svg b/tests/icons/test.svg new file mode 100644 index 0000000..2c9ec70 --- /dev/null +++ b/tests/icons/test.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="96 96 320 320"> + <title>Some cross lol + + + diff --git a/tests/services_test.yml b/tests/services_test.yml new file mode 100644 index 0000000..9a55bfc --- /dev/null +++ b/tests/services_test.yml @@ -0,0 +1,13 @@ +# This config is only here to stop a deprecation notice for Symfony 7 +framework: + http_method_override: false + +services: + pcm_icon.icon_extension: + alias: Pcm\IconBundle\Twig\Functions\IconExtension + public: true + + Pcm\IconBundle\Twig\Functions\IconExtension: + arguments: + $directories: + - '%kernel.project_dir%/tests/icons'