cheKey][$threeChars]) && \strpos($str, $threeChars) !== false ) { // DEBUG //\var_dump($str, $threeChars, $REPLACE_HELPER_CACHE[$cacheKey][$threeChars]); $charDone[$threeChars] = true; $str = \str_replace($threeChars, $REPLACE_HELPER_CACHE[$cacheKey][$threeChars], $str); // DEBUG //\var_dump($str, "\n"); } } foreach ($matches[0] as $keyTmp => $char) { if (isset($matches[0][$keyTmp + 1])) { $twoChars = $matches[0][$keyTmp + 0] . $matches[0][$keyTmp + 1]; } else { $twoChars = null; } if ( $twoChars && !isset($charDone[$twoChars]) && isset($REPLACE_HELPER_CACHE[$cacheKey][$twoChars]) && \strpos($str, $twoChars) !== false ) { // DEBUG //\var_dump($str, $twoChars, $REPLACE_HELPER_CACHE[$cacheKey][$twoChars]); $charDone[$twoChars] = true; $str = \str_replace($twoChars, $REPLACE_HELPER_CACHE[$cacheKey][$twoChars], $str); // DEBUG //\var_dump($str, "\n"); } } } foreach ($matches[0] as $char) { if ( !isset($charDone[$char]) && isset($REPLACE_HELPER_CACHE[$cacheKey][$char]) && \strpos($str, $char) !== false ) { // DEBUG //\var_dump($str, $char, $REPLACE_HELPER_CACHE[$cacheKey][$char]); $charDone[$char] = true; $str = \str_replace($char, $REPLACE_HELPER_CACHE[$cacheKey][$char], $str); // DEBUG //\var_dump($str, "\n"); } } } if (!isset(self::$ASCII_MAPS[$language])) { $use_transliterate = true; } if ($use_transliterate) { $str = self::to_transliterate($str, null, false); } if ($remove_unsupported_chars) { $str = (string) \str_replace(["\n\r", "\n", "\r", "\t"], ' ', $str); $str = (string) \preg_replace('/' . self::$REGEX_ASCII . '/', '', $str); } return $str; } /** * Convert given string to safe filename (and keep string case). * * EXAMPLE: * ASCII::to_filename('שדגשדג.png', true)); // 'shdgshdg.png' * * * @param string $str

The string input.

* @param bool $use_transliterate

ASCII::to_transliterate() is used by default - unsafe characters are * simply replaced with hyphen otherwise.

* @param string $fallback_char

The fallback character. - "-" is the default

* * @psalm-pure * * @return string *

A string that contains only safe characters for a filename.

*/ public static function to_filename( string $str, bool $use_transliterate = true, string $fallback_char = '-' ): string { if ($use_transliterate) { $str = self::to_transliterate($str, $fallback_char); } $fallback_char_escaped = \preg_quote($fallback_char, '/'); $str = (string) \preg_replace( [ '/[^' . $fallback_char_escaped . '.\\-a-zA-Z\d\\s]/', // 1) remove un-needed chars '/\s+/u', // 2) convert spaces to $fallback_char '/[' . $fallback_char_escaped . ']+/u', // 3) remove double $fallback_char's ], [ '', $fallback_char, $fallback_char, ], $str ); return \trim($str, $fallback_char); } /** * Converts a string into a URL-friendly slug. * * - This includes replacing non-ASCII characters with their closest ASCII equivalents, removing remaining * non-ASCII and non-alphanumeric characters, and replacing whitespace with $separator. * - The separator defaults to a single dash, and the string is also converted to lowercase. * - The language of the source string can also be supplied for language-specific transliteration. * * @param string $str

The string input.

* @param string $separator [optional]

The string used to replace whitespace.

* @param string $language [optional]

Language of the source string. * (default is 'en') | ASCII::*_LANGUAGE_CODE

* @param array $replacements [optional]

A map of replaceable strings.

* @param bool $replace_extra_symbols [optional]

Add some more replacements e.g. "£" with " * pound ".

* @param bool $use_str_to_lower [optional]

Use "string to lower" for the input.

* @param bool $use_transliterate [optional]

Use ASCII::to_transliterate() for unknown * chars.

* @psalm-pure * * @return string *

The URL-friendly slug.

* * @phpstan-param ASCII::*_LANGUAGE_CODE $language */ public static function to_slugify( string $str, string $separator = '-', string $language = self::ENGLISH_LANGUAGE_CODE, array $replacements = [], bool $replace_extra_symbols = false, bool $use_str_to_lower = true, bool $use_transliterate = false ): string { if ($str === '') { return ''; } foreach ($replacements as $from => $to) { $str = \str_replace($from, $to, $str); } $str = self::to_ascii( $str, $language, false, $replace_extra_symbols, $use_transliterate ); $str = \str_replace('@', $separator, $str); $str = (string) \preg_replace( '/[^a-zA-Z\\d\\s\\-_' . \preg_quote($separator, '/') . ']/', '', $str ); if ($use_str_to_lower) { $str = \strtolower($str); } $str = (string) \preg_replace('/^[\'\\s]+|[\'\\s]+$/', '', $str); $str = (string) \preg_replace('/\\B([A-Z])/', '-\1', $str); $str = (string) \preg_replace('/[\\-_\\s]+/', $separator, $str); $l = \strlen($separator); if ($l && \strpos($str, $separator) === 0) { $str = (string) \substr($str, $l); } if (\substr($str, -$l) === $separator) { $str = (string) \substr($str, 0, \strlen($str) - $l); } return $str; } /** * Returns an ASCII version of the string. A set of non-ASCII characters are * replaced with their closest ASCII counterparts, and the rest are removed * unless instructed otherwise. * * EXAMPLE: * ASCII::to_transliterate('déjà σσς iıii'); // 'deja sss iiii' * * * @param string $str

The input string.

* @param string|null $unknown [optional]

Character use if character unknown. (default is '?') * But you can also use NULL to keep the unknown chars.

* @param bool $strict [optional]

Use "transliterator_transliterate()" from PHP-Intl * * @psalm-pure * * @return string *

A String that contains only ASCII characters.

*/ public static function to_transliterate( string $str, $unknown = '?', bool $strict = false ): string { static $UTF8_TO_TRANSLIT = null; static $TRANSLITERATOR = null; static $SUPPORT_INTL = null; if ($str === '') { return ''; } if ($SUPPORT_INTL === null) { $SUPPORT_INTL = \extension_loaded('intl'); } // check if we only have ASCII, first (better performance) $str_tmp = $str; if (self::is_ascii($str)) { return $str; } $str = self::clean($str); // check again if we only have ASCII, now ... if ( $str_tmp !== $str && self::is_ascii($str) ) { return $str; } if ( $strict && $SUPPORT_INTL === true ) { if (!isset($TRANSLITERATOR)) { // INFO: see "*-Latin" rules via "transliterator_list_ids()" $TRANSLITERATOR = \transliterator_create('NFKC; [:Nonspacing Mark:] Remove; NFKC; Any-Latin; Latin-ASCII;'); } // INFO: https://unicode.org/cldr/utility/character.jsp $str_tmp = \transliterator_transliterate($TRANSLITERATOR, $str); if ($str_tmp !== false) { // check again if we only have ASCII, now ... if ( $str_tmp !== $str && self::is_ascii($str_tmp) ) { return $str_tmp; } $str = $str_tmp; } } if (self::$ORD === null) { self::$ORD = self::getData('ascii_ord'); } \preg_match_all('/.|[^\x00]$/us', $str, $array_tmp); $chars = $array_tmp[0]; $ord = null; $str_tmp = ''; foreach ($chars as &$c) { $ordC0 = self::$ORD[$c[0]]; if ($ordC0 >= 0 && $ordC0 <= 127) { $str_tmp .= $c; continue; } $ordC1 = self::$ORD[$c[1]]; // ASCII - next please if ($ordC0 >= 192 && $ordC0 <= 223) { $ord = ($ordC0 - 192) * 64 + ($ordC1 - 128); } if ($ordC0 >= 224) { $ordC2 = self::$ORD[$c[2]]; if ($ordC0 <= 239) { $ord = ($ordC0 - 224) * 4096 + ($ordC1 - 128) * 64 + ($ordC2 - 128); } if ($ordC0 >= 240) { $ordC3 = self::$ORD[$c[3]]; if ($ordC0 <= 247) { $ord = ($ordC0 - 240) * 262144 + ($ordC1 - 128) * 4096 + ($ordC2 - 128) * 64 + ($ordC3 - 128); } // We only process valid UTF-8 chars (<= 4 byte), so we don't need this code here ... /* if ($ordC0 >= 248) { $ordC4 = self::$ORD[$c[4]]; if ($ordC0 <= 251) { $ord = ($ordC0 - 248) * 16777216 + ($ordC1 - 128) * 262144 + ($ordC2 - 128) * 4096 + ($ordC3 - 128) * 64 + ($ordC4 - 128); } if ($ordC0 >= 252) { $ordC5 = self::$ORD[$c[5]]; if ($ordC0 <= 253) { $ord = ($ordC0 - 252) * 1073741824 + ($ordC1 - 128) * 16777216 + ($ordC2 - 128) * 262144 + ($ordC3 - 128) * 4096 + ($ordC4 - 128) * 64 + ($ordC5 - 128); } } } */ } } if ( $ordC0 === 254 || $ordC0 === 255 || $ord === null ) { $str_tmp .= $unknown ?? $c; continue; } $bank = $ord >> 8; if (!isset($UTF8_TO_TRANSLIT[$bank])) { $UTF8_TO_TRANSLIT[$bank] = self::getDataIfExists(\sprintf('x%03x', $bank)); } $new_char = $ord & 255; if (isset($UTF8_TO_TRANSLIT[$bank][$new_char])) { // keep for debugging /* echo "file: " . sprintf('x%02x', $bank) . "\n"; echo "char: " . $c . "\n"; echo "ord: " . $ord . "\n"; echo "new_char: " . $new_char . "\n"; echo "new_char: " . mb_chr($new_char) . "\n"; echo "ascii: " . $UTF8_TO_TRANSLIT[$bank][$new_char] . "\n"; echo "bank:" . $bank . "\n\n"; */ $new_char = $UTF8_TO_TRANSLIT[$bank][$new_char]; /* @noinspection PhpStatementHasEmptyBodyInspection */ if ($unknown === null && $new_char === '') { // nothing } elseif ( $new_char === '[?]' || $new_char === '[?] ' ) { $c = $unknown ?? $c; } else { $c = $new_char; } } else { // keep for debugging missing chars /* echo "file: " . sprintf('x%02x', $bank) . "\n"; echo "char: " . $c . "\n"; echo "ord: " . $ord . "\n"; echo "new_char: " . $new_char . "\n"; echo "new_char: " . mb_chr($new_char) . "\n"; echo "bank:" . $bank . "\n\n"; */ $c = $unknown ?? $c; } $str_tmp .= $c; } return $str_tmp; } /** * WARNING: This method will return broken characters and is only for special cases. * * Convert a UTF-8 encoded string to a single-byte string suitable for * functions that need the same string length after the conversion. * * The function simply uses (and updates) a tailored dynamic encoding * (in/out map parameter) where non-ascii characters are remapped to * the range [128-255] in order of appearance. * * Thus, it supports up to 128 different multibyte code points max over * the whole set of strings sharing this encoding. * * Source: https://github.com/KEINOS/mb_levenshtein * * @param string $str

UTF-8 string to be converted to extended ASCII.

* @param array $map

Internal-Map of code points to ASCII characters.

* * @return string *

Mapped broken string.

* * @phpstan-param array $map */ private static function to_ascii_remap_intern(string $str, array &$map): string { // find all utf-8 characters $matches = []; if (!\preg_match_all('/[\xC0-\xF7][\x80-\xBF]+/', $str, $matches)) { return $str; // plain ascii string } // update the encoding map with the characters not already met $mapCount = \count($map); foreach ($matches[0] as $mbc) { if (!isset($map[$mbc])) { $map[$mbc] = \chr(128 + $mapCount); ++$mapCount; } } // finally, remap non-ascii characters return \strtr($str, $map); } /** * Get the language from a string. * * e.g.: de_at -> de_at * de_DE -> de * DE_DE -> de * de-de -> de * * @return string */ private static function get_language(string $language) { if ($language === '') { return ''; } if ( \strpos($language, '_') === false && \strpos($language, '-') === false ) { return \strtolower($language); } $language = \str_replace('-', '_', \strtolower($language)); $regex = '/(?[a-z]+)_\g{first}/'; return (string) \preg_replace($regex, '$1', $language); } /** * Get data from "/data/*.php". * * @return array */ private static function getData(string $file) { return include __DIR__ . '/data/' . $file . '.php'; } /** * Get data from "/data/*.php". * * @return array */ private static function getDataIfExists(string $file): array { $file = __DIR__ . '/data/' . $file . '.php'; if (\is_file($file)) { return include $file; } return []; } /** * @return void */ private static function prepareAsciiAndExtrasMaps() { if (self::$ASCII_MAPS_AND_EXTRAS === null) { self::prepareAsciiMaps(); self::prepareAsciiExtras(); self::$ASCII_MAPS_AND_EXTRAS = \array_merge_recursive( self::$ASCII_MAPS ?? [], self::$ASCII_EXTRAS ?? [] ); } } /** * @return void */ private static function prepareAsciiMaps() { if (self::$ASCII_MAPS === null) { self::$ASCII_MAPS = self::getData('ascii_by_languages'); } } /** * @return void */ private static function prepareAsciiExtras() { if (self::$ASCII_EXTRAS === null) { self::$ASCII_EXTRAS = self::getData('ascii_extras_by_languages'); } } }