Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace voku\helper;
6
 
7
/**
8
 * ## 🇷🇺 Русским гражданам
9
 * В Украине сейчас идет война. Силами РФ наносятся удары по гражданской инфраструктуре в [Харькове][1], [Киеве][2], [Чернигове][3], [Сумах][4], [Ирпене][5] и десятках других городов. Гибнут люди - и гражданское население, и военные, в том числе российские призывники, которых бросили воевать. Чтобы лишить собственный народ доступа к информации, правительство РФ запретило называть войну войной, закрыло независимые СМИ и принимает сейчас ряд диктаторских законов. Эти законы призваны заткнуть рот всем, кто против войны. За обычный призыв к миру сейчас можно получить несколько лет тюрьмы.
10
 * Не молчите! Молчание - знак вашего согласия с политикой российского правительства.
11
 *  **Вы можете сделать выбор НЕ МОЛЧАТЬ.**
12
 * ---
13
 * ## 🇺🇸 To people of Russia
14
 * There is a war in Ukraine right now. The forces of the Russian Federation are attacking civilian infrastructure in [Kharkiv][1], [Kyiv][2], [Chernihiv][3], [Sumy][4], [Irpin][5] and dozens of other cities. People are dying – both civilians and military servicemen, including Russian conscripts who were thrown into the fighting. In order to deprive its own people of access to information, the government of the Russian Federation has forbidden calling a war a war, shut down independent media and is passing a number of dictatorial laws. These laws are meant to silence all those who are against war. You can be jailed for multiple years for simply calling for peace.
15
 * Do not be silent! Silence is a sign that you accept the Russian government's policy.
16
 * **You can choose NOT TO BE SILENT.**
17
 * ---
18
 * - [1] https://cloudfront-us-east-2.images.arcpublishing.com/reuters/P7K2MSZDGFMIJPDD7CI2GIROJI.jpg "Kharkiv under attack"
19
 * - [2] https://gdb.voanews.com/01bd0000-0aff-0242-fad0-08d9fc92c5b3_cx0_cy5_cw0_w1023_r1_s.jpg "Kyiv under attack"
20
 * - [3] https://ichef.bbci.co.uk/news/976/cpsprodpb/163DD/production/_123510119_hi074310744.jpg "Chernihiv under attack"
21
 * - [4] https://www.youtube.com/watch?v=8K-bkqKKf2A "Sumy under attack"
22
 * - [5] https://cloudfront-us-east-2.images.arcpublishing.com/reuters/K4MTMLEHTRKGFK3GSKAT4GR3NE.jpg "Irpin under attack"
23
 *
24
 * @psalm-immutable
25
 */
26
final class ASCII
27
{
28
    //
29
    // INFO: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
30
    //
31
 
32
    const UZBEK_LANGUAGE_CODE = 'uz';
33
 
34
    const TURKMEN_LANGUAGE_CODE = 'tk';
35
 
36
    const THAI_LANGUAGE_CODE = 'th';
37
 
38
    const PASHTO_LANGUAGE_CODE = 'ps';
39
 
40
    const ORIYA_LANGUAGE_CODE = 'or';
41
 
42
    const MONGOLIAN_LANGUAGE_CODE = 'mn';
43
 
44
    const KOREAN_LANGUAGE_CODE = 'ko';
45
 
46
    const KIRGHIZ_LANGUAGE_CODE = 'ky';
47
 
48
    const ARMENIAN_LANGUAGE_CODE = 'hy';
49
 
50
    const BENGALI_LANGUAGE_CODE = 'bn';
51
 
52
    const BELARUSIAN_LANGUAGE_CODE = 'be';
53
 
54
    const AMHARIC_LANGUAGE_CODE = 'am';
55
 
56
    const JAPANESE_LANGUAGE_CODE = 'ja';
57
 
58
    const CHINESE_LANGUAGE_CODE = 'zh';
59
 
60
    const DUTCH_LANGUAGE_CODE = 'nl';
61
 
62
    const ITALIAN_LANGUAGE_CODE = 'it';
63
 
64
    const MACEDONIAN_LANGUAGE_CODE = 'mk';
65
 
66
    const PORTUGUESE_LANGUAGE_CODE = 'pt';
67
 
68
    const GREEKLISH_LANGUAGE_CODE = 'el__greeklish';
69
 
70
    const GREEK_LANGUAGE_CODE = 'el';
71
 
72
    const HINDI_LANGUAGE_CODE = 'hi';
73
 
74
    const SWEDISH_LANGUAGE_CODE = 'sv';
75
 
76
    const TURKISH_LANGUAGE_CODE = 'tr';
77
 
78
    const BULGARIAN_LANGUAGE_CODE = 'bg';
79
 
80
    const HUNGARIAN_LANGUAGE_CODE = 'hu';
81
 
82
    const MYANMAR_LANGUAGE_CODE = 'my';
83
 
84
    const CROATIAN_LANGUAGE_CODE = 'hr';
85
 
86
    const FINNISH_LANGUAGE_CODE = 'fi';
87
 
88
    const GEORGIAN_LANGUAGE_CODE = 'ka';
89
 
90
    const RUSSIAN_LANGUAGE_CODE = 'ru';
91
 
92
    const RUSSIAN_PASSPORT_2013_LANGUAGE_CODE = 'ru__passport_2013';
93
 
94
    const RUSSIAN_GOST_2000_B_LANGUAGE_CODE = 'ru__gost_2000_b';
95
 
96
    const UKRAINIAN_LANGUAGE_CODE = 'uk';
97
 
98
    const KAZAKH_LANGUAGE_CODE = 'kk';
99
 
100
    const CZECH_LANGUAGE_CODE = 'cs';
101
 
102
    const DANISH_LANGUAGE_CODE = 'da';
103
 
104
    const POLISH_LANGUAGE_CODE = 'pl';
105
 
106
    const ROMANIAN_LANGUAGE_CODE = 'ro';
107
 
108
    const ESPERANTO_LANGUAGE_CODE = 'eo';
109
 
110
    const ESTONIAN_LANGUAGE_CODE = 'et';
111
 
112
    const LATVIAN_LANGUAGE_CODE = 'lv';
113
 
114
    const LITHUANIAN_LANGUAGE_CODE = 'lt';
115
 
116
    const NORWEGIAN_LANGUAGE_CODE = 'no';
117
 
118
    const VIETNAMESE_LANGUAGE_CODE = 'vi';
119
 
120
    const ARABIC_LANGUAGE_CODE = 'ar';
121
 
122
    const PERSIAN_LANGUAGE_CODE = 'fa';
123
 
124
    const SERBIAN_LANGUAGE_CODE = 'sr';
125
 
126
    const SERBIAN_CYRILLIC_LANGUAGE_CODE = 'sr__cyr';
127
 
128
    const SERBIAN_LATIN_LANGUAGE_CODE = 'sr__lat';
129
 
130
    const AZERBAIJANI_LANGUAGE_CODE = 'az';
131
 
132
    const SLOVAK_LANGUAGE_CODE = 'sk';
133
 
134
    const FRENCH_LANGUAGE_CODE = 'fr';
135
 
136
    const FRENCH_AUSTRIAN_LANGUAGE_CODE = 'fr_at';
137
 
138
    const FRENCH_SWITZERLAND_LANGUAGE_CODE = 'fr_ch';
139
 
140
    const GERMAN_LANGUAGE_CODE = 'de';
141
 
142
    const GERMAN_AUSTRIAN_LANGUAGE_CODE = 'de_at';
143
 
144
    const GERMAN_SWITZERLAND_LANGUAGE_CODE = 'de_ch';
145
 
146
    const ENGLISH_LANGUAGE_CODE = 'en';
147
 
148
    const EXTRA_LATIN_CHARS_LANGUAGE_CODE = 'latin';
149
 
150
    const EXTRA_WHITESPACE_CHARS_LANGUAGE_CODE = ' ';
151
 
152
    const EXTRA_MSWORD_CHARS_LANGUAGE_CODE = 'msword';
153
 
154
    /**
155
     * @var array<string, array<string, string>>|null
156
     */
157
    private static $ASCII_MAPS;
158
 
159
    /**
160
     * @var array<string, array<string, string>>|null
161
     */
162
    private static $ASCII_MAPS_AND_EXTRAS;
163
 
164
    /**
165
     * @var array<string, array<string, string>>|null
166
     */
167
    private static $ASCII_EXTRAS;
168
 
169
    /**
170
     * @var array<string, int>|null
171
     */
172
    private static $ORD;
173
 
174
    /**
175
     * @var array<string, int>|null
176
     */
177
    private static $LANGUAGE_MAX_KEY;
178
 
179
    /**
180
     * url: https://en.wikipedia.org/wiki/Wikipedia:ASCII#ASCII_printable_characters
181
     *
182
     * @var string
183
     */
184
    private static $REGEX_ASCII = "[^\x09\x10\x13\x0A\x0D\x20-\x7E]";
185
 
186
    /**
187
     * bidirectional text chars
188
     *
189
     * url: https://www.w3.org/International/questions/qa-bidi-unicode-controls
190
     *
191
     * @var array<int, string>
192
     */
193
    private static $BIDI_UNI_CODE_CONTROLS_TABLE = [
194
        // LEFT-TO-RIGHT EMBEDDING (use -> dir = "ltr")
195
        8234 => "\xE2\x80\xAA",
196
        // RIGHT-TO-LEFT EMBEDDING (use -> dir = "rtl")
197
        8235 => "\xE2\x80\xAB",
198
        // POP DIRECTIONAL FORMATTING // (use -> </bdo>)
199
        8236 => "\xE2\x80\xAC",
200
        // LEFT-TO-RIGHT OVERRIDE // (use -> <bdo dir = "ltr">)
201
        8237 => "\xE2\x80\xAD",
202
        // RIGHT-TO-LEFT OVERRIDE // (use -> <bdo dir = "rtl">)
203
        8238 => "\xE2\x80\xAE",
204
        // LEFT-TO-RIGHT ISOLATE // (use -> dir = "ltr")
205
        8294 => "\xE2\x81\xA6",
206
        // RIGHT-TO-LEFT ISOLATE // (use -> dir = "rtl")
207
        8295 => "\xE2\x81\xA7",
208
        // FIRST STRONG ISOLATE // (use -> dir = "auto")
209
        8296 => "\xE2\x81\xA8",
210
        // POP DIRECTIONAL ISOLATE
211
        8297 => "\xE2\x81\xA9",
212
    ];
213
 
214
    /**
215
     * Get all languages from the constants "ASCII::.*LANGUAGE_CODE".
216
     *
217
     * @return string[]
218
     *
219
     * @phpstan-return array<string, string>
220
     */
221
    public static function getAllLanguages(): array
222
    {
223
        // init
224
        static $LANGUAGES = [];
225
 
226
        if ($LANGUAGES !== []) {
227
            return $LANGUAGES;
228
        }
229
 
230
        foreach ((new \ReflectionClass(__CLASS__))->getConstants() as $constant => $lang) {
231
            if (\strpos($constant, 'EXTRA') !== false) {
232
                $LANGUAGES[\strtolower($constant)] = $lang;
233
            } else {
234
                $LANGUAGES[\strtolower(\str_replace('_LANGUAGE_CODE', '', $constant))] = $lang;
235
            }
236
        }
237
 
238
        return $LANGUAGES;
239
    }
240
 
241
    /**
242
     * Returns an replacement array for ASCII methods.
243
     *
244
     * EXAMPLE: <code>
245
     * $array = ASCII::charsArray();
246
     * var_dump($array['ru']['б']); // 'b'
247
     * </code>
248
     *
249
     * @psalm-suppress InvalidNullableReturnType - we use the prepare* methods here, so we don't get NULL here
250
     *
251
     * @param bool $replace_extra_symbols [optional] <p>Add some more replacements e.g. "£" with " pound ".</p>
252
     *
253
     * @psalm-pure
254
     *
255
     * @return array
256
     *
257
     * @phpstan-return array<string, array<string , string>>
258
     */
259
    public static function charsArray(bool $replace_extra_symbols = false): array
260
    {
261
        if ($replace_extra_symbols) {
262
            self::prepareAsciiAndExtrasMaps();
263
 
264
            return self::$ASCII_MAPS_AND_EXTRAS ?? [];
265
        }
266
 
267
        self::prepareAsciiMaps();
268
 
269
        return self::$ASCII_MAPS ?? [];
270
    }
271
 
272
    /**
273
     * Returns an replacement array for ASCII methods with a mix of multiple languages.
274
     *
275
     * EXAMPLE: <code>
276
     * $array = ASCII::charsArrayWithMultiLanguageValues();
277
     * var_dump($array['b']); // ['β', 'б', 'ဗ', 'ბ', 'ب']
278
     * </code>
279
     *
280
     * @param bool $replace_extra_symbols [optional] <p>Add some more replacements e.g. "£" with " pound ".</p>
281
     *
282
     * @psalm-pure
283
     *
284
     * @return array
285
     *               <p>An array of replacements.</p>
286
     *
287
     * @phpstan-return array<string, array<int, string>>
288
     */
289
    public static function charsArrayWithMultiLanguageValues(bool $replace_extra_symbols = false): array
290
    {
291
        /** @var array<string, array<string, array<int, string>>> */
292
        static $CHARS_ARRAY = [];
293
        $cacheKey = '' . $replace_extra_symbols;
294
 
295
        if (isset($CHARS_ARRAY[$cacheKey])) {
296
            return $CHARS_ARRAY[$cacheKey];
297
        }
298
 
299
        // init
300
        $return = [];
301
        $language_all_chars = self::charsArrayWithSingleLanguageValues(
302
            $replace_extra_symbols,
303
            false
304
        );
305
 
306
        /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
307
        /** @var array<string, string> $language_all_chars */
308
        $language_all_chars = $language_all_chars;
309
 
310
        /** @noinspection AlterInForeachInspection */
311
        foreach ($language_all_chars as $key => &$value) {
312
            $return[$value][] = $key;
313
        }
314
 
315
        $CHARS_ARRAY[$cacheKey] = $return;
316
 
317
        /** @var array<string, array<int, string>> $return - hack for phpstan */
318
        return $return;
319
    }
320
 
321
    /**
322
     * Returns an replacement array for ASCII methods with one language.
323
     *
324
     * For example, German will map 'ä' to 'ae', while other languages
325
     * will simply return e.g. 'a'.
326
     *
327
     * EXAMPLE: <code>
328
     * $array = ASCII::charsArrayWithOneLanguage('ru');
329
     * $tmpKey = \array_search('yo', $array['replace']);
330
     * echo $array['orig'][$tmpKey]; // 'ё'
331
     * </code>
332
     *
333
     * @psalm-suppress InvalidNullableReturnType - we use the prepare* methods here, so we don't get NULL here
334
     *
335
     * @param string $language              [optional] <p>Language of the source string e.g.: en, de_at, or de-ch.
336
     *                                      (default is 'en') | ASCII::*_LANGUAGE_CODE</p>
337
     * @param bool   $replace_extra_symbols [optional] <p>Add some more replacements e.g. "£" with " pound ".</p>
338
     * @param bool   $asOrigReplaceArray    [optional] <p>TRUE === return {orig: string[], replace: string[]}
339
     *                                      array</p>
340
     *
341
     * @psalm-pure
342
     *
343
     * @return array
344
     *               <p>An array of replacements.</p>
345
     *
346
     * @phpstan-param ASCII::*_LANGUAGE_CODE $language
347
     * @phpstan-return array{orig: string[], replace: string[]}|array<string, string>
348
     */
349
    public static function charsArrayWithOneLanguage(
350
        string $language = self::ENGLISH_LANGUAGE_CODE,
351
        bool $replace_extra_symbols = false,
352
        bool $asOrigReplaceArray = true
353
    ): array {
354
        $language = self::get_language($language);
355
 
356
        // init
357
        /** @var array<string, array<string, array<string, string>|array{orig: string[], replace: string[]}>> */
358
        static $CHARS_ARRAY = [];
359
        $cacheKey = '' . $replace_extra_symbols . '-' . $asOrigReplaceArray;
360
 
361
        // check static cache
362
        if (isset($CHARS_ARRAY[$cacheKey][$language])) {
363
            return $CHARS_ARRAY[$cacheKey][$language];
364
        }
365
 
366
        if ($replace_extra_symbols) {
367
            self::prepareAsciiAndExtrasMaps();
368
 
369
            /** @noinspection DuplicatedCode */
370
            if (isset(self::$ASCII_MAPS_AND_EXTRAS[$language])) {
371
                $tmpArray = self::$ASCII_MAPS_AND_EXTRAS[$language];
372
 
373
                if ($asOrigReplaceArray) {
374
                    $CHARS_ARRAY[$cacheKey][$language] = [
375
                        'orig'    => \array_keys($tmpArray),
376
                        'replace' => \array_values($tmpArray),
377
                    ];
378
                } else {
379
                    $CHARS_ARRAY[$cacheKey][$language] = $tmpArray;
380
                }
381
            } else {
382
                /** @noinspection NestedPositiveIfStatementsInspection */
383
                if ($asOrigReplaceArray) {
384
                    $CHARS_ARRAY[$cacheKey][$language] = [
385
                        'orig'    => [],
386
                        'replace' => [],
387
                    ];
388
                } else {
389
                    $CHARS_ARRAY[$cacheKey][$language] = [];
390
                }
391
            }
392
        } else {
393
            self::prepareAsciiMaps();
394
 
395
            /** @noinspection DuplicatedCode */
396
            if (isset(self::$ASCII_MAPS[$language])) {
397
                $tmpArray = self::$ASCII_MAPS[$language];
398
 
399
                if ($asOrigReplaceArray) {
400
                    $CHARS_ARRAY[$cacheKey][$language] = [
401
                        'orig'    => \array_keys($tmpArray),
402
                        'replace' => \array_values($tmpArray),
403
                    ];
404
                } else {
405
                    $CHARS_ARRAY[$cacheKey][$language] = $tmpArray;
406
                }
407
            } else {
408
                /** @noinspection NestedPositiveIfStatementsInspection */
409
                if ($asOrigReplaceArray) {
410
                    $CHARS_ARRAY[$cacheKey][$language] = [
411
                        'orig'    => [],
412
                        'replace' => [],
413
                    ];
414
                } else {
415
                    $CHARS_ARRAY[$cacheKey][$language] = [];
416
                }
417
            }
418
        }
419
 
420
        return $CHARS_ARRAY[$cacheKey][$language] ?? ['orig' => [], 'replace' => []];
421
    }
422
 
423
    /**
424
     * Returns an replacement array for ASCII methods with multiple languages.
425
     *
426
     * EXAMPLE: <code>
427
     * $array = ASCII::charsArrayWithSingleLanguageValues();
428
     * $tmpKey = \array_search('hnaik', $array['replace']);
429
     * echo $array['orig'][$tmpKey]; // '၌'
430
     * </code>
431
     *
432
     * @param bool $replace_extra_symbols [optional] <p>Add some more replacements e.g. "£" with " pound ".</p>
433
     * @param bool $asOrigReplaceArray    [optional] <p>TRUE === return {orig: string[], replace: string[]}
434
     *                                    array</p>
435
     *
436
     * @psalm-pure
437
     *
438
     * @return array
439
     *               <p>An array of replacements.</p>
440
     *
441
     * @phpstan-return array{orig: string[], replace: string[]}|array<string, string>
442
     */
443
    public static function charsArrayWithSingleLanguageValues(
444
        bool $replace_extra_symbols = false,
445
        bool $asOrigReplaceArray = true
446
    ): array {
447
        // init
448
        /** @var array<string, array<string, string>|array{orig: string[], replace: string[]}> */
449
        static $CHARS_ARRAY = [];
450
        $cacheKey = '' . $replace_extra_symbols . '-' . $asOrigReplaceArray;
451
 
452
        if (isset($CHARS_ARRAY[$cacheKey])) {
453
            return $CHARS_ARRAY[$cacheKey];
454
        }
455
 
456
        if ($replace_extra_symbols) {
457
            self::prepareAsciiAndExtrasMaps();
458
 
459
            /** @noinspection AlterInForeachInspection */
460
            /** @psalm-suppress PossiblyNullIterator - we use the prepare* methods here, so we don't get NULL here */
461
            foreach (self::$ASCII_MAPS_AND_EXTRAS ?? [] as &$map) {
462
                $CHARS_ARRAY[$cacheKey][] = $map;
463
            }
464
        } else {
465
            self::prepareAsciiMaps();
466
 
467
            /** @noinspection AlterInForeachInspection */
468
            /** @psalm-suppress PossiblyNullIterator - we use the prepare* methods here, so we don't get NULL here */
469
            foreach (self::$ASCII_MAPS ?? [] as &$map) {
470
                $CHARS_ARRAY[$cacheKey][] = $map;
471
            }
472
        }
473
 
474
        /** @phpstan-ignore-next-line - ... error? */
475
        $CHARS_ARRAY[$cacheKey] = \array_merge([], ...$CHARS_ARRAY[$cacheKey]);
476
 
477
        if ($asOrigReplaceArray) {
478
            $CHARS_ARRAY[$cacheKey] = [
479
                'orig'    => \array_keys($CHARS_ARRAY[$cacheKey]),
480
                'replace' => \array_values($CHARS_ARRAY[$cacheKey]),
481
            ];
482
        }
483
 
484
        return $CHARS_ARRAY[$cacheKey];
485
    }
486
 
487
    /**
488
     * Accepts a string and removes all non-UTF-8 characters from it + extras if needed.
489
     *
490
     * @param string $str                         <p>The string to be sanitized.</p>
491
     * @param bool   $normalize_whitespace        [optional] <p>Set to true, if you need to normalize the
492
     *                                            whitespace.</p>
493
     * @param bool   $normalize_msword            [optional] <p>Set to true, if you need to normalize MS Word chars
494
     *                                            e.g.: "…"
495
     *                                            => "..."</p>
496
     * @param bool   $keep_non_breaking_space     [optional] <p>Set to true, to keep non-breaking-spaces, in
497
     *                                            combination with
498
     *                                            $normalize_whitespace</p>
499
     * @param bool   $remove_invisible_characters [optional] <p>Set to false, if you not want to remove invisible
500
     *                                            characters e.g.: "\0"</p>
501
     *
502
     * @psalm-pure
503
     *
504
     * @return string
505
     *                <p>A clean UTF-8 string.</p>
506
     */
507
    public static function clean(
508
        string $str,
509
        bool $normalize_whitespace = true,
510
        bool $keep_non_breaking_space = false,
511
        bool $normalize_msword = true,
512
        bool $remove_invisible_characters = true
513
    ): string {
514
        // http://stackoverflow.com/questions/1401317/remove-non-utf8-characters-from-string
515
        // caused connection reset problem on larger strings
516
 
517
        $regex = '/
518
          (
519
            (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
520
            |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
521
            |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
522
            |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3
523
            ){1,100}                      # ...one or more times
524
          )
525
        | ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
526
        | ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
527
        /x';
528
        $str = (string) \preg_replace($regex, '$1', $str);
529
 
530
        if ($normalize_whitespace) {
531
            $str = self::normalize_whitespace($str, $keep_non_breaking_space);
532
        }
533
 
534
        if ($normalize_msword) {
535
            $str = self::normalize_msword($str);
536
        }
537
 
538
        if ($remove_invisible_characters) {
539
            $str = self::remove_invisible_characters($str);
540
        }
541
 
542
        return $str;
543
    }
544
 
545
    /**
546
     * Checks if a string is 7 bit ASCII.
547
     *
548
     * EXAMPLE: <code>
549
     * ASCII::is_ascii('白'); // false
550
     * </code>
551
     *
552
     * @param string $str <p>The string to check.</p>
553
     *
554
     * @psalm-pure
555
     *
556
     * @return bool
557
     *              <p>
558
     *              <strong>true</strong> if it is ASCII<br>
559
     *              <strong>false</strong> otherwise
560
     *              </p>
561
     */
562
    public static function is_ascii(string $str): bool
563
    {
564
        if ($str === '') {
565
            return true;
566
        }
567
 
568
        return !\preg_match('/' . self::$REGEX_ASCII . '/', $str);
569
    }
570
 
571
    /**
572
     * Returns a string with smart quotes, ellipsis characters, and dashes from
573
     * Windows-1252 (commonly used in Word documents) replaced by their ASCII
574
     * equivalents.
575
     *
576
     * EXAMPLE: <code>
577
     * ASCII::normalize_msword('„Abcdef…”'); // '"Abcdef..."'
578
     * </code>
579
     *
580
     * @param string $str <p>The string to be normalized.</p>
581
     *
582
     * @psalm-pure
583
     *
584
     * @return string
585
     *                <p>A string with normalized characters for commonly used chars in Word documents.</p>
586
     */
587
    public static function normalize_msword(string $str): string
588
    {
589
        if ($str === '') {
590
            return '';
591
        }
592
 
593
        /** @var array{orig: string[], replace: string[]} */
594
        static $MSWORD_CACHE = ['orig' => [], 'replace' => []];
595
 
596
        if (empty($MSWORD_CACHE['orig'])) {
597
            self::prepareAsciiMaps();
598
 
599
            /** @var array<string, string> */
600
            $map = self::$ASCII_MAPS[self::EXTRA_MSWORD_CHARS_LANGUAGE_CODE] ?? [];
601
 
602
            $MSWORD_CACHE = [
603
                'orig'    => \array_keys($map),
604
                'replace' => \array_values($map),
605
            ];
606
        }
607
 
608
        return \str_replace($MSWORD_CACHE['orig'], $MSWORD_CACHE['replace'], $str);
609
    }
610
 
611
    /**
612
     * Normalize the whitespace.
613
     *
614
     * EXAMPLE: <code>
615
     * ASCII::normalize_whitespace("abc-\xc2\xa0-öäü-\xe2\x80\xaf-\xE2\x80\xAC", true); // "abc-\xc2\xa0-öäü- -"
616
     * </code>
617
     *
618
     * @param string $str                          <p>The string to be normalized.</p>
619
     * @param bool   $keepNonBreakingSpace         [optional] <p>Set to true, to keep non-breaking-spaces.</p>
620
     * @param bool   $keepBidiUnicodeControls      [optional] <p>Set to true, to keep non-printable (for the web)
621
     *                                             bidirectional text chars.</p>
622
     * @param bool   $normalize_control_characters [optional] <p>Set to true, to convert e.g. LINE-, PARAGRAPH-SEPARATOR with "\n" and LINE TABULATION with "\t".</p>
623
     *
624
     * @psalm-pure
625
     *
626
     * @return string
627
     *                <p>A string with normalized whitespace.</p>
628
     */
629
    public static function normalize_whitespace(
630
        string $str,
631
        bool $keepNonBreakingSpace = false,
632
        bool $keepBidiUnicodeControls = false,
633
        bool $normalize_control_characters = false
634
    ): string {
635
        if ($str === '') {
636
            return '';
637
        }
638
 
639
        /** @var array<int,array<string,string>> */
640
        static $WHITESPACE_CACHE = [];
641
        $cacheKey = (int) $keepNonBreakingSpace;
642
 
643
        if ($normalize_control_characters) {
644
            $str = \str_replace(
645
                [
646
                    "\x0d\x0c",     // 'END OF LINE'
647
                    "\xe2\x80\xa8", // 'LINE SEPARATOR'
648
                    "\xe2\x80\xa9", // 'PARAGRAPH SEPARATOR'
649
                    "\x0c",         // 'FORM FEED' // "\f"
650
                    "\x0b",         // 'VERTICAL TAB' // "\v"
651
                ],
652
                [
653
                    "\n",
654
                    "\n",
655
                    "\n",
656
                    "\n",
657
                    "\t",
658
                ],
659
                $str
660
            );
661
        }
662
 
663
        if (!isset($WHITESPACE_CACHE[$cacheKey])) {
664
            self::prepareAsciiMaps();
665
 
666
            $WHITESPACE_CACHE[$cacheKey] = self::$ASCII_MAPS[self::EXTRA_WHITESPACE_CHARS_LANGUAGE_CODE] ?? [];
667
 
668
            if ($keepNonBreakingSpace) {
669
                unset($WHITESPACE_CACHE[$cacheKey]["\xc2\xa0"]);
670
            }
671
 
672
            $WHITESPACE_CACHE[$cacheKey] = array_keys($WHITESPACE_CACHE[$cacheKey]);
673
        }
674
 
675
        if (!$keepBidiUnicodeControls) {
676
            /** @var array<int,string>|null */
677
            static $BIDI_UNICODE_CONTROLS_CACHE = null;
678
 
679
            if ($BIDI_UNICODE_CONTROLS_CACHE === null) {
680
                $BIDI_UNICODE_CONTROLS_CACHE = self::$BIDI_UNI_CODE_CONTROLS_TABLE;
681
            }
682
 
683
            $str = \str_replace($BIDI_UNICODE_CONTROLS_CACHE, '', $str);
684
        }
685
 
686
        return \str_replace($WHITESPACE_CACHE[$cacheKey], ' ', $str);
687
    }
688
 
689
    /**
690
     * Remove invisible characters from a string.
691
     *
692
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
693
     *
694
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
695
     *
696
     * @param string $str
697
     * @param bool   $url_encoded
698
     * @param string $replacement
699
     * @param bool   $keep_basic_control_characters
700
     *
701
     * @psalm-pure
702
     *
703
     * @return string
704
     */
705
    public static function remove_invisible_characters(
706
        string $str,
707
        bool $url_encoded = false,
708
        string $replacement = '',
709
        bool $keep_basic_control_characters = true
710
    ): string {
711
        // init
712
        $non_displayables = [];
713
 
714
        // every control character except:
715
        // - newline (dec 10),
716
        // - carriage return (dec 13),
717
        // - horizontal tab (dec 09)
718
        if ($url_encoded) {
719
            $non_displayables[] = '/%0[0-8bcefBCEF]/'; // url encoded 00-08, 11, 12, 14, 15
720
            $non_displayables[] = '/%1[0-9a-fA-F]/'; // url encoded 16-31
721
        }
722
 
723
        if ($keep_basic_control_characters) {
724
            $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127
725
        } else {
726
            $str = self::normalize_whitespace($str, false, false, true);
727
            $non_displayables[] = '/[^\P{C}\s]/u';
728
        }
729
 
730
        do {
731
            $str = (string) \preg_replace($non_displayables, $replacement, $str, -1, $count);
732
        } while ($count !== 0);
733
 
734
        return $str;
735
    }
736
 
737
    /**
738
     *  WARNING: This method will return broken characters and is only for special cases.
739
     *
740
     * Convert two UTF-8 encoded string to a single-byte strings suitable for
741
     * functions that need the same string length after the conversion.
742
     *
743
     * The function simply uses (and updates) a tailored dynamic encoding
744
     * (in/out map parameter) where non-ascii characters are remapped to
745
     * the range [128-255] in order of appearance.
746
     *
747
     * @param string $str1
748
     * @param string $str2
749
     *
750
     * @return string[]
751
     *
752
     * @phpstan-return array{0: string, 1: string}
753
     */
754
    public static function to_ascii_remap(string $str1, string $str2): array
755
    {
756
        $charMap = [];
757
        $str1 = self::to_ascii_remap_intern($str1, $charMap);
758
        $str2 = self::to_ascii_remap_intern($str2, $charMap);
759
 
760
        return [$str1, $str2];
761
    }
762
 
763
    /**
764
     * Returns an ASCII version of the string. A set of non-ASCII characters are
765
     * replaced with their closest ASCII counterparts, and the rest are removed
766
     * by default. The language or locale of the source string can be supplied
767
     * for language-specific transliteration in any of the following formats:
768
     * en, en_GB, or en-GB. For example, passing "de" results in "äöü" mapping
769
     * to "aeoeue" rather than "aou" as in other languages.
770
     *
771
     * EXAMPLE: <code>
772
     * ASCII::to_ascii('�Düsseldorf�', 'en'); // Dusseldorf
773
     * </code>
774
     *
775
     * @param string    $str                       <p>The input string.</p>
776
     * @param string    $language                  [optional] <p>Language of the source string.
777
     *                                             (default is 'en') | ASCII::*_LANGUAGE_CODE</p>
778
     * @param bool      $remove_unsupported_chars  [optional] <p>Whether or not to remove the
779
     *                                             unsupported characters.</p>
780
     * @param bool      $replace_extra_symbols     [optional]  <p>Add some more replacements e.g. "£" with " pound
781
     *                                             ".</p>
782
     * @param bool      $use_transliterate         [optional]  <p>Use ASCII::to_transliterate() for unknown chars.</p>
783
     * @param bool|null $replace_single_chars_only [optional]  <p>Single char replacement is better for the
784
     *                                             performance, but some languages need to replace more then one char
785
     *                                             at the same time. | NULL === auto-setting, depended on the
786
     *                                             language</p>
787
     *
788
     * @psalm-pure
789
     *
790
     * @return string
791
     *                <p>A string that contains only ASCII characters.</p>
792
     *
793
     * @phpstan-param ASCII::*_LANGUAGE_CODE $language
794
     */
795
    public static function to_ascii(
796
        string $str,
797
        string $language = self::ENGLISH_LANGUAGE_CODE,
798
        bool $remove_unsupported_chars = true,
799
        bool $replace_extra_symbols = false,
800
        bool $use_transliterate = false,
801
        bool $replace_single_chars_only = null
802
    ): string {
803
        if ($str === '') {
804
            return '';
805
        }
806
 
807
        /** @phpstan-var ASCII::*_LANGUAGE_CODE - hack for phpstan */
808
        $language = self::get_language($language);
809
 
810
        static $EXTRA_SYMBOLS_CACHE = null;
811
 
812
        /** @var array<string,array<string,string>> */
813
        static $REPLACE_HELPER_CACHE = [];
814
        $cacheKey = $language . '-' . $replace_extra_symbols;
815
 
816
        if (!isset($REPLACE_HELPER_CACHE[$cacheKey])) {
817
            $langAll = self::charsArrayWithSingleLanguageValues($replace_extra_symbols, false);
818
 
819
            $langSpecific = self::charsArrayWithOneLanguage($language, $replace_extra_symbols, false);
820
 
821
            if ($langSpecific === []) {
822
                $REPLACE_HELPER_CACHE[$cacheKey] = $langAll;
823
            } else {
824
                $REPLACE_HELPER_CACHE[$cacheKey] = \array_merge([], $langAll, $langSpecific);
825
            }
826
        }
827
 
828
        if (
829
            $replace_extra_symbols
830
            &&
831
            $EXTRA_SYMBOLS_CACHE === null
832
        ) {
833
            $EXTRA_SYMBOLS_CACHE = [];
834
            foreach (self::$ASCII_EXTRAS ?? [] as $extrasDataTmp) {
835
                foreach ($extrasDataTmp as $extrasDataKeyTmp => $extrasDataValueTmp) {
836
                    $EXTRA_SYMBOLS_CACHE[$extrasDataKeyTmp] = $extrasDataKeyTmp;
837
                }
838
            }
839
            $EXTRA_SYMBOLS_CACHE = \implode('', $EXTRA_SYMBOLS_CACHE);
840
        }
841
 
842
        $charDone = [];
843
        if (\preg_match_all('/' . self::$REGEX_ASCII . ($replace_extra_symbols ? '|[' . $EXTRA_SYMBOLS_CACHE . ']' : '') . '/u', $str, $matches)) {
844
            if (!$replace_single_chars_only) {
845
                if (self::$LANGUAGE_MAX_KEY === null) {
846
                    self::$LANGUAGE_MAX_KEY = self::getData('ascii_language_max_key');
847
                }
848
 
849
                $maxKeyLength = self::$LANGUAGE_MAX_KEY[$language] ?? 0;
850
 
851
                if ($maxKeyLength >= 5) {
852
                    foreach ($matches[0] as $keyTmp => $char) {
853
                        if (isset($matches[0][$keyTmp + 4])) {
854
                            $fiveChars = $matches[0][$keyTmp + 0] . $matches[0][$keyTmp + 1] . $matches[0][$keyTmp + 2] . $matches[0][$keyTmp + 3] . $matches[0][$keyTmp + 4];
855
                        } else {
856
                            $fiveChars = null;
857
                        }
858
                        if (
859
                            $fiveChars
860
                            &&
861
                            !isset($charDone[$fiveChars])
862
                            &&
863
                            isset($REPLACE_HELPER_CACHE[$cacheKey][$fiveChars])
864
                            &&
865
                            \strpos($str, $fiveChars) !== false
866
                        ) {
867
                            // DEBUG
868
                            //\var_dump($str, $fiveChars, $REPLACE_HELPER_CACHE[$cacheKey][$fiveChars]);
869
 
870
                            $charDone[$fiveChars] = true;
871
                            $str = \str_replace($fiveChars, $REPLACE_HELPER_CACHE[$cacheKey][$fiveChars], $str);
872
 
873
                            // DEBUG
874
                            //\var_dump($str, "\n");
875
                        }
876
                    }
877
                }
878
 
879
                if ($maxKeyLength >= 4) {
880
                    foreach ($matches[0] as $keyTmp => $char) {
881
                        if (isset($matches[0][$keyTmp + 3])) {
882
                            $fourChars = $matches[0][$keyTmp + 0] . $matches[0][$keyTmp + 1] . $matches[0][$keyTmp + 2] . $matches[0][$keyTmp + 3];
883
                        } else {
884
                            $fourChars = null;
885
                        }
886
                        if (
887
                            $fourChars
888
                            &&
889
                            !isset($charDone[$fourChars])
890
                            &&
891
                            isset($REPLACE_HELPER_CACHE[$cacheKey][$fourChars])
892
                            &&
893
                            \strpos($str, $fourChars) !== false
894
                        ) {
895
                            // DEBUG
896
                            //\var_dump($str, $fourChars, $REPLACE_HELPER_CACHE[$cacheKey][$fourChars]);
897
 
898
                            $charDone[$fourChars] = true;
899
                            $str = \str_replace($fourChars, $REPLACE_HELPER_CACHE[$cacheKey][$fourChars], $str);
900
 
901
                            // DEBUG
902
                            //\var_dump($str, "\n");
903
                        }
904
                    }
905
                }
906
 
907
                foreach ($matches[0] as $keyTmp => $char) {
908
                    if (isset($matches[0][$keyTmp + 2])) {
909
                        $threeChars = $matches[0][$keyTmp + 0] . $matches[0][$keyTmp + 1] . $matches[0][$keyTmp + 2];
910
                    } else {
911
                        $threeChars = null;
912
                    }
913
                    if (
914
                        $threeChars
915
                        &&
916
                        !isset($charDone[$threeChars])
917
                        &&
918
                        isset($REPLACE_HELPER_CACHE[$cacheKey][$threeChars])
919
                        &&
920
                        \strpos($str, $threeChars) !== false
921
                    ) {
922
                        // DEBUG
923
                        //\var_dump($str, $threeChars, $REPLACE_HELPER_CACHE[$cacheKey][$threeChars]);
924
 
925
                        $charDone[$threeChars] = true;
926
                        $str = \str_replace($threeChars, $REPLACE_HELPER_CACHE[$cacheKey][$threeChars], $str);
927
 
928
                        // DEBUG
929
                        //\var_dump($str, "\n");
930
                    }
931
                }
932
 
933
                foreach ($matches[0] as $keyTmp => $char) {
934
                    if (isset($matches[0][$keyTmp + 1])) {
935
                        $twoChars = $matches[0][$keyTmp + 0] . $matches[0][$keyTmp + 1];
936
                    } else {
937
                        $twoChars = null;
938
                    }
939
                    if (
940
                        $twoChars
941
                        &&
942
                        !isset($charDone[$twoChars])
943
                        &&
944
                        isset($REPLACE_HELPER_CACHE[$cacheKey][$twoChars])
945
                        &&
946
                        \strpos($str, $twoChars) !== false
947
                    ) {
948
                        // DEBUG
949
                        //\var_dump($str, $twoChars, $REPLACE_HELPER_CACHE[$cacheKey][$twoChars]);
950
 
951
                        $charDone[$twoChars] = true;
952
                        $str = \str_replace($twoChars, $REPLACE_HELPER_CACHE[$cacheKey][$twoChars], $str);
953
 
954
                        // DEBUG
955
                        //\var_dump($str, "\n");
956
                    }
957
                }
958
            }
959
 
960
            foreach ($matches[0] as $char) {
961
                if (
962
                    !isset($charDone[$char])
963
                    &&
964
                    isset($REPLACE_HELPER_CACHE[$cacheKey][$char])
965
                    &&
966
                    \strpos($str, $char) !== false
967
                ) {
968
                    // DEBUG
969
                    //\var_dump($str, $char, $REPLACE_HELPER_CACHE[$cacheKey][$char]);
970
 
971
                    $charDone[$char] = true;
972
                    $str = \str_replace($char, $REPLACE_HELPER_CACHE[$cacheKey][$char], $str);
973
 
974
                    // DEBUG
975
                    //\var_dump($str, "\n");
976
                }
977
            }
978
        }
979
 
980
        /** @psalm-suppress PossiblyNullOperand - we use the prepare* methods here, so we don't get NULL here */
981
        if (!isset(self::$ASCII_MAPS[$language])) {
982
            $use_transliterate = true;
983
        }
984
 
985
        if ($use_transliterate) {
986
            $str = self::to_transliterate($str, null, false);
987
        }
988
 
989
        if ($remove_unsupported_chars) {
990
            $str = (string) \str_replace(["\n\r", "\n", "\r", "\t"], ' ', $str);
991
            $str = (string) \preg_replace('/' . self::$REGEX_ASCII . '/', '', $str);
992
        }
993
 
994
        return $str;
995
    }
996
 
997
    /**
998
     * Convert given string to safe filename (and keep string case).
999
     *
1000
     * EXAMPLE: <code>
1001
     * ASCII::to_filename('שדגשדג.png', true)); // 'shdgshdg.png'
1002
     * </code>
1003
     *
1004
     * @param string $str
1005
     * @param bool   $use_transliterate <p>ASCII::to_transliterate() is used by default - unsafe characters are
1006
     *                                  simply replaced with hyphen otherwise.</p>
1007
     * @param string $fallback_char
1008
     *
1009
     * @psalm-pure
1010
     *
1011
     * @return string
1012
     *                <p>A string that contains only safe characters for a filename.</p>
1013
     */
1014
    public static function to_filename(
1015
        string $str,
1016
        bool $use_transliterate = true,
1017
        string $fallback_char = '-'
1018
    ): string {
1019
        if ($use_transliterate) {
1020
            $str = self::to_transliterate($str, $fallback_char);
1021
        }
1022
 
1023
        $fallback_char_escaped = \preg_quote($fallback_char, '/');
1024
 
1025
        $str = (string) \preg_replace(
1026
            [
1027
                '/[^' . $fallback_char_escaped . '.\\-a-zA-Z\d\\s]/', // 1) remove un-needed chars
1028
                '/\s+/u',                                             // 2) convert spaces to $fallback_char
1029
                '/[' . $fallback_char_escaped . ']+/u',               // 3) remove double $fallback_char's
1030
            ],
1031
            [
1032
                '',
1033
                $fallback_char,
1034
                $fallback_char,
1035
            ],
1036
            $str
1037
        );
1038
 
1039
        return \trim($str, $fallback_char);
1040
    }
1041
 
1042
    /**
1043
     * Converts the string into an URL slug. This includes replacing non-ASCII
1044
     * characters with their closest ASCII equivalents, removing remaining
1045
     * non-ASCII and non-alphanumeric characters, and replacing whitespace with
1046
     * $separator. The separator defaults to a single dash, and the string
1047
     * is also converted to lowercase. The language of the source string can
1048
     * also be supplied for language-specific transliteration.
1049
     *
1050
     * @param string                $str
1051
     * @param string                $separator             [optional] <p>The string used to replace whitespace.</p>
1052
     * @param string                $language              [optional] <p>Language of the source string.
1053
     *                                                     (default is 'en') | ASCII::*_LANGUAGE_CODE</p>
1054
     * @param array<string, string> $replacements          [optional] <p>A map of replaceable strings.</p>
1055
     * @param bool                  $replace_extra_symbols [optional]  <p>Add some more replacements e.g. "£" with "
1056
     *                                                     pound ".</p>
1057
     * @param bool                  $use_str_to_lower      [optional] <p>Use "string to lower" for the input.</p>
1058
     * @param bool                  $use_transliterate     [optional]  <p>Use ASCII::to_transliterate() for unknown
1059
     *                                                     chars.</p>
1060
     * @psalm-pure
1061
     *
1062
     * @return string
1063
     *                <p>A string that has been converted to an URL slug.</p>
1064
     *
1065
     * @phpstan-param ASCII::*_LANGUAGE_CODE $language
1066
     */
1067
    public static function to_slugify(
1068
        string $str,
1069
        string $separator = '-',
1070
        string $language = self::ENGLISH_LANGUAGE_CODE,
1071
        array $replacements = [],
1072
        bool $replace_extra_symbols = false,
1073
        bool $use_str_to_lower = true,
1074
        bool $use_transliterate = false
1075
    ): string {
1076
        if ($str === '') {
1077
            return '';
1078
        }
1079
 
1080
        foreach ($replacements as $from => $to) {
1081
            $str = \str_replace($from, $to, $str);
1082
        }
1083
 
1084
        $str = self::to_ascii(
1085
            $str,
1086
            $language,
1087
            false,
1088
            $replace_extra_symbols,
1089
            $use_transliterate
1090
        );
1091
 
1092
        $str = \str_replace('@', $separator, $str);
1093
 
1094
        $str = (string) \preg_replace(
1095
            '/[^a-zA-Z\\d\\s\\-_' . \preg_quote($separator, '/') . ']/',
1096
            '',
1097
            $str
1098
        );
1099
 
1100
        if ($use_str_to_lower) {
1101
            $str = \strtolower($str);
1102
        }
1103
 
1104
        $str = (string) \preg_replace('/^[\'\\s]+|[\'\\s]+$/', '', $str);
1105
        $str = (string) \preg_replace('/\\B([A-Z])/', '-\1', $str);
1106
        $str = (string) \preg_replace('/[\\-_\\s]+/', $separator, $str);
1107
 
1108
        $l = \strlen($separator);
1109
        if ($l && \strpos($str, $separator) === 0) {
1110
            $str = (string) \substr($str, $l);
1111
        }
1112
 
1113
        if (\substr($str, -$l) === $separator) {
1114
            $str = (string) \substr($str, 0, \strlen($str) - $l);
1115
        }
1116
 
1117
        return $str;
1118
    }
1119
 
1120
    /**
1121
     * Returns an ASCII version of the string. A set of non-ASCII characters are
1122
     * replaced with their closest ASCII counterparts, and the rest are removed
1123
     * unless instructed otherwise.
1124
     *
1125
     * EXAMPLE: <code>
1126
     * ASCII::to_transliterate('déjà σσς iıii'); // 'deja sss iiii'
1127
     * </code>
1128
     *
1129
     * @param string      $str     <p>The input string.</p>
1130
     * @param string|null $unknown [optional] <p>Character use if character unknown. (default is '?')
1131
     *                             But you can also use NULL to keep the unknown chars.</p>
1132
     * @param bool        $strict  [optional] <p>Use "transliterator_transliterate()" from PHP-Intl
1133
     *
1134
     * @psalm-pure
1135
     *
1136
     * @return string
1137
     *                <p>A String that contains only ASCII characters.</p>
1138
     *
1139
     * @noinspection ParameterDefaultValueIsNotNullInspection
1140
     */
1141
    public static function to_transliterate(
1142
        string $str,
1143
        $unknown = '?',
1144
        bool $strict = false
1145
    ): string {
1146
        /** @var array<int,string>|null */
1147
        static $UTF8_TO_TRANSLIT = null;
1148
 
1149
        /** null|\Transliterator */
1150
        static $TRANSLITERATOR = null;
1151
 
1152
        /** @var bool|null */
1153
        static $SUPPORT_INTL = null;
1154
 
1155
        if ($str === '') {
1156
            return '';
1157
        }
1158
 
1159
        if ($SUPPORT_INTL === null) {
1160
            $SUPPORT_INTL = \extension_loaded('intl');
1161
        }
1162
 
1163
        // check if we only have ASCII, first (better performance)
1164
        $str_tmp = $str;
1165
        if (self::is_ascii($str)) {
1166
            return $str;
1167
        }
1168
 
1169
        $str = self::clean($str);
1170
 
1171
        // check again, if we only have ASCII, now ...
1172
        if (
1173
            $str_tmp !== $str
1174
            &&
1175
            self::is_ascii($str)
1176
        ) {
1177
            return $str;
1178
        }
1179
 
1180
        if (
1181
            $strict
1182
            &&
1183
            $SUPPORT_INTL === true
1184
        ) {
1185
            if (!isset($TRANSLITERATOR)) {
1186
                // INFO: see "*-Latin" rules via "transliterator_list_ids()"
1187
                /** @var \Transliterator */
1188
                $TRANSLITERATOR = \transliterator_create('NFKC; [:Nonspacing Mark:] Remove; NFKC; Any-Latin; Latin-ASCII;');
1189
            }
1190
 
1191
            // INFO: https://unicode.org/cldr/utility/character.jsp
1192
            $str_tmp = \transliterator_transliterate($TRANSLITERATOR, $str);
1193
 
1194
            if ($str_tmp !== false) {
1195
 
1196
                // check again, if we only have ASCII, now ...
1197
                if (
1198
                    $str_tmp !== $str
1199
                    &&
1200
                    self::is_ascii($str_tmp)
1201
                ) {
1202
                    return $str_tmp;
1203
                }
1204
 
1205
                $str = $str_tmp;
1206
            }
1207
        }
1208
 
1209
        if (self::$ORD === null) {
1210
            self::$ORD = self::getData('ascii_ord');
1211
        }
1212
 
1213
        \preg_match_all('/.|[^\x00]$/us', $str, $array_tmp);
1214
        $chars = $array_tmp[0];
1215
        $ord = null;
1216
        $str_tmp = '';
1217
        foreach ($chars as &$c) {
1218
            $ordC0 = self::$ORD[$c[0]];
1219
 
1220
            if ($ordC0 >= 0 && $ordC0 <= 127) {
1221
                $str_tmp .= $c;
1222
 
1223
                continue;
1224
            }
1225
 
1226
            $ordC1 = self::$ORD[$c[1]];
1227
 
1228
            // ASCII - next please
1229
            if ($ordC0 >= 192 && $ordC0 <= 223) {
1230
                $ord = ($ordC0 - 192) * 64 + ($ordC1 - 128);
1231
            }
1232
 
1233
            if ($ordC0 >= 224) {
1234
                $ordC2 = self::$ORD[$c[2]];
1235
 
1236
                if ($ordC0 <= 239) {
1237
                    $ord = ($ordC0 - 224) * 4096 + ($ordC1 - 128) * 64 + ($ordC2 - 128);
1238
                }
1239
 
1240
                if ($ordC0 >= 240) {
1241
                    $ordC3 = self::$ORD[$c[3]];
1242
 
1243
                    if ($ordC0 <= 247) {
1244
                        $ord = ($ordC0 - 240) * 262144 + ($ordC1 - 128) * 4096 + ($ordC2 - 128) * 64 + ($ordC3 - 128);
1245
                    }
1246
 
1247
                    // We only process valid UTF-8 chars (<= 4 byte), so we don't need this code here ...
1248
                    /*
1249
                    if ($ordC0 >= 248) {
1250
                        $ordC4 = self::$ORD[$c[4]];
1251
 
1252
                        if ($ordC0 <= 251) {
1253
                            $ord = ($ordC0 - 248) * 16777216 + ($ordC1 - 128) * 262144 + ($ordC2 - 128) * 4096 + ($ordC3 - 128) * 64 + ($ordC4 - 128);
1254
                        }
1255
 
1256
                        if ($ordC0 >= 252) {
1257
                            $ordC5 = self::$ORD[$c[5]];
1258
 
1259
                            if ($ordC0 <= 253) {
1260
                                $ord = ($ordC0 - 252) * 1073741824 + ($ordC1 - 128) * 16777216 + ($ordC2 - 128) * 262144 + ($ordC3 - 128) * 4096 + ($ordC4 - 128) * 64 + ($ordC5 - 128);
1261
                            }
1262
                        }
1263
                    }
1264
                     */
1265
                }
1266
            }
1267
 
1268
            if (
1269
                $ordC0 === 254
1270
                ||
1271
                $ordC0 === 255
1272
                ||
1273
                $ord === null
1274
            ) {
1275
                $str_tmp .= $unknown ?? $c;
1276
 
1277
                continue;
1278
            }
1279
 
1280
            $bank = $ord >> 8;
1281
            if (!isset($UTF8_TO_TRANSLIT[$bank])) {
1282
                $UTF8_TO_TRANSLIT[$bank] = self::getDataIfExists(\sprintf('x%03x', $bank));
1283
            }
1284
 
1285
            $new_char = $ord & 255;
1286
 
1287
            if (isset($UTF8_TO_TRANSLIT[$bank][$new_char])) {
1288
 
1289
                // keep for debugging
1290
                /*
1291
                echo "file: " . sprintf('x%02x', $bank) . "\n";
1292
                echo "char: " . $c . "\n";
1293
                echo "ord: " . $ord . "\n";
1294
                echo "new_char: " . $new_char . "\n";
1295
                echo "new_char: " . mb_chr($new_char) . "\n";
1296
                echo "ascii: " . $UTF8_TO_TRANSLIT[$bank][$new_char] . "\n";
1297
                echo "bank:" . $bank . "\n\n";
1298
                 */
1299
 
1300
                $new_char = $UTF8_TO_TRANSLIT[$bank][$new_char];
1301
 
1302
                /** @noinspection MissingOrEmptyGroupStatementInspection */
1303
                /** @noinspection PhpStatementHasEmptyBodyInspection */
1304
                if ($unknown === null && $new_char === '') {
1305
                    // nothing
1306
                } elseif (
1307
                    $new_char === '[?]'
1308
                    ||
1309
                    $new_char === '[?] '
1310
                ) {
1311
                    $c = $unknown ?? $c;
1312
                } else {
1313
                    $c = $new_char;
1314
                }
1315
            } else {
1316
 
1317
                // keep for debugging missing chars
1318
                /*
1319
                echo "file: " . sprintf('x%02x', $bank) . "\n";
1320
                echo "char: " . $c . "\n";
1321
                echo "ord: " . $ord . "\n";
1322
                echo "new_char: " . $new_char . "\n";
1323
                echo "new_char: " . mb_chr($new_char) . "\n";
1324
                echo "bank:" . $bank . "\n\n";
1325
                 */
1326
 
1327
                $c = $unknown ?? $c;
1328
            }
1329
 
1330
            $str_tmp .= $c;
1331
        }
1332
 
1333
        return $str_tmp;
1334
    }
1335
 
1336
    /**
1337
     * WARNING: This method will return broken characters and is only for special cases.
1338
     *
1339
     * Convert a UTF-8 encoded string to a single-byte string suitable for
1340
     * functions that need the same string length after the conversion.
1341
     *
1342
     * The function simply uses (and updates) a tailored dynamic encoding
1343
     * (in/out map parameter) where non-ascii characters are remapped to
1344
     * the range [128-255] in order of appearance.
1345
     *
1346
     * Thus, it supports up to 128 different multibyte code points max over
1347
     * the whole set of strings sharing this encoding.
1348
     *
1349
     * Source: https://github.com/KEINOS/mb_levenshtein
1350
     *
1351
     * @param string $str <p>UTF-8 string to be converted to extended ASCII.</p>
1352
     * @param array  $map <p>Internal-Map of code points to ASCII characters.</p>
1353
     *
1354
     * @return string
1355
     *                <p>Mapped borken string.</p>
1356
     *
1357
     * @phpstan-param array<string, string> $map
1358
     */
1359
    private static function to_ascii_remap_intern(string $str, array &$map): string
1360
    {
1361
        // find all utf-8 characters
1362
        $matches = [];
1363
        if (!\preg_match_all('/[\xC0-\xF7][\x80-\xBF]+/', $str, $matches)) {
1364
            return $str; // plain ascii string
1365
        }
1366
 
1367
        // update the encoding map with the characters not already met
1368
        $mapCount = \count($map);
1369
        foreach ($matches[0] as $mbc) {
1370
            if (!isset($map[$mbc])) {
1371
                $map[$mbc] = \chr(128 + $mapCount);
1372
                ++$mapCount;
1373
            }
1374
        }
1375
 
1376
        // finally, remap non-ascii characters
1377
        return \strtr($str, $map);
1378
    }
1379
 
1380
    /**
1381
     * Get the language from a string.
1382
     *
1383
     * e.g.: de_at -> de_at
1384
     *       de_DE -> de
1385
     *       DE_DE -> de
1386
     *       de-de -> de
1387
     *
1388
     * @noinspection ReturnTypeCanBeDeclaredInspection
1389
     *
1390
     * @param string $language
1391
     *
1392
     * @psalm-pure
1393
     *
1394
     * @return string
1395
     */
1396
    private static function get_language(string $language)
1397
    {
1398
        if ($language === '') {
1399
            return '';
1400
        }
1401
 
1402
        if (
1403
            \strpos($language, '_') === false
1404
            &&
1405
            \strpos($language, '-') === false
1406
        ) {
1407
            return \strtolower($language);
1408
        }
1409
 
1410
        $language = \str_replace('-', '_', \strtolower($language));
1411
 
1412
        $regex = '/(?<first>[a-z]+)_\g{first}/';
1413
 
1414
        return (string) \preg_replace($regex, '$1', $language);
1415
    }
1416
 
1417
    /**
1418
     * Get data from "/data/*.php".
1419
     *
1420
     * @noinspection ReturnTypeCanBeDeclaredInspection
1421
     *
1422
     * @param string $file
1423
     *
1424
     * @psalm-pure
1425
     *
1426
     * @return array<mixed>
1427
     */
1428
    private static function getData(string $file)
1429
    {
1430
        /** @noinspection PhpIncludeInspection */
1431
        /** @noinspection UsingInclusionReturnValueInspection */
1432
        /** @psalm-suppress UnresolvableInclude */
1433
        return include __DIR__ . '/data/' . $file . '.php';
1434
    }
1435
 
1436
    /**
1437
     * Get data from "/data/*.php".
1438
     *
1439
     * @param string $file
1440
     *
1441
     * @psalm-pure
1442
     *
1443
     * @return array<mixed>
1444
     */
1445
    private static function getDataIfExists(string $file): array
1446
    {
1447
        $file = __DIR__ . '/data/' . $file . '.php';
1448
        /** @psalm-suppress ImpureFunctionCall */
1449
        if (\is_file($file)) {
1450
            /** @noinspection PhpIncludeInspection */
1451
            /** @noinspection UsingInclusionReturnValueInspection */
1452
            /** @psalm-suppress UnresolvableInclude */
1453
            return include $file;
1454
        }
1455
 
1456
        return [];
1457
    }
1458
 
1459
    /**
1460
     * @psalm-pure
1461
     *
1462
     * @return void
1463
     */
1464
    private static function prepareAsciiAndExtrasMaps()
1465
    {
1466
        if (self::$ASCII_MAPS_AND_EXTRAS === null) {
1467
            self::prepareAsciiMaps();
1468
            self::prepareAsciiExtras();
1469
 
1470
            /** @psalm-suppress PossiblyNullArgument - we use the prepare* methods here, so we don't get NULL here */
1471
            self::$ASCII_MAPS_AND_EXTRAS = \array_merge_recursive(
1472
                self::$ASCII_MAPS ?? [],
1473
                self::$ASCII_EXTRAS ?? []
1474
            );
1475
        }
1476
    }
1477
 
1478
    /**
1479
     * @psalm-pure
1480
     *
1481
     * @return void
1482
     */
1483
    private static function prepareAsciiMaps()
1484
    {
1485
        if (self::$ASCII_MAPS === null) {
1486
            self::$ASCII_MAPS = self::getData('ascii_by_languages');
1487
        }
1488
    }
1489
 
1490
    /**
1491
     * @psalm-pure
1492
     *
1493
     * @return void
1494
     */
1495
    private static function prepareAsciiExtras()
1496
    {
1497
        if (self::$ASCII_EXTRAS === null) {
1498
            self::$ASCII_EXTRAS = self::getData('ascii_extras_by_languages');
1499
        }
1500
    }
1501
}