Compare commits
13 Commits
develop
...
issue/10-i
| Author | SHA1 | Date | |
|---|---|---|---|
| ade021b8a6 | |||
| 5b13a21a82 | |||
| 1b917b4cac | |||
| 440053bf4f | |||
| a8af463c5c | |||
| 3f27e50c32 | |||
| 3da5e00361 | |||
| a19f1f4438 | |||
| 6e2228b835 | |||
| 15ee51b884 | |||
| 6ab2baeb4e | |||
| 33892f81aa | |||
| bb7862c6ac |
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [X.X.X] - XXXX-XX-XX
|
||||||
|
- Allow passing in a space-separated string to the `classes` option
|
||||||
|
- Add new exceptions and messages to make debugging a little bit faster
|
||||||
|
- Set `aria-hidden` attribute depending on whether a `title` has been set
|
||||||
|
|
||||||
|
## [1.0.0] - 2023-06-27
|
||||||
|
- First major version release
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# DEPRECATED in favour of Symfony UX Icon
|
|
||||||
|
|
||||||
# PCM Icon Bundle
|
# PCM Icon Bundle
|
||||||
|
|
||||||
Use icons inside of Twig templates with ease! Project must be using Tailwind for the colours to work properly.
|
Use icons inside of Twig templates with ease! Project must be using Tailwind for the colours to work properly.
|
||||||
@@ -64,7 +62,7 @@ COLOUR:
|
|||||||
|
|
||||||
`hover (string)` Name of the colour to use when the icon is hovered over
|
`hover (string)` Name of the colour to use when the icon is hovered over
|
||||||
|
|
||||||
`classes (string[])` Additional classes to add to the icon. This can cause Tailwind class collisions, so only use it as a last resort.
|
`classes (string[]|string)` Additional classes to add to the icon as either an array of strings or a space-separated string. This can cause Tailwind class collisions, so it is not recommended to use.
|
||||||
|
|
||||||
## Reminders
|
## Reminders
|
||||||
Remember to add `./config/packages/*.yaml` as a value in your Tailwind config content array if it does not already exist. This will ensure Tailwind classes inside of the config file get compiled.
|
Remember to add `./config/packages/*.yaml` as a value in your Tailwind config content array if it does not already exist. This will ensure Tailwind classes inside of the config file get compiled.
|
||||||
|
|||||||
7
src/Exception/EmptyFileException.php
Normal file
7
src/Exception/EmptyFileException.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Exception;
|
||||||
|
|
||||||
|
class EmptyFileException extends \Exception {};
|
||||||
7
src/Exception/InvalidSvgException.php
Normal file
7
src/Exception/InvalidSvgException.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Exception;
|
||||||
|
|
||||||
|
class InvalidSvgException extends \Exception {};
|
||||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace Pcm\IconBundle\Twig\Runtime;
|
namespace Pcm\IconBundle\Twig\Runtime;
|
||||||
|
|
||||||
use Pcm\IconBundle\Exception\ColourNotFound;
|
use Pcm\IconBundle\Exception\ColourNotFound;
|
||||||
|
use Pcm\IconBundle\Exception\EmptyFileException;
|
||||||
use Pcm\IconBundle\Exception\IconNotFound;
|
use Pcm\IconBundle\Exception\IconNotFound;
|
||||||
|
use Pcm\IconBundle\Exception\InvalidSvgException;
|
||||||
use Twig\Extension\RuntimeExtensionInterface;
|
use Twig\Extension\RuntimeExtensionInterface;
|
||||||
|
|
||||||
final class IconRuntime implements RuntimeExtensionInterface
|
final class IconRuntime implements RuntimeExtensionInterface
|
||||||
@@ -16,7 +18,7 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
'size' => null,
|
'size' => null,
|
||||||
'colour' => null,
|
'colour' => null,
|
||||||
'hover' => null,
|
'hover' => null,
|
||||||
'classes' => [],
|
'classes' => "",
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(private array $defaultOptions, private array $directories, private array $colours)
|
public function __construct(private array $defaultOptions, private array $directories, private array $colours)
|
||||||
@@ -26,12 +28,13 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
* @param array $options
|
* @param array $options
|
||||||
* ```
|
* ```
|
||||||
* $options = [
|
* $options = [
|
||||||
* 'icon' => (string) **REQUIRED** Icon name without trailing `.svg`
|
* 'icon' => (string) **REQUIRED** Icon name without trailing `.svg`
|
||||||
* 'title' => (?string) Title text to appear on mouse hover
|
* 'title' => (?string) Title text to appear on mouse hover
|
||||||
* 'size' => (int) Height and width in px
|
* 'size' => (int) Height and width in px
|
||||||
* 'colour' => (string) Main colour
|
* 'colour' => (string) Main colour
|
||||||
* 'hover' => (?string) Hover colour
|
* 'hover' => (?string) Hover colour
|
||||||
* 'classes' => (array) Additional classes to add to the icon. Not recommended.
|
* 'classes' => (string[]|string) Additional classes to add to the icon, given as
|
||||||
|
* an array of strings or a space-separated string.
|
||||||
* ]
|
* ]
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -43,6 +46,14 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
|
|
||||||
$svg = $this->getSvg($options['icon']);
|
$svg = $this->getSvg($options['icon']);
|
||||||
|
|
||||||
|
if (empty($svg)) {
|
||||||
|
throw new EmptyFileException(\sprintf("The file %s.svg was found, but it was empty!", $options['icon']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isValidXml($svg)) {
|
||||||
|
throw new InvalidSvgException(\sprintf("The file %s.svg was found, but it does not contain valid SVG code!", $options['icon']));
|
||||||
|
}
|
||||||
|
|
||||||
$this->sanitiseSvg($svg);
|
$this->sanitiseSvg($svg);
|
||||||
|
|
||||||
$colourClasses = $this->getColourClasses($options['colour'], $options['hover']);
|
$colourClasses = $this->getColourClasses($options['colour'], $options['hover']);
|
||||||
@@ -51,8 +62,11 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
$classes = trim($colourClasses.' '.$extraClasses);
|
$classes = trim($colourClasses.' '.$extraClasses);
|
||||||
$this->addClassesToSvg($svg, $classes);
|
$this->addClassesToSvg($svg, $classes);
|
||||||
|
|
||||||
|
$svg = $this->addAttribute($svg, 'aria-hidden', 'true');
|
||||||
|
|
||||||
if (null !== $options['title']) {
|
if (null !== $options['title']) {
|
||||||
$this->addTitleToSvg($svg, $options['title']);
|
$this->addTitleToSvg($svg, $options['title']);
|
||||||
|
$svg = $this->addAttribute($svg, 'aria-hidden', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setSvgHeightAndWidth($svg, $options['size']);
|
$this->setSvgHeightAndWidth($svg, $options['size']);
|
||||||
@@ -60,9 +74,9 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
return $svg;
|
return $svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getExtraClasses(array $extraClasses): string
|
private function getExtraClasses(array|string $extraClasses): string
|
||||||
{
|
{
|
||||||
return implode(' ', $extraClasses);
|
return \is_array($extraClasses) ? implode(' ', $extraClasses) : $extraClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function mergeWithDefaultOptions(array $userOptions): array
|
private function mergeWithDefaultOptions(array $userOptions): array
|
||||||
@@ -103,6 +117,9 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
$this->removeBlackFillAttributes($svg);
|
$this->removeBlackFillAttributes($svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IconNotFound when an svg file with the passed in name cannot be found
|
||||||
|
*/
|
||||||
private function findSvgFilepath(string $iconName): string
|
private function findSvgFilepath(string $iconName): string
|
||||||
{
|
{
|
||||||
foreach ($this->directories as $directory) {
|
foreach ($this->directories as $directory) {
|
||||||
@@ -202,4 +219,23 @@ final class IconRuntime implements RuntimeExtensionInterface
|
|||||||
|
|
||||||
$svg = $this->removeXMLDeclaration($svgAsXml->saveXML());
|
$svg = $this->removeXMLDeclaration($svgAsXml->saveXML());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addAttribute(string $svg, string $attribute, string $value): string
|
||||||
|
{
|
||||||
|
$xml = new \SimpleXMLElement($svg);
|
||||||
|
$xml = $this->addAttributeToXmlElement($xml, $attribute, $value);
|
||||||
|
|
||||||
|
return $this->removeXMLDeclaration($xml->saveXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidXml(string $input): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
new \SimpleXMLElement($input);
|
||||||
|
} catch (\Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ namespace Pcm\IconBundle\Tests\Twig\Functions;
|
|||||||
|
|
||||||
use Pcm\IconBundle\DependencyInjection\Configuration;
|
use Pcm\IconBundle\DependencyInjection\Configuration;
|
||||||
use Pcm\IconBundle\Exception\ColourNotFound;
|
use Pcm\IconBundle\Exception\ColourNotFound;
|
||||||
|
use Pcm\IconBundle\Exception\EmptyFileException;
|
||||||
use Pcm\IconBundle\Exception\IconNotFound;
|
use Pcm\IconBundle\Exception\IconNotFound;
|
||||||
|
use Pcm\IconBundle\Exception\InvalidSvgException;
|
||||||
use Pcm\IconBundle\Twig\Runtime\IconRuntime;
|
use Pcm\IconBundle\Twig\Runtime\IconRuntime;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@@ -50,6 +52,18 @@ class IconRuntimeTest extends TestCase
|
|||||||
$this->icon->renderIcon(['icon' => random_bytes(8)]);
|
$this->icon->renderIcon(['icon' => random_bytes(8)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfPassedInAnEmptyFile(): void
|
||||||
|
{
|
||||||
|
$this->expectException(EmptyFileException::class);
|
||||||
|
$this->icon->renderIcon(['icon' => 'empty']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfContentsIsNotValidSvg(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidSvgException::class);
|
||||||
|
$this->icon->renderIcon(['icon' => 'invalid']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testNoTitleExistsIfNotPassedIn(): void
|
public function testNoTitleExistsIfNotPassedIn(): void
|
||||||
{
|
{
|
||||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
@@ -225,19 +239,26 @@ class IconRuntimeTest extends TestCase
|
|||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testExtraClassesThrowsIfNotAnArray(): void
|
public function testExtraClassesThrowsIfNotAnArrayOrString(): void
|
||||||
{
|
{
|
||||||
$this->expectException(\TypeError::class);
|
$this->expectException(\TypeError::class);
|
||||||
$this->icon->renderIcon(['icon' => self::ICON, 'classes' => 'string_value']);
|
$this->icon->renderIcon(['icon' => self::ICON, 'classes' => 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testExtraClassesGetAdded(): void
|
public function testExtraClassesGetAddedFromArray(): void
|
||||||
{
|
{
|
||||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => ['abc', 'def']]);
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => ['abc', 'def']]);
|
||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*abc.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*abc.*".*>/', $contents);
|
||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*def.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*def.*".*>/', $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testExtraClassesGetAddedFromString(): void
|
||||||
|
{
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => 'ghi jkl']);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+class=".*ghi.*".*>/', $contents);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+class=".*jkl.*".*>/', $contents);
|
||||||
|
}
|
||||||
|
|
||||||
public function testAddingExtraClassesDoesntStripAwayColourClasses(): void
|
public function testAddingExtraClassesDoesntStripAwayColourClasses(): void
|
||||||
{
|
{
|
||||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => ['abc']]);
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => ['abc']]);
|
||||||
@@ -259,4 +280,23 @@ class IconRuntimeTest extends TestCase
|
|||||||
$this->assertStringNotContainsString($needleA, $contents);
|
$this->assertStringNotContainsString($needleA, $contents);
|
||||||
$this->assertStringNotContainsString($needleB, $contents);
|
$this->assertStringNotContainsString($needleB, $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAriaHiddenAttributeIsAdded(): void
|
||||||
|
{
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+aria-hidden="true".*>/', $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAriaHiddenAttributeIsSetToTrueIfAlreadyPresent(): void
|
||||||
|
{
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => 'attr']);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+aria-hidden="true".*>/', $contents);
|
||||||
|
$this->assertDoesNotMatchRegularExpression('/<svg.+aria-hidden="false".*>/', $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAriaHiddenAttributeIsSetToFalseIfATitleIsPassedIn(): void
|
||||||
|
{
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'title' => 'something']);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+aria-hidden="false".*>/', $contents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
tests/icons/attr.svg
Normal file
10
tests/icons/attr.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg aria-hidden="false" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="96 96 320 320">
|
||||||
|
<title>Some cross lol</title>
|
||||||
|
<line fill="rgb(0, 0, 0)"></line>
|
||||||
|
<line fill="#000"></line>
|
||||||
|
<line fill="#000000"></line>
|
||||||
|
<line stroke="currentColor" fill="black"></line>
|
||||||
|
<line x1="256" y1="112" x2="256" y2="400" width="101" height="101" style="fill: none; stroke: rgb(0,0, 0); stroke-linecap: round; stroke-linejoin: round; stroke-width: 32px;"></line>
|
||||||
|
<line x1="400" y1="256" x2="112" y2="256" width="102" height="102" style="fill: none; stroke: #000; stroke-linecap: round; stroke-linejoin: round; stroke-width: 32px;"></line>
|
||||||
|
<polyline points="112 244 256 100 400 244" style="fill:#000;fill:#000000;fill: black;fill:rgb(0,0,0);stroke:#000;stroke:#000000;stroke:black;stroke:rgb(0, 0, 0);stroke-linecap:round;stroke-linejoin:round;stroke-width:48px" stroke='currentColor'/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 924 B |
0
tests/icons/empty.svg
Normal file
0
tests/icons/empty.svg
Normal file
1
tests/icons/invalid.svg
Normal file
1
tests/icons/invalid.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg><svg>
|
||||||
|
After Width: | Height: | Size: 11 B |
Reference in New Issue
Block a user