vendor/symfony/string/Slugger/AsciiSlugger.php line 71

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\String\Slugger;
  11. use Symfony\Component\Intl\Transliterator\EmojiTransliterator;
  12. use Symfony\Component\String\AbstractUnicodeString;
  13. use Symfony\Component\String\UnicodeString;
  14. use Symfony\Contracts\Translation\LocaleAwareInterface;
  15. if (!interface_exists(LocaleAwareInterface::class)) {
  16.     throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".');
  17. }
  18. /**
  19.  * @author Titouan Galopin <galopintitouan@gmail.com>
  20.  */
  21. class AsciiSlugger implements SluggerInterfaceLocaleAwareInterface
  22. {
  23.     private const LOCALE_TO_TRANSLITERATOR_ID = [
  24.         'am' => 'Amharic-Latin',
  25.         'ar' => 'Arabic-Latin',
  26.         'az' => 'Azerbaijani-Latin',
  27.         'be' => 'Belarusian-Latin',
  28.         'bg' => 'Bulgarian-Latin',
  29.         'bn' => 'Bengali-Latin',
  30.         'de' => 'de-ASCII',
  31.         'el' => 'Greek-Latin',
  32.         'fa' => 'Persian-Latin',
  33.         'he' => 'Hebrew-Latin',
  34.         'hy' => 'Armenian-Latin',
  35.         'ka' => 'Georgian-Latin',
  36.         'kk' => 'Kazakh-Latin',
  37.         'ky' => 'Kirghiz-Latin',
  38.         'ko' => 'Korean-Latin',
  39.         'mk' => 'Macedonian-Latin',
  40.         'mn' => 'Mongolian-Latin',
  41.         'or' => 'Oriya-Latin',
  42.         'ps' => 'Pashto-Latin',
  43.         'ru' => 'Russian-Latin',
  44.         'sr' => 'Serbian-Latin',
  45.         'sr_Cyrl' => 'Serbian-Latin',
  46.         'th' => 'Thai-Latin',
  47.         'tk' => 'Turkmen-Latin',
  48.         'uk' => 'Ukrainian-Latin',
  49.         'uz' => 'Uzbek-Latin',
  50.         'zh' => 'Han-Latin',
  51.     ];
  52.     private ?string $defaultLocale;
  53.     private \Closure|array $symbolsMap = [
  54.         'en' => ['@' => 'at''&' => 'and'],
  55.     ];
  56.     private bool|string $emoji false;
  57.     /**
  58.      * Cache of transliterators per locale.
  59.      *
  60.      * @var \Transliterator[]
  61.      */
  62.     private array $transliterators = [];
  63.     public function __construct(string $defaultLocale null, array|\Closure $symbolsMap null)
  64.     {
  65.         $this->defaultLocale $defaultLocale;
  66.         $this->symbolsMap $symbolsMap ?? $this->symbolsMap;
  67.     }
  68.     public function setLocale(string $locale)
  69.     {
  70.         $this->defaultLocale $locale;
  71.     }
  72.     public function getLocale(): string
  73.     {
  74.         return $this->defaultLocale;
  75.     }
  76.     /**
  77.      * @param bool|string $emoji true will use the same locale,
  78.      *                           false will disable emoji,
  79.      *                           and a string to use a specific locale
  80.      */
  81.     public function withEmoji(bool|string $emoji true): static
  82.     {
  83.         if (false !== $emoji && !class_exists(EmojiTransliterator::class)) {
  84.             throw new \LogicException(sprintf('You cannot use the "%s()" method as the "symfony/intl" package is not installed. Try running "composer require symfony/intl".'__METHOD__));
  85.         }
  86.         $new = clone $this;
  87.         $new->emoji $emoji;
  88.         return $new;
  89.     }
  90.     public function slug(string $stringstring $separator '-'string $locale null): AbstractUnicodeString
  91.     {
  92.         $locale ??= $this->defaultLocale;
  93.         $transliterator = [];
  94.         if ($locale && ('de' === $locale || str_starts_with($locale'de_'))) {
  95.             // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl)
  96.             $transliterator = ['de-ASCII'];
  97.         } elseif (\function_exists('transliterator_transliterate') && $locale) {
  98.             $transliterator = (array) $this->createTransliterator($locale);
  99.         }
  100.         if ($emojiTransliterator $this->createEmojiTransliterator($locale)) {
  101.             $transliterator[] = $emojiTransliterator;
  102.         }
  103.         if ($this->symbolsMap instanceof \Closure) {
  104.             // If the symbols map is passed as a closure, there is no need to fallback to the parent locale
  105.             // as the closure can just provide substitutions for all locales of interest.
  106.             $symbolsMap $this->symbolsMap;
  107.             array_unshift($transliterator, static function ($s) use ($symbolsMap$locale) {
  108.                 return $symbolsMap($s$locale);
  109.             });
  110.         }
  111.         $unicodeString = (new UnicodeString($string))->ascii($transliterator);
  112.         if (\is_array($this->symbolsMap)) {
  113.             $map null;
  114.             if (isset($this->symbolsMap[$locale])) {
  115.                 $map $this->symbolsMap[$locale];
  116.             } else {
  117.                 $parent self::getParentLocale($locale);
  118.                 if ($parent && isset($this->symbolsMap[$parent])) {
  119.                     $map $this->symbolsMap[$parent];
  120.                 }
  121.             }
  122.             if ($map) {
  123.                 foreach ($map as $char => $replace) {
  124.                     $unicodeString $unicodeString->replace($char' '.$replace.' ');
  125.                 }
  126.             }
  127.         }
  128.         return $unicodeString
  129.             ->replaceMatches('/[^A-Za-z0-9]++/'$separator)
  130.             ->trim($separator)
  131.         ;
  132.     }
  133.     private function createTransliterator(string $locale): ?\Transliterator
  134.     {
  135.         if (\array_key_exists($locale$this->transliterators)) {
  136.             return $this->transliterators[$locale];
  137.         }
  138.         // Exact locale supported, cache and return
  139.         if ($id self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) {
  140.             return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
  141.         }
  142.         // Locale not supported and no parent, fallback to any-latin
  143.         if (!$parent self::getParentLocale($locale)) {
  144.             return $this->transliterators[$locale] = null;
  145.         }
  146.         // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
  147.         if ($id self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
  148.             $transliterator \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
  149.         }
  150.         return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null;
  151.     }
  152.     private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator
  153.     {
  154.         if (\is_string($this->emoji)) {
  155.             $locale $this->emoji;
  156.         } elseif (!$this->emoji) {
  157.             return null;
  158.         }
  159.         while (null !== $locale) {
  160.             try {
  161.                 return EmojiTransliterator::create("emoji-$locale");
  162.             } catch (\IntlException) {
  163.                 $locale self::getParentLocale($locale);
  164.             }
  165.         }
  166.         return null;
  167.     }
  168.     private static function getParentLocale(?string $locale): ?string
  169.     {
  170.         if (!$locale) {
  171.             return null;
  172.         }
  173.         if (false === $str strrchr($locale'_')) {
  174.             // no parent locale
  175.             return null;
  176.         }
  177.         return substr($locale0, -\strlen($str));
  178.     }
  179. }