16 Commits
0.0.5 ... 0.0.7

Author SHA1 Message Date
Brabli
f82317cf99 Add documentation 2022-08-30 00:03:48 +01:00
Brabli
03212310fd Throw exception if option is not recognised 2022-08-29 23:32:12 +01:00
Brabli
eb2bea62a7 Update readme 2022-08-29 23:17:07 +01:00
Brabli
b28a6f69f9 Make sure required keys exist in pallete arrays 2022-08-29 23:17:00 +01:00
Brabli
9ef5d58a8a Update readme 2022-08-29 21:00:43 +01:00
Brabli
a4344687d4 Add cursor pointer class on hover 2022-08-29 20:29:04 +01:00
Brabli
4ec6224b25 Remove space 2022-08-29 20:27:48 +01:00
Brabli
7a3366c2da Add group hover 2022-08-29 20:12:55 +01:00
Brabli
78a321137e Get hover colours added 2022-08-29 20:08:27 +01:00
Brabli
5e3756773e Get tests passing 2022-08-29 20:01:39 +01:00
Brabli
6553d0d32f Change method name 2022-08-29 19:43:18 +01:00
Brabli
92eee8fa04 Add info 2022-08-29 19:41:57 +01:00
Brabli
e12eceed26 Adjust comment spacing 2022-08-29 19:41:54 +01:00
Brabli
27dfa0af2e Test for black fill and stroke style removal 2022-08-29 19:31:27 +01:00
Brabli
3e4b948569 Add example config 2022-08-29 19:21:08 +01:00
Brabli
e72ce28af4 Remove args from services.yaml 2022-08-29 19:18:47 +01:00
5 changed files with 185 additions and 49 deletions

View File

@@ -1,3 +1,60 @@
# PCM Icon Bundle # PCM Icon Bundle
# WIP DON'T USE YET Use icons inside of Twig templates with ease!
```php
{{ icon({ icon: 'person' }) }}
```
## Config
Example config:
```yaml
# config/packages/pcm_icon.yaml
pcm_icon:
directories:
- '%kernel.project_dir%/public/icons'
palletes:
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'
```
`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:
```yaml
PALLETE_NAME:
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
`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
`hover (string)` Name of the colour pallete to use when the icon is hovered over
## Reminders
Remember to add `./config/packages/*.yaml` as a value in your Tailwind config content array if it does not already exist.

View File

@@ -6,27 +6,7 @@ services:
pcm_icon.icon_extension: pcm_icon.icon_extension:
public: true public: true
class: Pcm\IconBundle\Twig\Functions\IconExtension 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
Pcm\IconBundle\Twig\Functions\IconExtension: Pcm\IconBundle\Twig\Functions\IconExtension:
public: false public: false
alias: pcm_icon.icon_extension alias: pcm_icon.icon_extension
arguments:
$directories:
- '%kernel.project_dir%/public/icons'
$palletes:
- primary:
stroke: stroke-primary,
fill: fill-primary
- white:
stroke: stroke-white,
fill: fill-white

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Pcm\IconBundle\Twig\Functions; namespace Pcm\IconBundle\Twig\Functions;
use InvalidArgumentException;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFunction; use Twig\TwigFunction;
@@ -16,13 +15,14 @@ final class IconExtension extends AbstractExtension
'icon' => null, 'icon' => null,
'title' => null, 'title' => null,
'size' => self::DEFAULT_SIZE, 'size' => self::DEFAULT_SIZE,
'colour' => 'primary' 'colour' => 'primary',
'hover' => null
]; ];
public function __construct(private array $directories, private array $palletes) public function __construct(private array $directories, private array $palletes)
{ {
if (empty($this->directories)) if (empty($this->directories))
throw new InvalidArgumentException('Directories array must contain at least one path!'); throw new \InvalidArgumentException('Directories array must contain at least one path!');
$dirsContainNonString = array_reduce($this->directories, $dirsContainNonString = array_reduce($this->directories,
fn($notString, $path) => $notString || !is_string($path)); fn($notString, $path) => $notString || !is_string($path));
@@ -31,7 +31,7 @@ final class IconExtension extends AbstractExtension
throw new \TypeError('Directories array must only contain strings!'); throw new \TypeError('Directories array must only contain strings!');
if (empty($this->palletes)) if (empty($this->palletes))
throw new InvalidArgumentException('Palletes array must contain at least one pallet!'); throw new \InvalidArgumentException('Palletes array must contain at least one pallet!');
$pelletesContainNonarray = array_reduce($this->palletes, $pelletesContainNonarray = array_reduce($this->palletes,
fn($notArray, $path) => $notArray || !is_array($path)); fn($notArray, $path) => $notArray || !is_array($path));
@@ -40,7 +40,14 @@ final class IconExtension extends AbstractExtension
throw new \TypeError('Palletes array must only contain arrays!'); throw new \TypeError('Palletes array must only contain arrays!');
foreach ($this->palletes as $pallete) { foreach ($this->palletes as $pallete) {
if (!(array_key_exists('stroke', $pallete) && array_key_exists('fill', $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!'); throw new \Exception('Palletes must contain a "stroke" and "fill" key!');
} }
} }
@@ -66,6 +73,7 @@ final class IconExtension extends AbstractExtension
* 'title' => (?string) Text to appear on mouse hover * 'title' => (?string) Text to appear on mouse hover
* 'size' => (int) Height and width in px * 'size' => (int) Height and width in px
* 'colour' => (string) Main colour pallete * 'colour' => (string) Main colour pallete
* 'hover' => (?string) Hover colour pallete
* ] * ]
* ``` * ```
*/ */
@@ -73,14 +81,23 @@ final class IconExtension extends AbstractExtension
{ {
$options = $this->mergeUserOptionsWithDefaults($userOptions); $options = $this->mergeUserOptionsWithDefaults($userOptions);
$this->throwIfUnrecognisedOptionExists($options);
$iconFilepath = $this->findSvgFilepath($options['icon']); $iconFilepath = $this->findSvgFilepath($options['icon']);
$rawSvgMarkup = $this->getSvgMarkup($iconFilepath); $rawSvgMarkup = $this->getSvgMarkup($iconFilepath);
$cleanSvgMarkup = $this->cleanSvgMarkup($rawSvgMarkup); $cleanSvgMarkup = $this->cleanSvgMarkup($rawSvgMarkup);
$mainPallete = $this->getMainPallete($options['colour']); $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 = new \SimpleXMLElement($cleanSvgMarkup);
$xml = $this->addAttributeToXmlElement($xml, 'class', $mainPallete['stroke'] . ' ' . $mainPallete['fill']); $xml = $this->addAttributeToXmlElement($xml, 'class', $colourClasses);
$markup = $this->removeXMLDeclaration($xml->saveXML()); $markup = $this->removeXMLDeclaration($xml->saveXML());
if ($this->isNonEmptyString($options['title'])) if ($this->isNonEmptyString($options['title']))
@@ -104,6 +121,14 @@ final class IconExtension extends AbstractExtension
return array_merge(self::DEFAULT_OPTIONS, $userOptions); 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 private function findSvgFilepath(string $iconName): string
{ {
foreach ($this->directories as $directory) { foreach ($this->directories as $directory) {
@@ -126,7 +151,7 @@ final class IconExtension extends AbstractExtension
return preg_replace('/<title>.*<\/title>/', '', $markup); return preg_replace('/<title>.*<\/title>/', '', $markup);
} }
private function getMainPallete(string $palleteName): array private function getPallete(string $palleteName): array
{ {
if (array_key_exists($palleteName, $this->palletes)) if (array_key_exists($palleteName, $this->palletes))
return $this->palletes[$palleteName]; return $this->palletes[$palleteName];
@@ -179,25 +204,15 @@ final class IconExtension extends AbstractExtension
private function removeBlackStrokeAttributes(string $content): string 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); 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 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); return preg_replace('/fill(=|:)"?\s*(#0{6}|#000|rgb\(\s*0,\s*0,\s*0\s*\)|black)\s*"?/', '', $content);
} }
} }
class IconNotFound extends \Exception {}; class IconNotFound extends \Exception {};
class PalleteNotFound 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

View File

@@ -15,12 +15,20 @@ class IconExtensionTest extends TestCase
private const PALLETES = [ private const PALLETES = [
'primary' => [ 'primary' => [
'fill' => 'fill-primary',
'stroke' => 'stroke-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' => [ 'white' => [
'fill' => 'fill-white',
'stroke' => 'stroke-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',
] ]
]; ];
@@ -182,13 +190,37 @@ class IconExtensionTest extends TestCase
public function testThrowsIfChildArrayDoesntContainStrokeKey(): void public function testThrowsIfChildArrayDoesntContainStrokeKey(): void
{ {
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
new IconExtension(['/'], [['fill' => '']]); new IconExtension(['/'], [['fill' => '', 'fill-hover' => '', 'stroke-hover' => '', 'fill-group-hover' => '', 'stroke-group-hover' => '']]);
} }
public function testThrowsIfChildArrayDoesntContainFillKey(): void public function testThrowsIfChildArrayDoesntContainFillKey(): void
{ {
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
new IconExtension(['/'], [['stroke' => '']]); 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
@@ -200,6 +232,15 @@ class IconExtensionTest extends TestCase
$this->assertDoesNotMatchRegularExpression('/stroke="\s*rgb\(0,\s*0,\s*0\)\s*"/', $content); $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 public function testBlackFillAttributeValuesAreRemoved(): void
{ {
$content = $this->icon->renderIcon(['icon' => self::ICON]); $content = $this->icon->renderIcon(['icon' => self::ICON]);
@@ -209,7 +250,16 @@ 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 testThrowsIfPalleteIsNotFound(): void public function testBlackFillStylesAreRemoved(): void
{
$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 testThrowsIfColourPalleteIsNotFound(): void
{ {
$this->expectException(PalleteNotFound::class); $this->expectException(PalleteNotFound::class);
$this->icon->renderIcon(['icon' => self::ICON, 'colour' => 'red']); $this->icon->renderIcon(['icon' => self::ICON, 'colour' => 'red']);
@@ -222,5 +272,38 @@ class IconExtensionTest extends TestCase
$this->assertMatchesRegularExpression('/<svg.*?class=".*?stroke-white?.*>/', $contents); $this->assertMatchesRegularExpression('/<svg.*?class=".*?stroke-white?.*>/', $contents);
} }
public function testThrowsIfHoverPalleteIsNotFound(): void
{
$this->expectException(PalleteNotFound::class);
$this->icon->renderIcon(['icon' => self::ICON, 'hover' => 'red']);
}
public function testSvgClassContainsHoverPalleteClasses(): 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 testSvgClassContainsHoverAndColourPalleteClasses(): 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 testThrowsIfInvalidOptionPassed(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->icon->renderIcon(['icon' => self::ICON, 'fake-key-should-throw' => null]);
}
} }

View File

@@ -6,4 +6,5 @@
<line fill="black"></line> <line 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"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 860 B