Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Icons] allow configuring multiple local directories and optionally prefixing them #1596

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/Icons/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,42 @@ No icons are provided by this package but there are several ways to include and
Local SVG Icons
~~~~~~~~~~~~~~~

Add your svg icons to the ``assets/icons/`` directory and commit them.
Add your svg icons to the configured local icon directory (``assets/icons/`` by default) and commit them.
The name of the file is used as the _name_ of the icon (``name.svg`` will be named ``name``).
If located in a subdirectory, the _name_ will be ``sub-dir:name``.

Multiple Directories
^^^^^^^^^^^^^^^^^^^^

You can configure multiple directories to search for icons. The first directory to contain the
icon will be used.

.. code-block:: yaml

ux_icons:
icon_dir:
- %kernel.project_dir%/assets/icons
- %kernel.project_dir%/vendor/your-vendor/your-bundle/assets/icons

.. tip::

You can suffix the path with ``@<alias>`` to use a prefix. For example, let's say you
have the ``fortawesome/font-awesome`` composer package installed and you want to use
the ``fa`` prefix:

.. code-block:: yaml

ux_icons:
icon_dir:
# ...
- %kernel.project_dir%/vendor/fortawesome/font-awesome/svgs@fa

Now, you can use the ``fa:icon-name`` syntax to render icons from the FontAwesome set:

.. code-block:: html+twig

{{ ux_icon('fa:solid:adjust', {class: 'w-4 h-4'}) }} <!-- renders "vendor/fortawesome/font-awesome/svgs/solid/adjust.svg" -->

Icons On-Demand
~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -130,8 +162,14 @@ Full Default Configuration
.. code-block:: yaml

ux_icons:
# The local directory where icons are stored.
icon_dir: '%kernel.project_dir%/assets/icons'
# The local directory('s) where icons are stored.
# Order matters as the first directory to contain the icon will be used.
# The first directory will be used to store imported icons.
# Suffix with "@<alias>" to use a prefix.
icon_dir:

# Default:
- %kernel.project_dir%/assets/icons

# Default attributes to add to all icons.
default_icon_attributes:
Expand Down
33 changes: 30 additions & 3 deletions src/Icons/src/DependencyInjection/UXIconsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,36 @@ public function getConfigTreeBuilder(): TreeBuilder

$rootNode
->children()
->scalarNode('icon_dir')
->info('The local directory where icons are stored.')
->defaultValue('%kernel.project_dir%/assets/icons')
->arrayNode('icon_dir')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I rename this to icons_dirs?

Copy link
Member

@yceruto yceruto Mar 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe paths if they can be relative?

->beforeNormalization()
->ifString()
->then(static fn ($v) => [$v])
->end()
->beforeNormalization()
->always(function (array $values) {
$ret = [];

foreach ($values as $value) {
if (2 === \count($parts = explode('@', $value, 2))) {
$ret[$parts[1]] = $parts[0];

continue;
}

$ret[] = $value;
}

return $ret;
})
->end()
->info(<<<EOF
The local directory('s) where icons are stored.
Order matters as the first directory to contain the icon will be used.
The first directory will be used to store imported icons.
Suffix with "@<alias>" to use a prefix.
EOF)
->scalarPrototype()->end()
->defaultValue(['%kernel.project_dir%/assets/icons'])
->end()
->variableNode('default_icon_attributes')
->info('Default attributes to add to all icons.')
Expand Down
20 changes: 15 additions & 5 deletions src/Icons/src/Registry/LocalSvgIconRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,32 @@
*/
final class LocalSvgIconRegistry implements IconRegistryInterface
{
public function __construct(private string $iconDir)
/**
* @param string[] $iconDirs
*/
public function __construct(private array $iconDirs)
{
}

public function get(string $name): Icon
{
if (!file_exists($filename = sprintf('%s/%s.svg', $this->iconDir, str_replace(':', '/', $name)))) {
throw new IconNotFoundException(sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
foreach ($this->iconDirs as $key => $dir) {
if (!is_numeric($key) && str_starts_with($name, $key.':')) {
$name = substr($name, \strlen($key) + 1);
}

if (file_exists($filename = sprintf('%s/%s.svg', $dir, str_replace(':', '/', $name)))) {
return Icon::fromFile($filename);
}
}

return Icon::fromFile($filename);
throw new IconNotFoundException(sprintf('The svg file for icon "%s" does not exist.', $name));
}

public function add(string $name, string $svg): void
{
$filename = sprintf('%s/%s.svg', $this->iconDir, $name);
$dir = $this->iconDirs[array_key_first($this->iconDirs)] ?? throw new \LogicException('No local icon directory configured.');
$filename = sprintf('%s/%s.svg', $dir, $name);

(new Filesystem())->dumpFile($filename, $svg);
}
Expand Down
6 changes: 5 additions & 1 deletion src/Icons/tests/Fixtures/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ protected function configureContainer(ContainerConfigurator $c): void
]);

$c->extension('ux_icons', [
'icon_dir' => '%kernel.project_dir%/tests/Fixtures/icons',
'icon_dir' => [
'%kernel.project_dir%/tests/Fixtures/icons',
'%kernel.project_dir%/tests/Fixtures/icons2',
'%kernel.project_dir%/tests/Fixtures/icons3@fa',
],
]);

$c->services()->set('logger', NullLogger::class);
Expand Down
1 change: 1 addition & 0 deletions src/Icons/tests/Fixtures/icons2/arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/Icons/tests/Fixtures/icons3/solid/thing.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/Icons/tests/Fixtures/templates/template2.html.twig
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{{ ux_icon('something:invalid') }}
{{ ux_icon('invalid') }}
{{ ux_icon('iconamoon:invalid') }}
{{ ux_icon('arrow') }}
{{ ux_icon('fa:solid:thing') }}
2 changes: 2 additions & 0 deletions src/Icons/tests/Integration/Command/WarmCacheCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public function testCanWarmCache(): void
->assertOutputContains('Warmed icon user.')
->assertOutputContains('Warmed icon sub:check.')
->assertOutputContains('Warmed icon iconamoon:3d-duotone.')
->assertOutputContains('Warmed icon arrow.')
->assertOutputContains('Warmed icon fa:solid:thing.')
->assertOutputContains('Icon cache warmed.')
;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Icons/tests/Unit/Registry/LocalSvgIconRegistryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ public static function invalidSvgProvider(): iterable

private function registry(): LocalSvgIconRegistry
{
return new LocalSvgIconRegistry(__DIR__.'/../../Fixtures/svg');
return new LocalSvgIconRegistry([__DIR__.'/../../Fixtures/svg']);
}
}
Loading