Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

19
vendor/symfony/ux-icons/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2024-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
use Symfony\UX\Icons\EventListener\WarmIconCacheOnAssetCompileListener;
return static function (ContainerConfigurator $container): void {
$container->services()
->set('.ux_icons.event_listener.warm_icon_cache_on_asset_compile', WarmIconCacheOnAssetCompileListener::class)
->args([
service('.ux_icons.cache_warmer'),
])
->tag('kernel.event_listener', [
'event' => PreAssetsCompileEvent::class,
'method' => '__invoke',
])
;
};

View File

@@ -0,0 +1,129 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\UX\Icons\Command\ImportIconCommand;
use Symfony\UX\Icons\Command\LockIconsCommand;
use Symfony\UX\Icons\Command\SearchIconCommand;
use Symfony\UX\Icons\Command\WarmCacheCommand;
use Symfony\UX\Icons\IconCacheWarmer;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\IconRenderer;
use Symfony\UX\Icons\IconRendererInterface;
use Symfony\UX\Icons\Registry\CacheIconRegistry;
use Symfony\UX\Icons\Registry\ChainIconRegistry;
use Symfony\UX\Icons\Registry\IconifyOnDemandRegistry;
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
use Symfony\UX\Icons\Twig\IconFinder;
use Symfony\UX\Icons\Twig\UXIconExtension;
use Symfony\UX\Icons\Twig\UXIconRuntime;
return static function (ContainerConfigurator $container): void {
$container->services()
->set('.ux_icons.cache')
->parent('cache.system')
->private()
->tag('cache.pool')
->set('.ux_icons.cache_icon_registry', CacheIconRegistry::class)
->args([
service('.ux_icons.chain_registry'),
service('.ux_icons.cache'),
])
->set('.ux_icons.local_svg_icon_registry', LocalSvgIconRegistry::class)
->args([
abstract_arg('icon_dir'),
])
->tag('ux_icons.registry', ['priority' => 10])
->set('.ux_icons.chain_registry', ChainIconRegistry::class)
->args([
tagged_iterator('ux_icons.registry'),
])
->alias('.ux_icons.icon_registry', '.ux_icons.cache_icon_registry')
->set('.ux_icons.twig_icon_extension', UXIconExtension::class)
->tag('twig.extension')
->set('.ux_icons.twig_icon_runtime', UXIconRuntime::class)
->args([
service('.ux_icons.icon_renderer'),
abstract_arg('ignore_not_found'),
service('logger')->ignoreOnInvalid(),
])
->tag('twig.runtime')
->tag('ux.twig_component.twig_renderer', ['key' => 'ux:icon'])
->set('.ux_icons.icon_renderer', IconRenderer::class)
->args([
service('.ux_icons.icon_registry'),
abstract_arg('default_icon_attributes'),
abstract_arg('icon_aliases'),
])
->alias(IconRendererInterface::class, '.ux_icons.icon_renderer')
->set('.ux_icons.icon_finder', IconFinder::class)
->args([
service('twig'),
abstract_arg('icon_dir'),
])
->set('.ux_icons.cache_warmer', IconCacheWarmer::class)
->args([
service('.ux_icons.cache_icon_registry'),
service('.ux_icons.icon_finder'),
])
->set('.ux_icons.command.warm_cache', WarmCacheCommand::class)
->args([
service('.ux_icons.cache_warmer'),
])
->tag('console.command')
->set('.ux_icons.iconify', Iconify::class)
->args([
service('.ux_icons.cache'),
abstract_arg('endpoint'),
service('http_client')->nullOnInvalid(),
])
->set('.ux_icons.iconify_on_demand_registry', IconifyOnDemandRegistry::class)
->args([
service('.ux_icons.iconify'),
])
->tag('ux_icons.registry', ['priority' => -10])
->set('.ux_icons.command.import', ImportIconCommand::class)
->args([
service('.ux_icons.iconify'),
service('.ux_icons.local_svg_icon_registry'),
])
->tag('console.command')
->set('.ux_icons.command.lock', LockIconsCommand::class)
->args([
service('.ux_icons.iconify'),
service('.ux_icons.local_svg_icon_registry'),
service('.ux_icons.icon_finder'),
])
->tag('console.command')
->set('.ux_icons.command.search', SearchIconCommand::class)
->args([
service('.ux_icons.iconify'),
])
->tag('console.command')
;
};

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\UX\Icons\Twig\UXIconComponent;
return static function (ContainerConfigurator $container): void {
$container->services()
->set('.ux_icons.twig_component.icon', UXIconComponent::class)
->tag('twig.component', ['key' => 'UX:Icon'])
;
};

View File

@@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
#[AsCommand(
name: 'ux:icons:import',
description: 'Import icon(s) from iconify.design',
)]
final class ImportIconCommand extends Command
{
public function __construct(private Iconify $iconify, private LocalSvgIconRegistry $registry)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('names', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Icon name from ux.symfony.com/icons (e.g. "mdi:home")')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$names = $input->getArgument('names');
$result = Command::SUCCESS;
$importedIcons = 0;
$prefixIcons = [];
foreach ($names as $name) {
if (!preg_match('#^([\w-]+):([\w-]+)$#', $name, $matches)) {
$io->error(\sprintf('Invalid icon name "%s".', $name));
$result = Command::FAILURE;
continue;
}
[, $prefix, $name] = $matches;
$prefixIcons[$prefix] ??= [];
$prefixIcons[$prefix][$name] = $name;
}
foreach ($prefixIcons as $prefix => $icons) {
if (!$this->iconify->hasIconSet($prefix)) {
$io->error(\sprintf('Icon set "%s" not found.', $prefix));
$result = Command::FAILURE;
continue;
}
$metadata = $this->iconify->metadataFor($prefix);
$io->newLine();
$io->writeln(\sprintf(' Icon set: %s (License: %s)', $metadata['name'], $metadata['license']['title']));
foreach ($this->iconify->chunk($prefix, array_keys($icons)) as $iconNames) {
$cursor = new Cursor($output);
foreach ($iconNames as $name) {
$io->writeln(\sprintf(' Importing %s:%s ...', $prefix, $name));
}
$cursor->moveUp(\count($iconNames));
try {
$batchResults = $this->iconify->fetchIcons($prefix, $iconNames);
} catch (\InvalidArgumentException $e) {
// At this point no exception should be thrown
$io->error($e->getMessage());
return Command::FAILURE;
}
foreach ($iconNames as $name) {
$cursor->clearLineAfter();
// If the icon is not found, the value will be null
if (null === $icon = $batchResults[$name] ?? null) {
$io->writeln(\sprintf(' <fg=red;options=bold>✗</> Not Found <fg=bright-white;bg=black>%s:</><fg=bright-red;bg=black>%s</>', $prefix, $name));
continue;
}
++$importedIcons;
$this->registry->add(\sprintf('%s/%s', $prefix, $name), (string) $icon);
$io->writeln(\sprintf(' <fg=bright-green;options=bold>✓</> Imported <fg=bright-white;bg=black>%s:</><fg=bright-magenta;bg=black;options>%s</>', $prefix, $name));
}
}
}
if ($importedIcons === $totalIcons = \count($names)) {
$io->success(\sprintf('Imported %d/%d icons.', $importedIcons, $totalIcons));
} elseif ($importedIcons > 0) {
$io->warning(\sprintf('Imported %d/%d icons.', $importedIcons, $totalIcons));
} else {
$io->error(\sprintf('Imported %d/%d icons.', $importedIcons, $totalIcons));
$result = Command::FAILURE;
}
return $result;
}
}

View File

@@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
use Symfony\UX\Icons\Twig\IconFinder;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
#[AsCommand(
name: 'ux:icons:lock',
description: 'Scan project and import icon(s) from iconify.design',
)]
final class LockIconsCommand extends Command
{
public function __construct(
private Iconify $iconify,
private LocalSvgIconRegistry $registry,
private IconFinder $iconFinder,
private readonly array $iconAliases = [],
private readonly array $iconSetAliases = [],
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption(
name: 'force',
mode: InputOption::VALUE_NONE,
description: 'Force re-import of all found icons'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$force = $input->getOption('force');
$count = 0;
$io->comment('Scanning project for icons...');
$finderIcons = $this->iconFinder->icons();
if ($this->iconAliases) {
$io->comment('Adding icons aliases...');
}
foreach ([...array_values($this->iconAliases), ...array_values($finderIcons)] as $icon) {
if (2 !== \count($parts = explode(':', $icon))) {
continue;
}
[$prefix, $name] = $parts;
$prefix = $this->iconSetAliases[$prefix] ?? $prefix;
if (!$force && $this->registry->has($prefix.':'.$name)) {
// icon already imported
continue;
}
if (!$this->iconify->hasIconSet($prefix)) {
// not an icon set? example: "og:twitter"
if ($io->isVeryVerbose()) {
$io->writeln(\sprintf(' <fg=bright-yellow;options=bold>✗</> IconSet Not Found: <fg=bright-white;bg=black>%s:%s</>.', $prefix, $name));
}
continue;
}
try {
$iconSvg = $this->iconify->fetchIcon($prefix, $name)->toHtml();
} catch (IconNotFoundException) {
// icon not found on iconify
if ($io->isVerbose()) {
$io->writeln(\sprintf(' <fg=bright-red;options=bold>✗</> Icon Not Found: <fg=bright-white;bg=black>%s:%s</>.', $prefix, $name));
}
continue;
}
$this->registry->add(\sprintf('%s/%s', $prefix, $name), $iconSvg);
$license = $this->iconify->metadataFor($prefix)['license'];
++$count;
$io->writeln(\sprintf(
" <fg=bright-green;options=bold>✓</> Imported <fg=bright-white;bg=black>%s:</><fg=bright-magenta;bg=black;options>%s</> (License: <href=%s>%s</>). Render with: <comment>{{ ux_icon('%s') }}</comment>",
$prefix,
$name,
$license['url'] ?? '#',
$license['title'],
$icon,
));
}
$io->success(\sprintf('Imported %d icons.', $count));
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,230 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableCellStyle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\UX\Icons\Iconify;
/**
* A console command to search icons and icon sets from ux.symfony.com.
*
* @author Simon André <smn.andre@gmail.com>
*
* @internal
*/
#[AsCommand(
name: 'ux:icons:search',
description: 'Search icons and icon sets from ux.symfony.com',
)]
final class SearchIconCommand extends Command
{
public function __construct(private Iconify $iconify)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('prefix', InputArgument::REQUIRED, 'Prefix or name of the icon set (ex: bootstrap, fa, tabler)')
->addArgument('name', InputArgument::OPTIONAL, 'Name of the icon (leave empty to search for sets)')
->setHelp(
<<<EOF
The <info>%command.name%</info> command search icon sets and icons from ux.symfony.com
To search for <comment>icon sets</comment>, pass the prefix or name of the icon set (or a part of it):
<info>php %command.full_name% bootstrap</info>
<info>php %command.full_name% material</info>
To search for <comment>icons</comment>, pass the prefix of the icon set and the name of the icon:
<info>php %command.full_name% bootstrap star</info>
EOF
);
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
if (null === $input->getArgument('prefix')) {
$io = new SymfonyStyle($input, $output);
if ($prefix = $io->ask('Prefix or name of the icon set (ex: bootstrap, fa, tabler)')) {
$input->setArgument('prefix', $prefix);
}
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$prefix = $input->getArgument('prefix');
$name = $input->getArgument('name');
$iconSets = $this->findIconSets($prefix);
if (null === $name) {
$this->renderIconSetTable($io, $iconSets);
if (1 === \count($iconSets)) {
$iconSet = reset($iconSets);
$searchTerm = 'arrow';
$io->writeln(\sprintf('Search <comment>"%s"</comment> in <comment>%s</comment> icons:', $searchTerm, $iconSet['name']));
$io->newLine();
$io->writeln(' '.\sprintf('php bin/console <comment>ux:icons:search %s %s</comment>', $iconSet['prefix'], $searchTerm));
$io->newLine();
}
return Command::SUCCESS;
}
if (false === $iconSet = reset($iconSets)) {
$io->error(\sprintf('No icon sets found for prefix "%s".', $prefix));
return Command::INVALID;
}
if (1 < \count($iconSets) && $prefix !== $iconSet['prefix']) {
$choices = array_combine(array_keys($iconSets), array_column($iconSets, 'name'));
$choice = $io->choice('Select an icon set', array_values($choices));
if (!$choice || false === $prefix = array_search($choice, $choices, true)) {
$io->error('No icon set selected.');
return Command::INVALID;
}
$iconSet = $iconSets[$prefix];
}
$io->write(\sprintf('Searching <comment>%s</comment> icons "<comment>%s</comment>"...', $iconSet['name'], $name));
try {
$results = $this->iconify->searchIcons($prefix, $name);
} catch (\Throwable $e) {
$io->write(' <fg=bright-red;options=bold>✗</>');
$io->error('An error occurred while searching for icons.');
if ($io->isVerbose()) {
$io->writeln($e->getMessage());
if ($io->isDebug()) {
$io->writeln($e->getTraceAsString());
}
}
return Command::FAILURE;
}
$io->newLine();
$icons = $results['icons'] ?? [];
sort($icons);
$iconPages = array_chunk($icons, 24);
$nbPages = \count($iconPages);
$cursor = new Cursor($output);
$io->writeln(\sprintf('Found <info>%d</info> icons.', \count($icons)));
foreach ($iconPages as $page => $iconPage) {
$this->renderIconTable($io, $prefix, $name, $iconPage);
if ($page + 1 === $nbPages) {
break;
}
if (!$io->confirm(\sprintf('Page <comment>%d</comment>/<comment>%d</comment>. Continue?', $page + 1, $nbPages))) {
break;
}
$cursor->moveUp(5)->clearLineAfter();
}
$io->newLine();
$io->writeln(\sprintf('See all the <comment>%s</comment> icons on: https://ux.symfony.com/icons?set=%s', $prefix, $prefix));
$io->newLine();
return Command::SUCCESS;
}
private function renderIconTable(SymfonyStyle $io, string $prefix, string $name, array $icons): void
{
$table = new Table($io);
$table->setStyle('symfony-style-guide');
$table->setColumnWidths([40, 40]);
foreach ($icons as $i => $icon) {
$icons[$i] = str_replace($name, '<fg=bright-blue>'.$name.'</>', $icon);
}
$table->addRows(array_chunk($icons, 2));
$table->render();
}
private function renderIconSetTable(SymfonyStyle $io, array $iconSets): void
{
$results = [];
foreach ($iconSets as $prefix => $iconSet) {
$results[] = [
$iconSet['name'] ?? $prefix,
new TableCell($iconSet['total'], [
'style' => new TableCellStyle(['align' => 'right']),
]),
$iconSet['license']['title'] ?? '',
$iconSet['prefix'],
$this->formatIcon($io, $prefix.':'.$iconSet['samples'][0], false),
];
}
$io->table(['Icon set', 'Icons', 'License', 'Prefix', 'Example'], $results);
}
private function findIconSets(string $query): array
{
$iconSets = [];
$query = mb_strtolower($query);
foreach ($this->iconify->getIconSets() as $prefix => $iconSet) {
if (!str_contains($prefix, $query) && !str_contains(mb_strtolower($iconSet['name']), $query)) {
continue;
}
$iconSets[$prefix] = [...$iconSet, 'prefix' => $prefix];
}
return $iconSets;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if (!$input->mustSuggestArgumentValuesFor('prefix')) {
return;
}
$prefixes = array_keys($this->iconify->getIconSets());
if ($input->getArgument('prefix')) {
$prefixes = array_filter($prefixes, fn ($prefix) => str_contains($prefix, $input->getArgument('prefix')));
}
$suggestions->suggestValues($prefixes);
}
private function formatIcon(OutputInterface $output, string $icon, bool $padding = true): string
{
if (!$output->getFormatter()->hasStyle('icon-prefix')) {
$output->getFormatter()->setStyle('icon-prefix', new OutputFormatterStyle('bright-white', 'black'));
}
if (!$output->getFormatter()->hasStyle('icon-name')) {
$output->getFormatter()->setStyle('icon-name', new OutputFormatterStyle('bright-magenta', 'black'));
}
[$prefix, $name] = explode(':', $icon.':');
$padding = $padding ? ' ' : '';
return \sprintf('<icon-prefix>%s%s:</><icon-name>%s%s</>', $padding, $prefix, $name, $padding);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\UX\Icons\IconCacheWarmer;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
#[AsCommand(
name: 'ux:icons:warm-cache',
description: 'Warm the icon cache',
)]
final class WarmCacheCommand extends Command
{
public function __construct(private IconCacheWarmer $warmer)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->comment('Warming the icon cache...');
$this->warmer->warm(
onSuccess: function (string $name) use ($io) {
if ($io->isVerbose()) {
$io->writeln(\sprintf(' Warmed icon <comment>%s</comment>.', $name));
}
},
onFailure: function (string $name, \Exception $e) use ($io) {
if ($io->isVerbose()) {
$io->writeln(\sprintf(' Failed to warm (potential) icon <error>%s</error>.', $name));
}
}
);
$io->success('Icon cache warmed.');
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,180 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\DependencyInjection;
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
use Symfony\UX\Icons\Iconify;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class UXIconsExtension extends ConfigurableExtension implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$builder = new TreeBuilder('ux_icons');
$rootNode = $builder->getRootNode();
$rootNode
->children()
->scalarNode('icon_dir')
->info('The local directory where icons are stored.')
->defaultValue('%kernel.project_dir%/assets/icons')
->end()
->variableNode('default_icon_attributes')
->info('Default attributes to add to all icons.')
->defaultValue(['fill' => 'currentColor'])
->example(['class' => 'icon'])
->end()
->arrayNode('icon_sets')
->info('Icon sets configuration.')
->defaultValue([])
->normalizeKeys(false)
->useAttributeAsKey('prefix')
->arrayPrototype()
->info('the icon set prefix (e.g. "acme")')
->children()
->scalarNode('path')
->info("The local icon set directory path.\n(cannot be used with 'alias')")
->example('%kernel.project_dir%/assets/svg/acme')
->end()
->scalarNode('alias')
->info("The remote icon set identifier.\n(cannot be used with 'path')")
->example('simple-icons')
->end()
->arrayNode('icon_attributes')
->info('Override default icon attributes for icons in this set.')
->example(['class' => 'icon icon-acme', 'fill' => 'none'])
->normalizeKeys(false)
->variablePrototype()
->end()
->end()
->end()
->end()
->validate()
->ifTrue(fn (array $v) => isset($v['path']) && isset($v['alias']))
->thenInvalid('You cannot define both "path" and "alias" for an icon set.')
->end()
->end()
->arrayNode('aliases')
->info('Icon aliases (map of alias => full name).')
->example([
'dots' => 'clarity:ellipsis-horizontal-line',
'privacy' => 'bi:cookie',
])
->normalizeKeys(false)
->scalarPrototype()
->cannotBeEmpty()
->end()
->end()
->arrayNode('iconify')
->info('Configuration for the remote icon service.')
->canBeDisabled()
->children()
->booleanNode('on_demand')
->info('Whether to download icons "on demand".')
->defaultTrue()
->end()
->scalarNode('endpoint')
->info('The endpoint for the Iconify icons API.')
->defaultValue(Iconify::API_ENDPOINT)
->cannotBeEmpty()
->end()
->end()
->end()
->booleanNode('ignore_not_found')
->info("Ignore error when an icon is not found.\nSet to 'true' to fail silently.")
->defaultFalse()
->end()
->end()
;
return $builder;
}
public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
{
return $this;
}
protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));
$loader->load('services.php');
if (isset($container->getParameter('kernel.bundles')['TwigComponentBundle'])) {
$loader->load('twig_component.php');
}
if (class_exists(PreAssetsCompileEvent::class)) {
$loader->load('asset_mapper.php');
}
$iconSetAliases = [];
$iconSetAttributes = [];
$iconSetPaths = [];
foreach ($mergedConfig['icon_sets'] as $prefix => $config) {
if (isset($config['icon_attributes'])) {
$iconSetAttributes[$prefix] = $config['icon_attributes'];
}
if (isset($config['alias'])) {
$iconSetAliases[$prefix] = $config['alias'];
}
if (isset($config['path'])) {
$iconSetPaths[$prefix] = $config['path'];
}
}
$container->getDefinition('.ux_icons.local_svg_icon_registry')
->setArguments([
$mergedConfig['icon_dir'],
$iconSetPaths,
])
;
$container->getDefinition('.ux_icons.icon_finder')
->setArgument(1, $mergedConfig['icon_dir'])
;
$container->getDefinition('.ux_icons.icon_renderer')
->setArgument(1, $mergedConfig['default_icon_attributes'])
->setArgument(2, $mergedConfig['aliases'])
->setArgument(3, $iconSetAttributes)
;
$container->getDefinition('.ux_icons.twig_icon_runtime')
->setArgument(1, $mergedConfig['ignore_not_found'])
;
$container->getDefinition('.ux_icons.iconify')
->setArgument(1, $mergedConfig['iconify']['endpoint']);
$container->getDefinition('.ux_icons.iconify_on_demand_registry')
->setArgument(1, $iconSetAliases);
$container->getDefinition('.ux_icons.command.lock')
->setArgument(3, $mergedConfig['aliases'])
->setArgument(4, $iconSetAliases);
if (!$mergedConfig['iconify']['on_demand'] || !$mergedConfig['iconify']['enabled']) {
$container->removeDefinition('.ux_icons.iconify_on_demand_registry');
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\EventListener;
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
use Symfony\UX\Icons\IconCacheWarmer;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class WarmIconCacheOnAssetCompileListener
{
public function __construct(private IconCacheWarmer $warmer)
{
}
public function __invoke(PreAssetsCompileEvent $event): void
{
$event->getOutput()->writeln('Warming the icon cache...');
$this->warmer->warm();
$event->getOutput()->writeln('Icon cache warmed.');
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Exception;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class HttpClientNotInstalledException extends \LogicException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Exception;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class IconNotFoundException extends \RuntimeException
{
}

214
vendor/symfony/ux-icons/src/Icon.php vendored Normal file
View File

@@ -0,0 +1,214 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
/**
* @author Simon André <smn.andre@gmail.com>
*
* @internal
*/
final class Icon implements \Stringable
{
/**
* Transforms a valid icon ID into an icon name.
*
* @throws \InvalidArgumentException if the ID is not valid
*
* @see isValidId()
*/
public static function idToName(string $id): string
{
if (!self::isValidId($id)) {
throw new \InvalidArgumentException(\sprintf('The id "%s" is not a valid id.', $id));
}
return str_replace('--', ':', $id);
}
/**
* Transforms a valid icon name into an ID.
*
* @throws \InvalidArgumentException if the name is not valid
*
* @see isValidName()
*/
public static function nameToId(string $name): string
{
if (!self::isValidName($name)) {
throw new \InvalidArgumentException(\sprintf('The name "%s" is not a valid name.', $name));
}
return str_replace(':', '--', $name);
}
/**
* Returns whether the given string is a valid icon ID.
*
* An icon ID is a string that contains only lowercase letters, numbers, and hyphens.
* It must be composed of slugs separated by double hyphens.
*
* @see https://regex101.com/r/mmvl5t/1
*/
public static function isValidId(string $id): bool
{
return (bool) preg_match('#^([a-z0-9]+(-[a-z0-9]+)*)(--[a-z0-9]+(-[a-z0-9]+)*)*$#', $id);
}
/**
* Returns whether the given string is a valid icon name.
*
* An icon name is a string that contains only lowercase letters, numbers, and hyphens.
* It must be composed of slugs separated by colons.
*
* @see https://regex101.com/r/Gh2Z9s/1
*/
public static function isValidName(string $name): bool
{
return (bool) preg_match('#^([a-z0-9]+(-[a-z0-9]+)*)(:[a-z0-9]+(-[a-z0-9]+)*)*$#', $name);
}
public static function fromFile(string $filename): self
{
if (!class_exists(\DOMDocument::class)) {
throw new \LogicException('The "DOM" PHP extension is required to create icons from files.');
}
$svg = file_get_contents($filename) ?: throw new \RuntimeException(\sprintf('The icon file "%s" could not be read.', $filename));
$svgDoc = new \DOMDocument();
$svgDoc->preserveWhiteSpace = false;
try {
$svgDoc->loadXML($svg);
} catch (\Throwable $e) {
throw new \RuntimeException(\sprintf('The icon file "%s" does not contain a valid SVG.', $filename), previous: $e);
}
$svgElements = $svgDoc->getElementsByTagName('svg');
if (0 === $svgElements->length) {
throw new \RuntimeException(\sprintf('The icon file "%s" does not contain a valid SVG.', $filename));
}
if (1 !== $svgElements->length) {
throw new \RuntimeException(\sprintf('The icon file "%s" contains more than one SVG.', $filename));
}
$svgElement = $svgElements->item(0) ?? throw new \RuntimeException(\sprintf('The icon file "%s" does not contain a valid SVG.', $filename));
$innerSvg = '';
foreach ($svgElement->childNodes as $node) {
// Ignore comments and text nodes
if ($node instanceof \DOMComment || $node instanceof \DOMText) {
continue;
}
// Ignore script tags
if ($node instanceof \DOMElement && 'script' === $node->nodeName) {
continue;
}
$innerSvg .= $svgDoc->saveHTML($node);
}
if (!$innerSvg) {
throw new \RuntimeException(\sprintf('The icon file "%s" contains an empty SVG.', $filename));
}
$attributes = array_map(static fn (\DOMAttr $a) => $a->value, [...$svgElement->attributes]);
return new self($innerSvg, $attributes);
}
public function __construct(
private readonly string $innerSvg,
private readonly array $attributes = [],
) {
}
public function toHtml(): string
{
$htmlAttributes = '';
foreach ($this->attributes as $name => $value) {
if (false === $value) {
continue;
}
// Special case for aria-* attributes
// https://www.w3.org/TR/wai-aria-1.1/#state_prop_def
if (true === $value && str_starts_with($name, 'aria-')) {
$value = 'true';
}
$htmlAttributes .= ' '.$name;
if (true === $value) {
continue;
}
$value = htmlspecialchars($value, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
$htmlAttributes .= '="'.$value.'"';
}
return '<svg'.$htmlAttributes.'>'.$this->innerSvg.'</svg>';
}
public function getInnerSvg(): string
{
return $this->innerSvg;
}
/**
* @return array<string, string|bool>
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* @param array<string, string|bool|int|float> $attributes
*/
public function withAttributes(array $attributes): self
{
foreach ($attributes as $name => $value) {
if (!\is_string($name)) {
throw new \InvalidArgumentException(\sprintf('Attribute names must be string, "%s" given.', get_debug_type($name)));
}
if (!ctype_alnum($name) && !str_contains($name, '-')) {
throw new \InvalidArgumentException(\sprintf('Invalid attribute name "%s".', $name));
}
if (!\is_string($value) && !\is_bool($value) && !\is_int($value) && !\is_float($value)) {
throw new \InvalidArgumentException(\sprintf('Invalid value type for attribute "%s". Boolean, string, int or float allowed, "%s" provided. ', $name, get_debug_type($value)));
}
}
return new self($this->innerSvg, [...$this->attributes, ...$attributes]);
}
public function __toString(): string
{
return $this->toHtml();
}
public function __serialize(): array
{
return [$this->innerSvg, $this->attributes];
}
public function __unserialize(array $data): void
{
[$this->innerSvg, $this->attributes] = $data;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Registry\CacheIconRegistry;
use Symfony\UX\Icons\Twig\IconFinder;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class IconCacheWarmer
{
public function __construct(private CacheIconRegistry $registry, private IconFinder $icons)
{
}
/**
* @param callable(string,Icon):void|null $onSuccess
* @param callable(string,\Exception):void|null $onFailure
*/
public function warm(?callable $onSuccess = null, ?callable $onFailure = null): void
{
$onSuccess ??= fn (string $name, Icon $icon) => null;
$onFailure ??= fn (string $name) => null;
foreach ($this->icons->icons() as $name) {
try {
$icon = $this->registry->get($name, refresh: true);
$onSuccess($name, $icon);
} catch (IconNotFoundException $e) {
$onFailure($name, $e);
}
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
use Symfony\UX\Icons\Exception\IconNotFoundException;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @extends \IteratorAggregate<string>
*
* @internal
*/
interface IconRegistryInterface
{
/**
* @throws IconNotFoundException
*/
public function get(string $name): Icon;
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class IconRenderer implements IconRendererInterface
{
/**
* @param array<string, mixed> $defaultIconAttributes
* @param array<string, string> $iconAliases
* @param array<string, array<string, mixed>> $iconSetsAttributes
*/
public function __construct(
private readonly IconRegistryInterface $registry,
private readonly array $defaultIconAttributes = [],
private readonly array $iconAliases = [],
private readonly array $iconSetsAttributes = [],
) {
}
/**
* Renders an icon.
*
* Provided attributes are merged with the default attributes.
* Existing icon attributes are then merged with those new attributes.
*
* Precedence order:
* Icon file < Renderer configuration < Renderer invocation
*/
public function renderIcon(string $name, array $attributes = []): string
{
$iconName = $this->iconAliases[$name] ?? $name;
$icon = $this->registry->get($iconName);
if (0 < (int) $pos = strpos($name, ':')) {
$setAttributes = $this->iconSetsAttributes[substr($name, 0, $pos)] ?? [];
} elseif ($iconName !== $name && 0 < (int) $pos = strpos($iconName, ':')) {
$setAttributes = $this->iconSetsAttributes[substr($iconName, 0, $pos)] ?? [];
}
$icon = $icon->withAttributes([...$this->defaultIconAttributes, ...($setAttributes ?? []), ...$attributes]);
foreach ($this->getPreRenderers() as $preRenderer) {
$icon = $preRenderer($icon);
}
return $icon->toHtml();
}
/**
* @return iterable<callable(Icon): Icon>
*/
private function getPreRenderers(): iterable
{
yield self::setAriaHidden(...);
}
/**
* Set `aria-hidden=true` if not defined & no textual alternative provided.
*/
private static function setAriaHidden(Icon $icon): Icon
{
if ([] === array_intersect(['aria-hidden', 'aria-label', 'aria-labelledby', 'title'], array_keys($icon->getAttributes()))) {
return $icon->withAttributes(['aria-hidden' => 'true']);
}
return $icon;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
use Symfony\UX\Icons\Exception\IconNotFoundException;
/**
* @author Simon André <smn.andre@gmail.com>
* @author Kevin Bond <kevinbond@gmail.com>
*/
interface IconRendererInterface
{
/**
* Renders an icon by its name and returns the SVG string.
*
* @param string $name the icon name, optionally prefixed with the icon set
* @param array<string, string|bool> $attributes an array of HTML attributes
*
* @throws IconNotFoundException
*
* @example
* $iconRenderer->renderIcon('arrow-right');
* // Renders the "arrow-right" icon from the default icons directory.
*
* $iconRenderer->renderIcon('lucide:heart', ['class' => 'color-red']);
* // Renders the "heart" icon from the "lucide" icon set, with the "color-red" class.
*/
public function renderIcon(string $name, array $attributes = []): string;
}

223
vendor/symfony/ux-icons/src/Iconify.php vendored Normal file
View File

@@ -0,0 +1,223 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\UX\Icons\Exception\HttpClientNotInstalledException;
use Symfony\UX\Icons\Exception\IconNotFoundException;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class Iconify
{
public const API_ENDPOINT = 'https://api.iconify.design';
private const ATTR_XMLNS_URL = 'http://www.w3.org/2000/svg';
// URL must be 500 chars max (iconify limit)
// -39 chars: https://api.iconify.design/XXX.json?icons=
// -safe margin
private const MAX_ICONS_QUERY_LENGTH = 400;
// https://github.com/iconify/iconify/blob/00cc144b040b838bd86474ab83f0e50e6c6a12a1/packages/utils/src/icon/defaults.ts#L23-L30
private const DEFAULT_ICON_WIDTH = 16;
private const DEFAULT_ICON_HEIGHT = 16;
private HttpClientInterface $http;
private \ArrayObject $sets;
private int $maxIconsQueryLength;
public function __construct(
private CacheInterface $cache,
private string $endpoint = self::API_ENDPOINT,
private ?HttpClientInterface $httpClient = null,
?int $maxIconsQueryLength = null,
) {
$this->maxIconsQueryLength = min(self::MAX_ICONS_QUERY_LENGTH, $maxIconsQueryLength ?? self::MAX_ICONS_QUERY_LENGTH);
}
public function metadataFor(string $prefix): array
{
return $this->sets()[$prefix] ?? throw new \RuntimeException(\sprintf('The icon prefix "%s" does not exist on iconify.design.', $prefix));
}
public function fetchIcon(string $prefix, string $name): Icon
{
if (!isset($this->sets()[$prefix])) {
throw new IconNotFoundException(\sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
}
$response = $this->http()->request('GET', \sprintf('/%s.json?icons=%s', $prefix, $name));
if (200 !== $response->getStatusCode()) {
throw new IconNotFoundException(\sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
}
try {
$data = $response->toArray();
} catch (JsonException) {
throw new IconNotFoundException(\sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
}
$nameArg = $name;
if (isset($data['aliases'][$name])) {
$name = $data['aliases'][$name]['parent'];
}
if (!isset($data['icons'][$name]['body'])) {
throw new IconNotFoundException(\sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $nameArg));
}
$height = $data['icons'][$name]['height'] ?? $data['height'] ?? $this->sets()[$prefix]['height'] ?? null;
$width = $data['icons'][$name]['width'] ?? $data['width'] ?? $this->sets()[$prefix]['width'] ?? null;
return new Icon($data['icons'][$name]['body'], [
'xmlns' => self::ATTR_XMLNS_URL,
'viewBox' => \sprintf('0 0 %s %s', $width ?? $height ?? self::DEFAULT_ICON_WIDTH, $height ?? $width ?? self::DEFAULT_ICON_HEIGHT),
]);
}
public function fetchIcons(string $prefix, array $names): array
{
if (!isset($this->sets()[$prefix])) {
throw new IconNotFoundException(\sprintf('The icon set "%s" does not exist on iconify.design.', $prefix));
}
// Sort to enforce cache hits
sort($names);
$queryString = implode(',', $names);
if (!preg_match('#^[a-z0-9-,]+$#', $queryString)) {
throw new \InvalidArgumentException('Invalid icon names.'.$queryString);
}
if (self::MAX_ICONS_QUERY_LENGTH < \strlen($prefix.$queryString)) {
throw new \InvalidArgumentException('The query string is too long.');
}
$response = $this->http()->request('GET', \sprintf('/%s.json', $prefix), [
'headers' => [
'Accept' => 'application/json',
],
'query' => [
'icons' => strtolower($queryString),
],
]);
if (200 !== $response->getStatusCode()) {
throw new IconNotFoundException(\sprintf('The icon set "%s" does not exist on iconify.design.', $prefix));
}
$data = $response->toArray();
$icons = [];
foreach ($names as $iconName) {
$iconData = $data['icons'][$data['aliases'][$iconName]['parent'] ?? $iconName] ?? null;
if (!$iconData) {
continue;
}
$height = $iconData['height'] ?? $data['height'] ??= $this->sets()[$prefix]['height'] ?? null;
$width = $iconData['width'] ?? $data['width'] ??= $this->sets()[$prefix]['width'] ?? null;
$icons[$iconName] = new Icon($iconData['body'], [
'xmlns' => self::ATTR_XMLNS_URL,
'viewBox' => \sprintf('0 0 %d %d', $width ?? $height ?? self::DEFAULT_ICON_WIDTH, $height ?? $width ?? self::DEFAULT_ICON_HEIGHT),
]);
}
return $icons;
}
public function hasIconSet(string $prefix): bool
{
return isset($this->sets()[$prefix]);
}
public function getIconSets(): array
{
return $this->sets()->getArrayCopy();
}
public function searchIcons(string $prefix, string $query)
{
$response = $this->http()->request('GET', '/search', [
'query' => [
'query' => $query,
'prefix' => $prefix,
],
]);
return new \ArrayObject($response->toArray());
}
/**
* @return iterable<string[]>
*/
public function chunk(string $prefix, array $names): iterable
{
if (100 < ($prefixLength = \strlen($prefix))) {
throw new \InvalidArgumentException(\sprintf('The icon prefix "%s" is too long.', $prefix));
}
$maxLength = $this->maxIconsQueryLength - $prefixLength;
$curBatch = [];
$curLength = 0;
foreach ($names as $name) {
if (100 < ($nameLength = \strlen($name))) {
throw new \InvalidArgumentException(\sprintf('The icon name "%s" is too long.', $name));
}
if ($curLength && ($maxLength < ($curLength + $nameLength + 1))) {
yield $curBatch;
$curBatch = [];
$curLength = 0;
}
$curLength += $nameLength + 1;
$curBatch[] = $name;
}
if ($curLength) {
yield $curBatch;
}
yield from [];
}
private function sets(): \ArrayObject
{
return $this->sets ??= $this->cache->get('iconify-sets', function () {
$response = $this->http()->request('GET', '/collections');
return new \ArrayObject($response->toArray());
});
}
private function http(): HttpClientInterface
{
if (isset($this->http)) {
return $this->http;
}
if (!class_exists(HttpClient::class)) {
throw new HttpClientNotInstalledException('You must install "symfony/http-client" to use icons from ux.symfony.com/icons. Try running "composer require symfony/http-client".');
}
return $this->http = ScopingHttpClient::forBaseUri($this->httpClient ?? HttpClient::create(), $this->endpoint);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Registry;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Icon;
use Symfony\UX\Icons\IconRegistryInterface;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class CacheIconRegistry implements IconRegistryInterface
{
public function __construct(private IconRegistryInterface $inner, private CacheInterface $cache)
{
}
public function get(string $name, bool $refresh = false): Icon
{
if (!Icon::isValidName($name)) {
throw new IconNotFoundException(\sprintf('The icon name "%s" is not valid.', $name));
}
return $this->cache->get(
Icon::nameToId($name),
fn () => $this->inner->get($name),
beta: $refresh ? \INF : null,
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Registry;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Icon;
use Symfony\UX\Icons\IconRegistryInterface;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class ChainIconRegistry implements IconRegistryInterface
{
/**
* @param IconRegistryInterface[] $registries
*/
public function __construct(private iterable $registries)
{
}
public function get(string $name): Icon
{
foreach ($this->registries as $registry) {
try {
return $registry->get($name);
} catch (IconNotFoundException $e) {
}
}
$message = \sprintf('Icon "%s" not found.', $name);
if (isset($e)) {
$message .= " {$e->getMessage()}";
}
throw new IconNotFoundException($message, previous: $e ?? null);
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Registry;
use Symfony\UX\Icons\Exception\HttpClientNotInstalledException;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Icon;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\IconRegistryInterface;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class IconifyOnDemandRegistry implements IconRegistryInterface
{
public function __construct(
private Iconify $iconify,
private ?array $prefixAliases = [],
) {
}
public function get(string $name): Icon
{
if (2 !== \count($parts = explode(':', $name))) {
throw new IconNotFoundException(\sprintf('The icon name "%s" is not valid.', $name));
}
[$prefix, $icon] = $parts;
try {
return $this->iconify->fetchIcon($this->prefixAliases[$prefix] ?? $prefix, $icon);
} catch (HttpClientNotInstalledException $e) {
throw new IconNotFoundException($e->getMessage());
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Registry;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Icon;
use Symfony\UX\Icons\IconRegistryInterface;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class LocalSvgIconRegistry implements IconRegistryInterface
{
/**
* @param array<string, string> $iconSetPaths
*/
public function __construct(
private readonly string $iconDir,
private readonly array $iconSetPaths = [],
) {
}
public function get(string $name): Icon
{
if (str_contains($name, ':')) {
[$prefix, $icon] = explode(':', $name, 2) + ['', ''];
if ('' === $prefix || '' === $icon) {
throw new IconNotFoundException(\sprintf('The icon name "%s" is not valid.', $name));
}
if ($prefixPath = $this->iconSetPaths[$prefix] ?? null) {
if (!file_exists($filename = $prefixPath.'/'.str_replace(':', '/', $icon).'.svg')) {
throw new IconNotFoundException(\sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
}
return Icon::fromFile($filename);
}
}
$filepath = str_replace(':', '/', $name).'.svg';
if (file_exists($filename = $this->iconDir.'/'.$filepath)) {
return Icon::fromFile($filename);
}
throw new IconNotFoundException(\sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
}
public function has(string $name): bool
{
try {
$this->get($name);
return true;
} catch (IconNotFoundException) {
return false;
}
}
public function add(string $name, string $svg): void
{
$filename = \sprintf('%s/%s.svg', $this->iconDir, $name);
(new Filesystem())->dumpFile($filename, $svg);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Twig;
use Symfony\Component\Finder\Finder;
use Twig\Environment;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
use Twig\Loader\LoaderInterface;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class IconFinder
{
public function __construct(
private Environment $twig,
private string $iconDirectory,
) {
}
/**
* @return string[]
*/
public function icons(): array
{
$found = [];
// https://regex101.com/r/WGa4iF/1
$token = '[a-z0-9]+(?:-[a-z0-9]+)*';
$pattern = "#(?:'$token:$token')|(?:\"$token:$token\")#i";
// Extract icon names from strings in app templates
foreach ($this->templateFiles($this->twig->getLoader()) as $file) {
$contents = file_get_contents($file);
if (preg_match_all($pattern, $contents, $matches)) {
$found[] = array_map(fn ($res) => trim($res, '"\''), $matches[0]);
}
}
$found = array_merge(...$found);
// Extract prefix-less SVG files from the root of the icon directory
if (is_dir($this->iconDirectory)) {
$icons = (new Finder())->files()->in($this->iconDirectory)->depth(0)->name('*.svg');
foreach ($icons as $icon) {
$found[] = $icon->getBasename('.svg');
}
}
return array_unique($found);
}
/**
* @return string[]
*/
private function templateFiles(LoaderInterface $loader): iterable
{
if ($loader instanceof FilesystemLoader) {
$paths = $loader->getPaths();
foreach ($loader->getNamespaces() as $namespace) {
$paths = [...$paths, ...$loader->getPaths($namespace)];
}
if ($paths) {
foreach ((new Finder())->files()->in($paths)->name('*.twig') as $file) {
yield (string) $file;
}
}
}
if ($loader instanceof ChainLoader) {
foreach ($loader->getLoaders() as $subLoader) {
yield from $this->templateFiles($subLoader);
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Twig;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class UXIconComponent
{
public string $name;
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class UXIconExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('ux_icon', [UXIconRuntime::class, 'renderIcon'], ['is_safe' => ['html']]),
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons\Twig;
use Psr\Log\LoggerInterface;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\IconRendererInterface;
use Twig\Extension\RuntimeExtensionInterface;
/**
* @author Simon André <smn.andre@gmail.com>
*
* @internal
*/
final class UXIconRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly IconRendererInterface $iconRenderer,
private readonly bool $ignoreNotFound = false,
private readonly ?LoggerInterface $logger = null,
) {
}
/**
* @param array<string, bool|string> $attributes
*/
public function renderIcon(string $name, array $attributes = []): string
{
try {
return $this->iconRenderer->renderIcon($name, $attributes);
} catch (IconNotFoundException $e) {
if ($this->ignoreNotFound) {
$this->logger?->warning($e->getMessage());
return '';
}
throw $e;
}
}
public function render(array $args = []): string
{
$name = $args['name'];
unset($args['name']);
return $this->renderIcon($name, $args);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Icons;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class UXIconsBundle extends Bundle
{
public function getPath(): string
{
return \dirname(__DIR__);
}
}