Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3ec9c7d4b | |||
| 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 | ||
|
|
f82317cf99 | ||
|
|
03212310fd | ||
|
|
eb2bea62a7 | ||
|
|
b28a6f69f9 | ||
|
|
9ef5d58a8a | ||
|
|
a4344687d4 | ||
|
|
4ec6224b25 | ||
|
|
7a3366c2da | ||
|
|
78a321137e | ||
|
|
5e3756773e | ||
|
|
6553d0d32f | ||
|
|
92eee8fa04 | ||
|
|
e12eceed26 | ||
|
|
27dfa0af2e | ||
|
|
3e4b948569 | ||
|
|
e72ce28af4 |
69
README.md
69
README.md
@@ -1,3 +1,70 @@
|
||||
# DEPRECATED in favour of Symfony UX Icon
|
||||
|
||||
# PCM Icon Bundle
|
||||
|
||||
# WIP DON'T USE YET
|
||||
Use icons inside of Twig templates with ease! Project must be using Tailwind for the colours to work properly.
|
||||
|
||||
|
||||
```php
|
||||
{{ pcm_icon({ icon: 'person' }) }}
|
||||
```
|
||||
|
||||
## Config
|
||||
Example config:
|
||||
```yaml
|
||||
# config/packages/pcm_icon.yaml
|
||||
pcm_icon:
|
||||
default:
|
||||
size: 32
|
||||
colour: primary
|
||||
directories:
|
||||
- '%kernel.project_dir%/public/icons'
|
||||
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'
|
||||
red:
|
||||
fill: 'fill-red-800'
|
||||
stroke: 'stroke-red-800'
|
||||
fill-hover: 'hover:fill-red-800'
|
||||
stroke-hover: 'hover:stroke-red-800'
|
||||
fill-group-hover: 'group-hover:fill-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.
|
||||
|
||||
`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
|
||||
COLOUR:
|
||||
fill: 'fill-COLOUR'
|
||||
stroke: 'stroke-COLOUR'
|
||||
fill-hover: 'hover:fill-COLOUR'
|
||||
stroke-hover: 'hover:stroke-COLOUR'
|
||||
fill-group-hover: 'group-hover:fill-COLOUR'
|
||||
stroke-group-hover: 'group-hover:stroke-COLOUR'
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
`icon (string)` **(REQUIRED)** Icon to use, without the `.svg` extension.
|
||||
|
||||
`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.
|
||||
|
||||
`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 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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -2,18 +2,16 @@ when@dev:
|
||||
pcm_icon:
|
||||
directories:
|
||||
- '%kernel.project_dir%/public/icons'
|
||||
palletes: []
|
||||
colours: []
|
||||
|
||||
when@prod:
|
||||
pcm_icon:
|
||||
directories:
|
||||
- '%kernel.project_dir%/public/icons'
|
||||
palletes: []
|
||||
colours: []
|
||||
|
||||
when@test:
|
||||
pcm_icon:
|
||||
directories:
|
||||
- '%kernel.project_dir%/tests/icons'
|
||||
palletes:
|
||||
- []
|
||||
|
||||
colours: []
|
||||
|
||||
@@ -5,28 +5,16 @@ services:
|
||||
|
||||
pcm_icon.icon_extension:
|
||||
public: true
|
||||
class: Pcm\IconBundle\Twig\Functions\IconExtension
|
||||
arguments:
|
||||
$directories:
|
||||
- '%kernel.project_dir%/public/icons'
|
||||
$palletes:
|
||||
- primary:
|
||||
stroke: stroke-primary,
|
||||
fill: fill-primary
|
||||
- white:
|
||||
stroke: stroke-white,
|
||||
fill: fill-white
|
||||
class: Pcm\IconBundle\Twig\Extension\IconExtension
|
||||
|
||||
Pcm\IconBundle\Twig\Functions\IconExtension:
|
||||
Pcm\IconBundle\Twig\Extension\IconExtension:
|
||||
public: false
|
||||
alias: pcm_icon.icon_extension
|
||||
|
||||
Pcm\IconBundle\Twig\Runtime\IconRuntime:
|
||||
tags:
|
||||
- { name: twig.runtime }
|
||||
arguments:
|
||||
$directories:
|
||||
- '%kernel.project_dir%/public/icons'
|
||||
$palletes:
|
||||
- primary:
|
||||
stroke: stroke-primary,
|
||||
fill: fill-primary
|
||||
- white:
|
||||
stroke: stroke-white,
|
||||
fill: fill-white
|
||||
$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;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
class Configuration implements ConfigurationInterface
|
||||
{
|
||||
public const DEFAULT_SIZE = 32;
|
||||
public const DEFAULT_COLOUR = 'primary';
|
||||
|
||||
public function getConfigTreeBuilder(): TreeBuilder
|
||||
{
|
||||
$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()
|
||||
->arrayNode('directories')
|
||||
->scalarPrototype()->end()
|
||||
->end()
|
||||
->arrayNode('palletes')
|
||||
->variablePrototype()->end()
|
||||
->arrayNode('default')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('colour')->defaultValue(self::DEFAULT_COLOUR)->end()
|
||||
->integerNode('size')->defaultValue(self::DEFAULT_SIZE)->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();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$definition = $container->getDefinition('pcm_icon.icon_extension');
|
||||
|
||||
$definition->addArgument($config['directories']);
|
||||
$definition->addArgument($config['palletes']);
|
||||
$container->setParameter('pcm.icon_bundle.default_options', $config['default']);
|
||||
$container->setParameter('pcm.icon_bundle.directories', $config['directories']);
|
||||
$container->setParameter('pcm.icon_bundle.colours', $config['colours']);
|
||||
}
|
||||
}
|
||||
|
||||
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/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;
|
||||
|
||||
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\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Component\HttpKernel\Bundle\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,203 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pcm\IconBundle\Twig\Functions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
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'
|
||||
];
|
||||
|
||||
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>.*<\/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</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
|
||||
205
src/Twig/Runtime/IconRuntime.php
Normal file
205
src/Twig/Runtime/IconRuntime.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pcm\IconBundle\Twig\Runtime;
|
||||
|
||||
use Pcm\IconBundle\Exception\ColourNotFound;
|
||||
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' => (array) Additional classes to add to the icon. Not recommended.
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @return string Processed SVG
|
||||
*/
|
||||
public function renderIcon(array $userOptions): string
|
||||
{
|
||||
$options = $this->mergeWithDefaultOptions($userOptions);
|
||||
|
||||
$svg = $this->getSvg($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 $extraClasses): string
|
||||
{
|
||||
return implode(' ', $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);
|
||||
}
|
||||
|
||||
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,57 +4,44 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pcm\IconBundle\Tests\Twig\Functions;
|
||||
|
||||
use Pcm\IconBundle\Twig\Functions\IconExtension;
|
||||
use Pcm\IconBundle\Twig\Functions\IconNotFound;
|
||||
use Pcm\IconBundle\Twig\Functions\PalleteNotFound;
|
||||
use Pcm\IconBundle\DependencyInjection\Configuration;
|
||||
use Pcm\IconBundle\Exception\ColourNotFound;
|
||||
use Pcm\IconBundle\Exception\IconNotFound;
|
||||
use Pcm\IconBundle\Twig\Runtime\IconRuntime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class IconExtensionTest extends TestCase
|
||||
class IconRuntimeTest extends TestCase
|
||||
{
|
||||
private const ICON = 'test';
|
||||
|
||||
private const PALLETES = [
|
||||
private const COLOURS = [
|
||||
'primary' => [
|
||||
'fill' => 'fill-primary',
|
||||
'stroke' => 'stroke-primary',
|
||||
'fill' => 'fill-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',
|
||||
],
|
||||
'white' => [
|
||||
'fill' => 'fill-white',
|
||||
'stroke' => 'stroke-white',
|
||||
'fill' => 'fill-white'
|
||||
'fill-hover' => 'hover:fill-white',
|
||||
'stroke-hover' => 'hover:stroke-white',
|
||||
'fill-group-hover' => 'group-hover:fill-white',
|
||||
'stroke-group-hover' => 'group-hover:stroke-white',
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* @var IconExtension
|
||||
*/
|
||||
private IconExtension $icon;
|
||||
private IconRuntime $icon;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->icon = new IconExtension(['tests/icons'], self::PALLETES);
|
||||
}
|
||||
|
||||
public function testInstanceOf(): void
|
||||
{
|
||||
$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);
|
||||
$this->icon = new IconRuntime(
|
||||
defaultOptions: ['size' => Configuration::DEFAULT_SIZE, 'colour' => Configuration::DEFAULT_COLOUR],
|
||||
directories: ['tests/icons'],
|
||||
colours: self::COLOURS
|
||||
);
|
||||
}
|
||||
|
||||
public function testThrowsWhenPassedAnInvalidIconName(): void
|
||||
@@ -137,7 +124,7 @@ class IconExtensionTest extends TestCase
|
||||
|
||||
public function testDefaultSizeIsSetOnSvgIfNoSizeOptionPassed(): void
|
||||
{
|
||||
$defaultSize = IconExtension::DEFAULT_SIZE;
|
||||
$defaultSize = Configuration::DEFAULT_SIZE;
|
||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||
|
||||
$regex = "/^<svg.+?width=\"{$defaultSize}\"/";
|
||||
@@ -149,7 +136,7 @@ class IconExtensionTest extends TestCase
|
||||
|
||||
public function testDefaultSizeIsOnlySetOnce(): void
|
||||
{
|
||||
$defaultSize = IconExtension::DEFAULT_SIZE;
|
||||
$defaultSize = Configuration::DEFAULT_SIZE;
|
||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||
|
||||
$widthRegex = "/width=\"{$defaultSize}\"/";
|
||||
@@ -161,36 +148,6 @@ class IconExtensionTest extends TestCase
|
||||
$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' => '']]);
|
||||
}
|
||||
|
||||
public function testThrowsIfChildArrayDoesntContainFillKey(): void
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
new IconExtension(['/'], [['stroke' => '']]);
|
||||
}
|
||||
|
||||
public function testBlackStrokeAttributeValuesAreRemoved(): void
|
||||
{
|
||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||
@@ -200,6 +157,15 @@ class IconExtensionTest extends TestCase
|
||||
$this->assertDoesNotMatchRegularExpression('/stroke="\s*rgb\(0,\s*0,\s*0\)\s*"/', $content);
|
||||
}
|
||||
|
||||
public function testBlackStrokeStylesAreRemoved(): void
|
||||
{
|
||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||
$this->assertDoesNotMatchRegularExpression('/stroke:\s*#000\s*/', $content);
|
||||
$this->assertDoesNotMatchRegularExpression('/stroke:\s*#000000\s*/', $content);
|
||||
$this->assertDoesNotMatchRegularExpression('/stroke:\s*black\s*/', $content);
|
||||
$this->assertDoesNotMatchRegularExpression('/stroke:\s*rgb\(0,\s*0,\s*0\)\s*/', $content);
|
||||
}
|
||||
|
||||
public function testBlackFillAttributeValuesAreRemoved(): void
|
||||
{
|
||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||
@@ -209,18 +175,88 @@ class IconExtensionTest extends TestCase
|
||||
$this->assertDoesNotMatchRegularExpression('/fill="\s*rgb\(0,\s*0,\s*0\)\s*"/', $content);
|
||||
}
|
||||
|
||||
public function testThrowsIfPalleteIsNotFound(): void
|
||||
public function testBlackFillStylesAreRemoved(): void
|
||||
{
|
||||
$this->expectException(PalleteNotFound::class);
|
||||
$content = $this->icon->renderIcon(['icon' => self::ICON]);
|
||||
$this->assertDoesNotMatchRegularExpression('/fill:\s*#000\s*/', $content);
|
||||
$this->assertDoesNotMatchRegularExpression('/fill:\s*#000000\s*/', $content);
|
||||
$this->assertDoesNotMatchRegularExpression('/fill:\s*black\s*/', $content);
|
||||
$this->assertDoesNotMatchRegularExpression('/fill:\s*rgb\(0,\s*0,\s*0\)\s*/', $content);
|
||||
}
|
||||
|
||||
public function testThrowsIfColourIsNotFound(): void
|
||||
{
|
||||
$this->expectException(ColourNotFound::class);
|
||||
$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']);
|
||||
$this->assertMatchesRegularExpression('/<svg.*?class=".*?fill-white.*?>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.*?class=".*?stroke-white?.*>/', $contents);
|
||||
}
|
||||
|
||||
public function testThrowsIfHoverColourIsNotFound(): void
|
||||
{
|
||||
$this->expectException(ColourNotFound::class);
|
||||
$this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'red']);
|
||||
}
|
||||
|
||||
public function testSvgClassContainsHoverColourClasses(): void
|
||||
{
|
||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'white']);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*cursor-pointer.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*hover:fill-white.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*hover:stroke-white.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:fill-white.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
||||
}
|
||||
|
||||
public function testSvgClassContainsHoverAndColourClasses(): void
|
||||
{
|
||||
$contents = $this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'white', 'colour' => 'primary']);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*fill-primary.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*stroke-primary.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*cursor-pointer.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*hover:fill-white.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*hover:stroke-white.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:fill-white.*".*>/', $contents);
|
||||
$this->assertMatchesRegularExpression('/<svg.+class=".*group-hover:stroke-white.*".*>/', $contents);
|
||||
}
|
||||
|
||||
public function testExtraClassesThrowsIfNotAnArray(): void
|
||||
{
|
||||
$this->expectException(\TypeError::class);
|
||||
$this->icon->renderIcon(['icon' => self::ICON, 'classes' => 'string_value']);
|
||||
}
|
||||
|
||||
public function testExtraClassesGetAdded(): 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 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
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
<line fill="rgb(0, 0, 0)"></line>
|
||||
<line fill="#000"></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="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>
|
||||
|
||||
|
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 904 B |
Reference in New Issue
Block a user