Compare commits
55 Commits
0.0.7
...
issue/7-th
| Author | SHA1 | Date | |
|---|---|---|---|
| 3da5e00361 | |||
| a19f1f4438 | |||
| 6e2228b835 | |||
| 15ee51b884 | |||
| 6ab2baeb4e | |||
| 33892f81aa | |||
| bb7862c6ac | |||
| c1ac0faf78 | |||
| 1d91b50c32 | |||
| 9248b51908 | |||
| 0e2ee19f8f | |||
| 6e357ee8b7 | |||
| 3df034d309 | |||
| abfff194f5 | |||
| 6cf1deb136 | |||
| 03a1f033ee | |||
| 173c08b0d0 | |||
| e97ef857ef | |||
| dbd4ccb35d | |||
| 4142c24e00 | |||
| 8b698bb3fd | |||
| 692789b21d | |||
| 3b6a85c3b3 | |||
| ac6a573ce2 | |||
| f6443f5dff | |||
| 9c2321fe3a | |||
| f9e35368c0 | |||
| 6d63e2f29b | |||
|
|
7b76f8f01f | ||
|
|
4ce873d15c | ||
|
|
f33bf7ce6a | ||
|
|
19076b58e1 | ||
|
|
fdab83bd28 | ||
|
|
e3d3818b10 | ||
|
|
90a354f822 | ||
|
|
6ad58d9cfc | ||
|
|
850ba765a2 | ||
|
|
493f0c275b | ||
|
|
08d906260a | ||
|
|
ec4a730b53 | ||
|
|
66276640d7 | ||
|
|
b2e76dd1b3 | ||
|
|
eec11801b0 | ||
|
|
633e4a284e | ||
|
|
b66e7821a7 | ||
| 9983121cbb | |||
| 2b1c7b9c12 | |||
| a34d2c5dca | |||
| 839c96a104 | |||
| e7c0465fdb | |||
|
|
7469cc78f8 | ||
|
|
87a6204f25 | ||
|
|
d5278f3473 | ||
|
|
b2f6e07d97 | ||
|
|
9195df9028 |
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [X.X.X] - XXXX-XX-XX
|
||||||
|
- Allow passing in a space-separated string to the `classes` option
|
||||||
|
|
||||||
|
## [1.0.0] - 2023-06-27
|
||||||
|
- First major version release
|
||||||
30
README.md
30
README.md
@@ -1,9 +1,10 @@
|
|||||||
# PCM Icon Bundle
|
# PCM Icon Bundle
|
||||||
|
|
||||||
Use icons inside of Twig templates with ease!
|
Use icons inside of Twig templates with ease! Project must be using Tailwind for the colours to work properly.
|
||||||
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
{{ icon({ icon: 'person' }) }}
|
{{ pcm_icon({ icon: 'person' }) }}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
@@ -11,9 +12,12 @@ Example config:
|
|||||||
```yaml
|
```yaml
|
||||||
# config/packages/pcm_icon.yaml
|
# config/packages/pcm_icon.yaml
|
||||||
pcm_icon:
|
pcm_icon:
|
||||||
|
default:
|
||||||
|
size: 32
|
||||||
|
colour: primary
|
||||||
directories:
|
directories:
|
||||||
- '%kernel.project_dir%/public/icons'
|
- '%kernel.project_dir%/public/icons'
|
||||||
palletes:
|
colours:
|
||||||
primary:
|
primary:
|
||||||
fill: 'fill-primary'
|
fill: 'fill-primary'
|
||||||
stroke: 'stroke-primary'
|
stroke: 'stroke-primary'
|
||||||
@@ -30,12 +34,14 @@ pcm_icon:
|
|||||||
stroke-group-hover: 'group-hover:stroke-red-800'
|
stroke-group-hover: 'group-hover:stroke-red-800'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`default` - Override the default options provided to the bundle.
|
||||||
|
|
||||||
`directories` - Which directories to look in for icons.
|
`directories` - Which directories to look in for icons.
|
||||||
|
|
||||||
`palletes` - Custom colour palletes to use when colouring icons. Because this extension relies on Tailwind classes for colouring we must specify all the classes. This is annoyingly verbose, but you can just copy this template:
|
`colours` - Custom colours to use when colouring icons. Because this extension relies on Tailwind classes for colouring we **must** explicitly list all the classes required for Tailwind to render them properly. You can just copy this template replacing COLOUR as appropriate:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
PALLETE_NAME:
|
COLOUR:
|
||||||
fill: 'fill-COLOUR'
|
fill: 'fill-COLOUR'
|
||||||
stroke: 'stroke-COLOUR'
|
stroke: 'stroke-COLOUR'
|
||||||
fill-hover: 'hover:fill-COLOUR'
|
fill-hover: 'hover:fill-COLOUR'
|
||||||
@@ -46,15 +52,17 @@ PALLETE_NAME:
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
`icon (string)` **(REQUIRED)** Icon to use, without the `.svg` extension
|
`icon (string)` **(REQUIRED)** Icon to use, without the `.svg` extension.
|
||||||
|
|
||||||
`title (string)` Optional text to show on hover
|
`title (string)` Optional text to show on hover. This can be null, but not an empty string.
|
||||||
|
|
||||||
`size (int)` Size of the icon in pixels
|
`size (int)` Size of the icon in pixels.
|
||||||
|
|
||||||
`colour (string)` Name of the main colour pallete to use. Defaults to `"primary"`, assuming that a pallete with the project's primary colour will exist
|
`colour (string)` Name of the colour to use. Defaults to `primary` as it assumes you have a Tailwind colour set up called `primary`. If you are not using `primary` to set a Tailwind primary colour in your project you can change the default colour in the `pcm_icon.yaml` file.
|
||||||
|
|
||||||
`hover (string)` Name of the colour pallete to use when the icon is hovered over
|
`hover (string)` Name of the colour to use when the icon is hovered over
|
||||||
|
|
||||||
|
`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.
|
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.
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ when@dev:
|
|||||||
pcm_icon:
|
pcm_icon:
|
||||||
directories:
|
directories:
|
||||||
- '%kernel.project_dir%/public/icons'
|
- '%kernel.project_dir%/public/icons'
|
||||||
palletes: []
|
colours: []
|
||||||
|
|
||||||
when@prod:
|
when@prod:
|
||||||
pcm_icon:
|
pcm_icon:
|
||||||
directories:
|
directories:
|
||||||
- '%kernel.project_dir%/public/icons'
|
- '%kernel.project_dir%/public/icons'
|
||||||
palletes: []
|
colours: []
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
pcm_icon:
|
pcm_icon:
|
||||||
directories:
|
directories:
|
||||||
- '%kernel.project_dir%/tests/icons'
|
- '%kernel.project_dir%/tests/icons'
|
||||||
palletes:
|
colours: []
|
||||||
- []
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ services:
|
|||||||
|
|
||||||
pcm_icon.icon_extension:
|
pcm_icon.icon_extension:
|
||||||
public: true
|
public: true
|
||||||
class: Pcm\IconBundle\Twig\Functions\IconExtension
|
class: Pcm\IconBundle\Twig\Extension\IconExtension
|
||||||
|
|
||||||
Pcm\IconBundle\Twig\Functions\IconExtension:
|
Pcm\IconBundle\Twig\Extension\IconExtension:
|
||||||
public: false
|
public: false
|
||||||
alias: pcm_icon.icon_extension
|
alias: pcm_icon.icon_extension
|
||||||
|
|
||||||
|
Pcm\IconBundle\Twig\Runtime\IconRuntime:
|
||||||
|
tags:
|
||||||
|
- { name: twig.runtime }
|
||||||
|
arguments:
|
||||||
|
$defaultOptions: '%pcm.icon_bundle.default_options%'
|
||||||
|
$directories: '%pcm.icon_bundle.directories%'
|
||||||
|
$colours: '%pcm.icon_bundle.colours%'
|
||||||
|
|||||||
@@ -4,26 +4,78 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pcm\IconBundle\DependencyInjection;
|
namespace Pcm\IconBundle\DependencyInjection;
|
||||||
|
|
||||||
|
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||||
|
|
||||||
class Configuration implements ConfigurationInterface
|
class Configuration implements ConfigurationInterface
|
||||||
{
|
{
|
||||||
|
public const DEFAULT_SIZE = 32;
|
||||||
|
public const DEFAULT_COLOUR = 'primary';
|
||||||
|
|
||||||
public function getConfigTreeBuilder(): TreeBuilder
|
public function getConfigTreeBuilder(): TreeBuilder
|
||||||
{
|
{
|
||||||
$treeBuilder = new TreeBuilder('pcm_icon');
|
$treeBuilder = new TreeBuilder('pcm_icon');
|
||||||
|
$this->addValidationRules($treeBuilder->getRootNode());
|
||||||
|
|
||||||
$treeBuilder->getRootNode()
|
return $treeBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addValidationRules(ArrayNodeDefinition $rootNode): void
|
||||||
|
{
|
||||||
|
// I've split the tree up like this because Intelephense was crying.
|
||||||
|
// Plus I think it's a little easier to read.
|
||||||
|
|
||||||
|
$rootNode
|
||||||
->children()
|
->children()
|
||||||
->arrayNode('directories')
|
->arrayNode('default')
|
||||||
->scalarPrototype()->end()
|
->addDefaultsIfNotSet()
|
||||||
|
->children()
|
||||||
|
->scalarNode('colour')->defaultValue(self::DEFAULT_COLOUR)->end()
|
||||||
|
->integerNode('size')->defaultValue(self::DEFAULT_SIZE)->end()
|
||||||
->end()
|
->end()
|
||||||
->arrayNode('palletes')
|
|
||||||
->variablePrototype()->end()
|
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
return $treeBuilder;
|
$rootNode
|
||||||
|
->children()
|
||||||
|
->arrayNode('directories')
|
||||||
|
->validate()
|
||||||
|
->ifEmpty()
|
||||||
|
->thenInvalid("Directories cannot be empty!")
|
||||||
|
->end()
|
||||||
|
->scalarPrototype()
|
||||||
|
->validate()
|
||||||
|
->ifTrue(fn($path) => !is_dir($path))
|
||||||
|
->thenInvalid('%s is not a directory!')
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
|
|
||||||
|
$rootNode
|
||||||
|
->children()
|
||||||
|
->arrayNode('colours')
|
||||||
|
->validate()
|
||||||
|
->ifEmpty()
|
||||||
|
->thenInvalid('Colours cannot be empty!')
|
||||||
|
->end()
|
||||||
|
|
||||||
|
->arrayPrototype()
|
||||||
|
->normalizeKeys(false) # Don't normalize hyphens into underscores etc
|
||||||
|
->children()
|
||||||
|
->scalarNode('fill')->isRequired()->end()
|
||||||
|
->scalarNode('stroke')->isRequired()->end()
|
||||||
|
->scalarNode('fill-hover')->isRequired()->end()
|
||||||
|
->scalarNode('stroke-hover')->isRequired()->end()
|
||||||
|
->scalarNode('fill-group-hover')->isRequired()->end()
|
||||||
|
->scalarNode('stroke-group-hover')->isRequired()->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ class PcmIconExtension extends Extension
|
|||||||
$configuration = new Configuration();
|
$configuration = new Configuration();
|
||||||
$config = $this->processConfiguration($configuration, $configs);
|
$config = $this->processConfiguration($configuration, $configs);
|
||||||
|
|
||||||
$definition = $container->getDefinition('pcm_icon.icon_extension');
|
$container->setParameter('pcm.icon_bundle.default_options', $config['default']);
|
||||||
|
$container->setParameter('pcm.icon_bundle.directories', $config['directories']);
|
||||||
$definition->addArgument($config['directories']);
|
$container->setParameter('pcm.icon_bundle.colours', $config['colours']);
|
||||||
$definition->addArgument($config['palletes']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/Exception/ColourNotFound.php
Normal file
8
src/Exception/ColourNotFound.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Exception;
|
||||||
|
|
||||||
|
class ColourNotFound extends \Exception {};
|
||||||
|
|
||||||
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/IconNotFound.php
Normal file
7
src/Exception/IconNotFound.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Exception;
|
||||||
|
|
||||||
|
class IconNotFound extends \Exception {};
|
||||||
@@ -5,10 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Pcm\IconBundle;
|
namespace Pcm\IconBundle;
|
||||||
|
|
||||||
use Pcm\IconBundle\DependencyInjection\PcmIconExtension;
|
use Pcm\IconBundle\DependencyInjection\PcmIconExtension;
|
||||||
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
||||||
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
|
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
|
||||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
|
||||||
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
|
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
|
||||||
|
|
||||||
class PcmIconBundle extends AbstractBundle
|
class PcmIconBundle extends AbstractBundle
|
||||||
|
|||||||
22
src/Twig/Extension/IconExtension.php
Normal file
22
src/Twig/Extension/IconExtension.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Twig\Extension;
|
||||||
|
|
||||||
|
use Pcm\IconBundle\Twig\Runtime\IconRuntime;
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
|
||||||
|
final class IconExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getFunctions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFunction('pcm_icon', [IconRuntime::class, 'renderIcon'], ['is_safe' => ['html']])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pcm\IconBundle\Twig\Functions;
|
|
||||||
|
|
||||||
use Twig\Extension\AbstractExtension;
|
|
||||||
use Twig\TwigFunction;
|
|
||||||
|
|
||||||
final class IconExtension extends AbstractExtension
|
|
||||||
{
|
|
||||||
public const DEFAULT_SIZE = 32;
|
|
||||||
|
|
||||||
private const DEFAULT_OPTIONS = [
|
|
||||||
'icon' => null,
|
|
||||||
'title' => null,
|
|
||||||
'size' => self::DEFAULT_SIZE,
|
|
||||||
'colour' => 'primary',
|
|
||||||
'hover' => null
|
|
||||||
];
|
|
||||||
|
|
||||||
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) &&
|
|
||||||
array_key_exists('fill-hover', $pallete) &&
|
|
||||||
array_key_exists('stroke-hover', $pallete) &&
|
|
||||||
array_key_exists('fill-group-hover', $pallete) &&
|
|
||||||
array_key_exists('stroke-group-hover', $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
|
|
||||||
* 'hover' => (?string) Hover colour pallete
|
|
||||||
* ]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public function renderIcon(array $userOptions): string
|
|
||||||
{
|
|
||||||
$options = $this->mergeUserOptionsWithDefaults($userOptions);
|
|
||||||
|
|
||||||
$this->throwIfUnrecognisedOptionExists($options);
|
|
||||||
|
|
||||||
$iconFilepath = $this->findSvgFilepath($options['icon']);
|
|
||||||
$rawSvgMarkup = $this->getSvgMarkup($iconFilepath);
|
|
||||||
$cleanSvgMarkup = $this->cleanSvgMarkup($rawSvgMarkup);
|
|
||||||
|
|
||||||
$mainPallete = $this->getPallete($options['colour']);
|
|
||||||
|
|
||||||
$colourClasses = "{$mainPallete['stroke']} {$mainPallete['fill']}";
|
|
||||||
|
|
||||||
if (null !== $options['hover']) {
|
|
||||||
$hoverPallete = $this->getPallete($options['hover']);
|
|
||||||
$colourClasses .= " cursor-pointer hover:{$hoverPallete['stroke']} hover:{$hoverPallete['fill']} group-hover:{$hoverPallete['stroke']} group-hover:{$hoverPallete['fill']}";
|
|
||||||
}
|
|
||||||
|
|
||||||
$xml = new \SimpleXMLElement($cleanSvgMarkup);
|
|
||||||
$xml = $this->addAttributeToXmlElement($xml, 'class', $colourClasses);
|
|
||||||
$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 throwIfUnrecognisedOptionExists(array $options): void
|
|
||||||
{
|
|
||||||
foreach (array_keys($options) as $key) {
|
|
||||||
if (!key_exists($key, self::DEFAULT_OPTIONS))
|
|
||||||
throw new \InvalidArgumentException("Unrecognised option '{$key}'!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>.*<\/title>/', '', $markup);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getPallete(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</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 {};
|
|
||||||
214
src/Twig/Runtime/IconRuntime.php
Normal file
214
src/Twig/Runtime/IconRuntime.php
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Twig\Runtime;
|
||||||
|
|
||||||
|
use Pcm\IconBundle\Exception\ColourNotFound;
|
||||||
|
use Pcm\IconBundle\Exception\EmptyFileException;
|
||||||
|
use Pcm\IconBundle\Exception\IconNotFound;
|
||||||
|
use Twig\Extension\RuntimeExtensionInterface;
|
||||||
|
|
||||||
|
final class IconRuntime implements RuntimeExtensionInterface
|
||||||
|
{
|
||||||
|
private const OPTIONS = [
|
||||||
|
'icon' => null,
|
||||||
|
'title' => null,
|
||||||
|
'size' => null,
|
||||||
|
'colour' => null,
|
||||||
|
'hover' => null,
|
||||||
|
'classes' => "",
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(private array $defaultOptions, private array $directories, private array $colours)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $options
|
||||||
|
* ```
|
||||||
|
* $options = [
|
||||||
|
* 'icon' => (string) **REQUIRED** Icon name without trailing `.svg`
|
||||||
|
* 'title' => (?string) Title text to appear on mouse hover
|
||||||
|
* 'size' => (int) Height and width in px
|
||||||
|
* 'colour' => (string) Main colour
|
||||||
|
* 'hover' => (?string) Hover colour
|
||||||
|
* 'classes' => (string[]|string) Additional classes to add to the icon, given as
|
||||||
|
* an array of strings or a space-separated string.
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @return string Processed SVG
|
||||||
|
*/
|
||||||
|
public function renderIcon(array $userOptions): string
|
||||||
|
{
|
||||||
|
$options = $this->mergeWithDefaultOptions($userOptions);
|
||||||
|
|
||||||
|
$svg = $this->getSvg($options['icon']);
|
||||||
|
|
||||||
|
if (empty($svg)) {
|
||||||
|
throw new EmptyFileException(\sprintf("The file %s.svg was found, but it was empty!", $options['icon']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sanitiseSvg($svg);
|
||||||
|
|
||||||
|
$colourClasses = $this->getColourClasses($options['colour'], $options['hover']);
|
||||||
|
$extraClasses = $this->getExtraClasses($options['classes']);
|
||||||
|
|
||||||
|
$classes = trim($colourClasses.' '.$extraClasses);
|
||||||
|
$this->addClassesToSvg($svg, $classes);
|
||||||
|
|
||||||
|
if (null !== $options['title']) {
|
||||||
|
$this->addTitleToSvg($svg, $options['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setSvgHeightAndWidth($svg, $options['size']);
|
||||||
|
|
||||||
|
return $svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getExtraClasses(array|string $extraClasses): string
|
||||||
|
{
|
||||||
|
return \is_array($extraClasses) ? implode(' ', $extraClasses) : $extraClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mergeWithDefaultOptions(array $userOptions): array
|
||||||
|
{
|
||||||
|
$this->throwIfUnrecognisedOptionExists($userOptions);
|
||||||
|
return $this->mergeUserOptionsWithDefaults($userOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mergeUserOptionsWithDefaults(array $userOptions): array
|
||||||
|
{
|
||||||
|
$options = self::OPTIONS;
|
||||||
|
$options['size'] = $this->defaultOptions['size'];
|
||||||
|
$options['colour'] = $this->defaultOptions['colour'];
|
||||||
|
|
||||||
|
return array_merge($options, $userOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function throwIfUnrecognisedOptionExists(array $options): void
|
||||||
|
{
|
||||||
|
foreach (array_keys($options) as $key) {
|
||||||
|
if (!key_exists($key, self::OPTIONS)) {
|
||||||
|
throw new \InvalidArgumentException("Unrecognised option '{$key}'!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSvg(string $iconName): string
|
||||||
|
{
|
||||||
|
$iconFilepath = $this->findSvgFilepath($iconName);
|
||||||
|
return file_get_contents($iconFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitiseSvg(string &$svg): void
|
||||||
|
{
|
||||||
|
$this->removeExistingTitleElement($svg);
|
||||||
|
$this->removeBlackStrokeAttributes($svg);
|
||||||
|
$this->removeStrokeCurrentColor($svg);
|
||||||
|
$this->removeBlackFillAttributes($svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IconNotFound when an svg file with the passed in name cannot be found
|
||||||
|
*/
|
||||||
|
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 removeExistingTitleElement(string &$svg): void
|
||||||
|
{
|
||||||
|
$svg = preg_replace('/<title>.*<\/title>/', '', $svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeBlackStrokeAttributes(string &$svg): void
|
||||||
|
{
|
||||||
|
$svg = preg_replace('/stroke(=|:)"?\s*(#0{6}|#000|rgb\(\s*0,\s*0,\s*0\s*\)|black)\s*"?/', '', $svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeStrokeCurrentColor(string &$svg): void
|
||||||
|
{
|
||||||
|
$svg = \preg_replace('/stroke=(\'|")currentColor(\'|")/', '', $svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeBlackFillAttributes(string &$svg): void
|
||||||
|
{
|
||||||
|
$svg = preg_replace('/fill(=|:)"?\s*(#0{6}|#000|rgb\(\s*0,\s*0,\s*0\s*\)|black)\s*"?/', '', $svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getColourClasses(string $primaryColour, ?string $hoverColour): string
|
||||||
|
{
|
||||||
|
$mainColour = $this->getColour($primaryColour);
|
||||||
|
$colourClasses = "{$mainColour['stroke']} {$mainColour['fill']}";
|
||||||
|
|
||||||
|
if (null !== $hoverColour) {
|
||||||
|
$hoverColour = $this->getColour($hoverColour);
|
||||||
|
$colourClasses .= " cursor-pointer {$hoverColour['stroke-hover']} {$hoverColour['fill-hover']} {$hoverColour['stroke-group-hover']} {$hoverColour['fill-group-hover']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $colourClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getColour(string $colourName): array
|
||||||
|
{
|
||||||
|
if (array_key_exists($colourName, $this->colours)) {
|
||||||
|
return $this->colours[$colourName];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ColourNotFound("The colour \"$colourName\" was not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addClassesToSvg(string &$svg, string $classes): void
|
||||||
|
{
|
||||||
|
$xml = new \SimpleXMLElement($svg);
|
||||||
|
$xml = $this->addAttributeToXmlElement($xml, 'class', $classes);
|
||||||
|
$svg = $this->removeXMLDeclaration($xml->saveXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addTitleToSvg(string &$svg, ?string $title): void
|
||||||
|
{
|
||||||
|
if ('' === $title) {
|
||||||
|
throw new \InvalidArgumentException('Title must not be an empty string!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$svg = preg_replace('/(<svg(.|\n)*?>\n?)/', "$1<title>$title</title>", $svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 removeXMLDeclaration(string $content): string
|
||||||
|
{
|
||||||
|
return trim(preg_replace('/<\?xml.*\?>/', '', $content));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSvgHeightAndWidth(string &$svg, int $size): void
|
||||||
|
{
|
||||||
|
if ($size < 0) {
|
||||||
|
throw new \InvalidArgumentException('Size must not be negative');
|
||||||
|
}
|
||||||
|
|
||||||
|
$svgAsXml = new \SimpleXMLElement($svg);
|
||||||
|
$svgAsXml = $this->addAttributeToXmlElement($svgAsXml, 'width', $size);
|
||||||
|
$svgAsXml = $this->addAttributeToXmlElement($svgAsXml, 'height', $size);
|
||||||
|
|
||||||
|
$svg = $this->removeXMLDeclaration($svgAsXml->saveXML());
|
||||||
|
}
|
||||||
|
}
|
||||||
128
tests/Config/ConfigurationTest.php
Normal file
128
tests/Config/ConfigurationTest.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\IconBundle\Tests\Config;
|
||||||
|
|
||||||
|
use Pcm\IconBundle\DependencyInjection\Configuration;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Config\Definition\Processor;
|
||||||
|
|
||||||
|
class ConfigurationTest extends TestCase
|
||||||
|
{
|
||||||
|
private Configuration $configuration;
|
||||||
|
private Processor $processor;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->configuration = new Configuration();
|
||||||
|
$this->processor = new Processor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfDirectoriesIsEmpty()
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
$config['directories'] = [];
|
||||||
|
$this->expectExceptionMessage("Directories cannot be empty!");
|
||||||
|
$this->validateConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfDirectoryIsNotFound(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
$fakeDir = "abc/def/ghi";
|
||||||
|
$config['directories'] = [$fakeDir];
|
||||||
|
$this->expectExceptionMessage("\"abc\/def\/ghi\" is not a directory!");
|
||||||
|
$this->validateConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfMultipleDirectoriesPassedAndOneNotFound(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
$fakeDir = "abc/def/ghi";
|
||||||
|
$config['directories'] = ['./', $fakeDir];
|
||||||
|
$this->expectExceptionMessage("\"abc\/def\/ghi\" is not a directory!");
|
||||||
|
$this->validateConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfNoColoursExist(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
$config['colours'] = [];
|
||||||
|
$this->expectExceptionMessage("Colours cannot be empty!");
|
||||||
|
$this->validateConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfColourHasMissingClass(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
unset($config['colours']['primary']['fill']);
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
$this->validateConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsIfColourHasExtraClass(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
$config['colours']['primary']['extra'] = "test";
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
$this->validateConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultSizeIs32Pixels(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
unset($config['default']['size']);
|
||||||
|
$validatedConfig = $this->validateConfig($config);
|
||||||
|
|
||||||
|
$this->assertSame(32, $validatedConfig['default']['size']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSizeIsSetToProvidedValue(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
$config['default']['size'] = 99;
|
||||||
|
$validatedConfig = $this->validateConfig($config);
|
||||||
|
|
||||||
|
$this->assertSame(99, $validatedConfig['default']['size']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultColourIsPrimary(): void
|
||||||
|
{
|
||||||
|
$config = $this->getValidConfig();
|
||||||
|
unset($config['default']['colour']);
|
||||||
|
$validatedConfig = $this->validateConfig($config);
|
||||||
|
|
||||||
|
$this->assertSame('primary', $validatedConfig['default']['colour']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateConfig(array $config): array
|
||||||
|
{
|
||||||
|
return $this->processor->processConfiguration($this->configuration, [$config]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getValidConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'colour' => 'primary',
|
||||||
|
'size' => 32
|
||||||
|
],
|
||||||
|
|
||||||
|
'directories' => [
|
||||||
|
'./',
|
||||||
|
],
|
||||||
|
|
||||||
|
'colours' => [
|
||||||
|
'primary' => [
|
||||||
|
'fill' => 'fill-primary',
|
||||||
|
'stroke' => 'stroke-primary',
|
||||||
|
'fill-hover' => 'hover:fill-primary',
|
||||||
|
'stroke-hover' => 'hover:stroke-primary',
|
||||||
|
'fill-group-hover' => 'group-hover:fill-primary',
|
||||||
|
'stroke-group-hover' => 'group-hover:stroke-primary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pcm\IconBundle\Tests\Twig\Functions;
|
namespace Pcm\IconBundle\Tests\Twig\Functions;
|
||||||
|
|
||||||
use Pcm\IconBundle\Twig\Functions\IconExtension;
|
use Pcm\IconBundle\DependencyInjection\Configuration;
|
||||||
use Pcm\IconBundle\Twig\Functions\IconNotFound;
|
use Pcm\IconBundle\Exception\ColourNotFound;
|
||||||
use Pcm\IconBundle\Twig\Functions\PalleteNotFound;
|
use Pcm\IconBundle\Exception\EmptyFileException;
|
||||||
|
use Pcm\IconBundle\Exception\IconNotFound;
|
||||||
|
use Pcm\IconBundle\Twig\Runtime\IconRuntime;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class IconExtensionTest extends TestCase
|
class IconRuntimeTest extends TestCase
|
||||||
{
|
{
|
||||||
private const ICON = 'test';
|
private const ICON = 'test';
|
||||||
|
|
||||||
private const PALLETES = [
|
private const COLOURS = [
|
||||||
'primary' => [
|
'primary' => [
|
||||||
'fill' => 'fill-primary',
|
'fill' => 'fill-primary',
|
||||||
'stroke' => 'stroke-primary',
|
'stroke' => 'stroke-primary',
|
||||||
@@ -32,37 +34,15 @@ class IconExtensionTest extends TestCase
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
private IconRuntime $icon;
|
||||||
* @var IconExtension
|
|
||||||
*/
|
|
||||||
private IconExtension $icon;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->icon = new IconExtension(['tests/icons'], self::PALLETES);
|
$this->icon = new IconRuntime(
|
||||||
}
|
defaultOptions: ['size' => Configuration::DEFAULT_SIZE, 'colour' => Configuration::DEFAULT_COLOUR],
|
||||||
|
directories: ['tests/icons'],
|
||||||
public function testInstanceOf(): void
|
colours: self::COLOURS
|
||||||
{
|
);
|
||||||
$this->assertInstanceOf(IconExtension::class, $this->icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfDirectoriesIsEmpty(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
|
||||||
new IconExtension([], self::PALLETES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfDirectoriesContainsNonString(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\TypeError::class);
|
|
||||||
new IconExtension([99], self::PALLETES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfDirectoriesContainsNonStringAmongStrings(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\TypeError::class);
|
|
||||||
new IconExtension(['string', 99, 'string'], self::PALLETES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testThrowsWhenPassedAnInvalidIconName(): void
|
public function testThrowsWhenPassedAnInvalidIconName(): void
|
||||||
@@ -71,6 +51,12 @@ class IconExtensionTest 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 testNoTitleExistsIfNotPassedIn(): void
|
public function testNoTitleExistsIfNotPassedIn(): void
|
||||||
{
|
{
|
||||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
@@ -145,7 +131,7 @@ class IconExtensionTest extends TestCase
|
|||||||
|
|
||||||
public function testDefaultSizeIsSetOnSvgIfNoSizeOptionPassed(): void
|
public function testDefaultSizeIsSetOnSvgIfNoSizeOptionPassed(): void
|
||||||
{
|
{
|
||||||
$defaultSize = IconExtension::DEFAULT_SIZE;
|
$defaultSize = Configuration::DEFAULT_SIZE;
|
||||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
|
|
||||||
$regex = "/^<svg.+?width=\"{$defaultSize}\"/";
|
$regex = "/^<svg.+?width=\"{$defaultSize}\"/";
|
||||||
@@ -157,7 +143,7 @@ class IconExtensionTest extends TestCase
|
|||||||
|
|
||||||
public function testDefaultSizeIsOnlySetOnce(): void
|
public function testDefaultSizeIsOnlySetOnce(): void
|
||||||
{
|
{
|
||||||
$defaultSize = IconExtension::DEFAULT_SIZE;
|
$defaultSize = Configuration::DEFAULT_SIZE;
|
||||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
|
|
||||||
$widthRegex = "/width=\"{$defaultSize}\"/";
|
$widthRegex = "/width=\"{$defaultSize}\"/";
|
||||||
@@ -169,60 +155,6 @@ class IconExtensionTest extends TestCase
|
|||||||
$this->assertSame(1, $timesMatched);
|
$this->assertSame(1, $timesMatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testThrowsIfPalletsIsEmpty(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
|
||||||
new IconExtension(['/'], []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfPalletesContainsNonArray(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\TypeError::class);
|
|
||||||
new IconExtension(['/'], [99]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfPalletesContainsNonArrayInbetweenArrays(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\TypeError::class);
|
|
||||||
new IconExtension(['/'], [[], 99, []]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfChildArrayDoesntContainStrokeKey(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
new IconExtension(['/'], [['fill' => '', 'fill-hover' => '', 'stroke-hover' => '', 'fill-group-hover' => '', 'stroke-group-hover' => '']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfChildArrayDoesntContainFillKey(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
new IconExtension(['/'], [['stroke' => '', 'fill-hover' => '', 'stroke-hover' => '', 'fill-group-hover' => '', 'stroke-group-hover' => '']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfChildArrayDoesntContainFillHoverKey(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
new IconExtension(['/'], [['fill' => '', 'stroke' => '', 'stroke-hover' => '', 'fill-group-hover' => '', 'stroke-group-hover' => '']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfChildArrayDoesntContainStrokeHoverKey(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
new IconExtension(['/'], [['fill' => '', 'stroke' => '', 'fill-hover' => '', 'fill-group-hover' => '', 'stroke-group-hover' => '']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfChildArrayDoesntContainFillGroupHoverKey(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
new IconExtension(['/'], [['fill' => '', 'stroke' => '', 'fill-hover' => '', 'stroke-hover' => '', 'stroke-group-hover' => '']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testThrowsIfChildArrayDoesntContainStrokeGroupHoverKey(): void
|
|
||||||
{
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
new IconExtension(['/'], [['fill' => '', 'stroke' => '', 'fill-hover' => '', 'stroke-hover' => '', 'fill-group-hover' => '']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testBlackStrokeAttributeValuesAreRemoved(): void
|
public function testBlackStrokeAttributeValuesAreRemoved(): void
|
||||||
{
|
{
|
||||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
@@ -259,26 +191,26 @@ class IconExtensionTest extends TestCase
|
|||||||
$this->assertDoesNotMatchRegularExpression('/fill:\s*rgb\(0,\s*0,\s*0\)\s*/', $content);
|
$this->assertDoesNotMatchRegularExpression('/fill:\s*rgb\(0,\s*0,\s*0\)\s*/', $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testThrowsIfColourPalleteIsNotFound(): void
|
public function testThrowsIfColourIsNotFound(): void
|
||||||
{
|
{
|
||||||
$this->expectException(PalleteNotFound::class);
|
$this->expectException(ColourNotFound::class);
|
||||||
$this->icon->renderIcon(['icon' => self::ICON, 'colour' => 'red']);
|
$this->icon->renderIcon(['icon' => self::ICON, 'colour' => 'red']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSvgClassContainsPalleteClasses(): void
|
public function testSvgClassContainsColourClasses(): void
|
||||||
{
|
{
|
||||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'colour' => 'white']);
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'colour' => 'white']);
|
||||||
$this->assertMatchesRegularExpression('/<svg.*?class=".*?fill-white.*?>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.*?class=".*?fill-white.*?>/', $contents);
|
||||||
$this->assertMatchesRegularExpression('/<svg.*?class=".*?stroke-white?.*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.*?class=".*?stroke-white?.*>/', $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testThrowsIfHoverPalleteIsNotFound(): void
|
public function testThrowsIfHoverColourIsNotFound(): void
|
||||||
{
|
{
|
||||||
$this->expectException(PalleteNotFound::class);
|
$this->expectException(ColourNotFound::class);
|
||||||
$this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'red']);
|
$this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'red']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSvgClassContainsHoverPalleteClasses(): void
|
public function testSvgClassContainsHoverColourClasses(): void
|
||||||
{
|
{
|
||||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'white']);
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'white']);
|
||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*cursor-pointer.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*cursor-pointer.*".*>/', $contents);
|
||||||
@@ -288,7 +220,7 @@ class IconExtensionTest extends TestCase
|
|||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSvgClassContainsHoverAndColourPalleteClasses(): void
|
public function testSvgClassContainsHoverAndColourClasses(): void
|
||||||
{
|
{
|
||||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'white', 'colour' => 'primary']);
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'white', 'colour' => 'primary']);
|
||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*fill-primary.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*fill-primary.*".*>/', $contents);
|
||||||
@@ -300,10 +232,45 @@ class IconExtensionTest extends TestCase
|
|||||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testExtraClassesThrowsIfNotAnArrayOrString(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\TypeError::class);
|
||||||
|
$this->icon->renderIcon(['icon' => self::ICON, 'classes' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExtraClassesGetAddedFromArray(): void
|
||||||
|
{
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => ['abc', 'def']]);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+class=".*abc.*".*>/', $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
|
||||||
|
{
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'classes' => ['abc']]);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+class=".*abc.*".*>/', $contents);
|
||||||
|
$this->assertMatchesRegularExpression('/<svg.+class=".*fill-primary.*".*>/', $contents);
|
||||||
|
}
|
||||||
|
|
||||||
public function testThrowsIfInvalidOptionPassed(): void
|
public function testThrowsIfInvalidOptionPassed(): void
|
||||||
{
|
{
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
$this->icon->renderIcon(['icon' => self::ICON, 'fake-key-should-throw' => null]);
|
$this->icon->renderIcon(['icon' => self::ICON, 'fake-key-should-throw' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStrokeCurrentColorInstancesAreRemoved(): void
|
||||||
|
{
|
||||||
|
$needleA = 'stroke="currentColor"';
|
||||||
|
$needleB = 'stroke=\'currentColor\'';
|
||||||
|
$contents = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||||
|
$this->assertStringNotContainsString($needleA, $contents);
|
||||||
|
$this->assertStringNotContainsString($needleB, $contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0
tests/icons/empty.svg
Normal file
0
tests/icons/empty.svg
Normal file
@@ -3,8 +3,8 @@
|
|||||||
<line fill="rgb(0, 0, 0)"></line>
|
<line fill="rgb(0, 0, 0)"></line>
|
||||||
<line fill="#000"></line>
|
<line fill="#000"></line>
|
||||||
<line fill="#000000"></line>
|
<line fill="#000000"></line>
|
||||||
<line fill="black"></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="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>
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 860 B After Width: | Height: | Size: 904 B |
Reference in New Issue
Block a user