PluginProbe ʕ •ᴥ•ʔ
Kubio AI Page Builder / 2.8.4
Kubio AI Page Builder v2.8.4
2.8.4 2.8.3 2.8.2 2.8.1 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.7.3 1.8.0 1.8.1 1.8.2 1.9.0 2.0.0 2.1.1 2.1.2 2.1.3 2.2.0 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.5 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.6.2 2.6.3 2.6.5 2.6.6 2.6.7 2.7.0 2.7.1 2.7.2 2.7.3 2.8.0
kubio / vendor / symfony / string / AbstractUnicodeString.php
kubio / vendor / symfony / string Last commit date
Exception 1 year ago Inflector 1 year ago Resources 1 year ago Slugger 1 year ago AbstractString.php 1 year ago AbstractUnicodeString.php 1 year ago ByteString.php 1 year ago CHANGELOG.md 1 year ago CodePointString.php 1 year ago LICENSE 1 year ago LazyString.php 1 year ago README.md 1 year ago UnicodeString.php 1 year ago composer.json 1 year ago
AbstractUnicodeString.php
647 lines
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\String;
13
14 use Symfony\Component\String\Exception\ExceptionInterface;
15 use Symfony\Component\String\Exception\InvalidArgumentException;
16 use Symfony\Component\String\Exception\RuntimeException;
17
18 /**
19 * Represents a string of abstract Unicode characters.
20 *
21 * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters).
22 * This class is the abstract type to use as a type-hint when the logic you want to
23 * implement is Unicode-aware but doesn't care about code points vs grapheme clusters.
24 *
25 * @author Nicolas Grekas <p@tchwork.com>
26 *
27 * @throws ExceptionInterface
28 */
29 abstract class AbstractUnicodeString extends AbstractString
30 {
31 public const NFC = \Normalizer::NFC;
32 public const NFD = \Normalizer::NFD;
33 public const NFKC = \Normalizer::NFKC;
34 public const NFKD = \Normalizer::NFKD;
35
36 // all ASCII letters sorted by typical frequency of occurrence
37 private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
38
39 // the subset of folded case mappings that is not in lower case mappings
40 private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', '�
41 ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ff', 'fi', 'fl', 'ffi', 'ffl', '�
42 ', '', '', '', '', '', ''];
43 private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', '�
44 ι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', '�
45 ι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', '', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', '', 'ῆι', 'ηι', '', '', '', '', '', '', '', 'ὼι', 'ωι', 'ώι', '', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ'];
46
47 // the subset of upper case mappings that map one code point to many code points
48 private const UPPER_FROM = ['ß', 'ff', 'fi', 'fl', 'ffi', 'ffl', '�
49 ', '', 'և', '', '', '', '', '', 'ʼn', 'ΐ', 'ΰ', 'ǰ', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''];
50 private const UPPER_TO = ['SS', 'FF', 'FI', 'FL', 'FFI', 'FFL', 'ST', 'ST', 'ԵՒ', 'ՄՆ', 'ՄԵ', 'ՄԻ', 'ՎՆ', 'ՄԽ', 'ʼN', 'Ϊ́', 'Ϋ́', '', '', '', '', '', '', 'Υ̓', 'Υ̓̀', 'Υ̓́', 'Υ̓͂', 'Α͂', 'Η͂', 'Ϊ̀', 'Ϊ́', 'Ι͂', 'Ϊ͂', 'Ϋ̀', 'Ϋ́', 'Ρ̓', 'Υ͂', 'Ϋ͂', 'Ω͂'];
51
52 // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD
53 private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ʼn', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'DŽ', '�
54 ', 'dž', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', '�
55 ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', '�
56 ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '�
57 �', '�
58 �', '�
59 �', '�
60 �', '�
61 �', '�
62 �', '�
63 �', '�
64 �', '�
65 �', '�
66 �', '�
67 �', '�
68 �', '�
69 �', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '�
70 ', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '⦅', '⦆', '。', '、', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '�
71 ', ''];
72 private const TRANSLIT_TO = ['AE', 'D', 'O', 'TH', 'ss', 'ae', 'd', 'o', 'th', 'D', 'd', 'H', 'h', 'i', 'q', 'L', 'l', 'L', 'l', '\'n', 'N', 'n', 'OE', 'oe', 'T', 't', 'b', 'B', 'B', 'b', 'C', 'c', 'D', 'D', 'D', 'd', 'E', 'F', 'f', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'N', 'n', 'OI', 'oi', 'P', 'p', 't', 'T', 't', 'T', 'V', 'Y', 'y', 'Z', 'z', 'DZ', 'Dz', 'dz', 'G', 'g', 'd', 'Z', 'z', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's', 'z', 'B', 'U', 'E', 'e', 'J', 'j', 'R', 'r', 'Y', 'y', 'b', 'c', 'd', 'd', 'e', 'j', 'g', 'g', 'G', 'h', 'h', 'i', 'I', 'l', 'l', 'l', 'm', 'n', 'n', 'N', 'OE', 'r', 'r', 'r', 'R', 's', 't', 'u', 'v', 'Y', 'z', 'z', 'B', 'G', 'H', 'j', 'L', 'q', 'dz', 'dz', 'ts', 'ls', 'lz', 'A', 'AE', 'B', 'C', 'D', 'D', 'E', 'J', 'K', 'L', 'M', 'O', 'P', 'T', 'U', 'V', 'W', 'Z', 'ue', 'b', 'd', 'f', 'm', 'n', 'p', 'r', 'r', 's', 't', 'z', 'th', 'I', 'p', 'U', 'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 'v', 'x', 'z', 'a', 'd', 'e', 'e', 'i', 'u', 'a', 's', 's', 'SS', 'LL', 'll', 'V', 'v', 'Y', 'y', '(C)', '(R)', 'CE', 'Cr', 'Fr.', 'L.', 'Pts', 'TL', 'Rs', 'x', 'Rx', 'm/s', 'rad/s', 'C/kg', 'pH', 'V/m', 'A/m', ' 1/4', ' 1/2', ' 3/4', ' 1/3', ' 2/3', ' 1/5', ' 2/5', ' 3/5', ' 4/5', ' 1/6', ' 5/6', ' 1/8', ' 3/8', ' 5/8', ' 7/8', ' 1/', '0', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))'];
73
74 private static $transliterators = [];
75 private static $tableZero;
76 private static $tableWide;
77
78 /**
79 * @return static
80 */
81 public static function fromCodePoints(int ...$codes): self
82 {
83 $string = '';
84
85 foreach ($codes as $code) {
86 if (0x80 > $code %= 0x200000) {
87 $string .= \chr($code);
88 } elseif (0x800 > $code) {
89 $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
90 } elseif (0x10000 > $code) {
91 $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
92 } else {
93 $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
94 }
95 }
96
97 return new static($string);
98 }
99
100 /**
101 * Generic UTF-8 to ASCII transliteration.
102 *
103 * Install the intl extension for best results.
104 *
105 * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs()
106 */
107 public function ascii(array $rules = []): self
108 {
109 $str = clone $this;
110 $s = $str->string;
111 $str->string = '';
112
113 array_unshift($rules, 'nfd');
114 $rules[] = 'latin-ascii';
115
116 if (\function_exists('transliterator_transliterate')) {
117 $rules[] = 'any-latin/bgn';
118 }
119
120 $rules[] = 'nfkd';
121 $rules[] = '[:nonspacing mark:] remove';
122
123 while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) {
124 if (0 < --$i) {
125 $str->string .= substr($s, 0, $i);
126 $s = substr($s, $i);
127 }
128
129 if (!$rule = array_shift($rules)) {
130 $rules = []; // An empty rule interrupts the next ones
131 }
132
133 if ($rule instanceof \Transliterator) {
134 $s = $rule->transliterate($s);
135 } elseif ($rule instanceof \Closure) {
136 $s = $rule($s);
137 } elseif ($rule) {
138 if ('nfd' === $rule = strtolower($rule)) {
139 normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD);
140 } elseif ('nfkd' === $rule) {
141 normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD);
142 } elseif ('[:nonspacing mark:] remove' === $rule) {
143 $s = preg_replace('/\p{Mn}++/u', '', $s);
144 } elseif ('latin-ascii' === $rule) {
145 $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s);
146 } elseif ('de-ascii' === $rule) {
147 $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s);
148 $s = str_replace(["a\u{0308}", "o\u{0308}", "u\u{0308}", "A\u{0308}", "O\u{0308}", "U\u{0308}"], ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], $s);
149 } elseif (\function_exists('transliterator_transliterate')) {
150 if (null === $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule)) {
151 if ('any-latin/bgn' === $rule) {
152 $rule = 'any-latin';
153 $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule);
154 }
155
156 if (null === $transliterator) {
157 throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule));
158 }
159
160 self::$transliterators['any-latin/bgn'] = $transliterator;
161 }
162
163 $s = $transliterator->transliterate($s);
164 }
165 } elseif (!\function_exists('iconv')) {
166 $s = preg_replace('/[^\x00-\x7F]/u', '?', $s);
167 } else {
168 $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) {
169 $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]);
170
171 if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) {
172 throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class));
173 }
174
175 return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?');
176 }, $s);
177 }
178 }
179
180 $str->string .= $s;
181
182 return $str;
183 }
184
185 public function camel(): parent
186 {
187 $str = clone $this;
188 $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) use (&$i) {
189 return 1 === ++$i ? ('İ' === $m[0] ? '' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8');
190 }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string)));
191
192 return $str;
193 }
194
195 /**
196 * @return int[]
197 */
198 public function codePointsAt(int $offset): array
199 {
200 $str = $this->slice($offset, 1);
201
202 if ('' === $str->string) {
203 return [];
204 }
205
206 $codePoints = [];
207
208 foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) {
209 $codePoints[] = mb_ord($c, 'UTF-8');
210 }
211
212 return $codePoints;
213 }
214
215 public function folded(bool $compat = true): parent
216 {
217 $str = clone $this;
218
219 if (!$compat || \PHP_VERSION_ID < 70300 || !\defined('Normalizer::NFKC_CF')) {
220 $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC);
221 $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $str->string), 'UTF-8');
222 } else {
223 $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF);
224 }
225
226 return $str;
227 }
228
229 public function join(array $strings, ?string $lastGlue = null): parent
230 {
231 $str = clone $this;
232
233 $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : '';
234 $str->string = implode($this->string, $strings).$tail;
235
236 if (!preg_match('//u', $str->string)) {
237 throw new InvalidArgumentException('Invalid UTF-8 string.');
238 }
239
240 return $str;
241 }
242
243 public function lower(): parent
244 {
245 $str = clone $this;
246 $str->string = mb_strtolower(str_replace('İ', '', $str->string), 'UTF-8');
247
248 return $str;
249 }
250
251 public function match(string $regexp, int $flags = 0, int $offset = 0): array
252 {
253 $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match';
254
255 if ($this->ignoreCase) {
256 $regexp .= 'i';
257 }
258
259 set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
260
261 try {
262 if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
263 $lastError = preg_last_error();
264
265 foreach (get_defined_constants(true)['pcre'] as $k => $v) {
266 if ($lastError === $v && '_ERROR' === substr($k, -6)) {
267 throw new RuntimeException('Matching failed with '.$k.'.');
268 }
269 }
270
271 throw new RuntimeException('Matching failed with unknown error code.');
272 }
273 } finally {
274 restore_error_handler();
275 }
276
277 return $matches;
278 }
279
280 /**
281 * @return static
282 */
283 public function normalize(int $form = self::NFC): self
284 {
285 if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) {
286 throw new InvalidArgumentException('Unsupported normalization form.');
287 }
288
289 $str = clone $this;
290 normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form);
291
292 return $str;
293 }
294
295 public function padBoth(int $length, string $padStr = ' '): parent
296 {
297 if ('' === $padStr || !preg_match('//u', $padStr)) {
298 throw new InvalidArgumentException('Invalid UTF-8 string.');
299 }
300
301 $pad = clone $this;
302 $pad->string = $padStr;
303
304 return $this->pad($length, $pad, \STR_PAD_BOTH);
305 }
306
307 public function padEnd(int $length, string $padStr = ' '): parent
308 {
309 if ('' === $padStr || !preg_match('//u', $padStr)) {
310 throw new InvalidArgumentException('Invalid UTF-8 string.');
311 }
312
313 $pad = clone $this;
314 $pad->string = $padStr;
315
316 return $this->pad($length, $pad, \STR_PAD_RIGHT);
317 }
318
319 public function padStart(int $length, string $padStr = ' '): parent
320 {
321 if ('' === $padStr || !preg_match('//u', $padStr)) {
322 throw new InvalidArgumentException('Invalid UTF-8 string.');
323 }
324
325 $pad = clone $this;
326 $pad->string = $padStr;
327
328 return $this->pad($length, $pad, \STR_PAD_LEFT);
329 }
330
331 public function replaceMatches(string $fromRegexp, $to): parent
332 {
333 if ($this->ignoreCase) {
334 $fromRegexp .= 'i';
335 }
336
337 if (\is_array($to) || $to instanceof \Closure) {
338 if (!\is_callable($to)) {
339 throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class));
340 }
341
342 $replace = 'preg_replace_callback';
343 $to = static function (array $m) use ($to): string {
344 $to = $to($m);
345
346 if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) {
347 throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.');
348 }
349
350 return $to;
351 };
352 } elseif ('' !== $to && !preg_match('//u', $to)) {
353 throw new InvalidArgumentException('Invalid UTF-8 string.');
354 } else {
355 $replace = 'preg_replace';
356 }
357
358 set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
359
360 try {
361 if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) {
362 $lastError = preg_last_error();
363
364 foreach (get_defined_constants(true)['pcre'] as $k => $v) {
365 if ($lastError === $v && '_ERROR' === substr($k, -6)) {
366 throw new RuntimeException('Matching failed with '.$k.'.');
367 }
368 }
369
370 throw new RuntimeException('Matching failed with unknown error code.');
371 }
372 } finally {
373 restore_error_handler();
374 }
375
376 $str = clone $this;
377 $str->string = $string;
378
379 return $str;
380 }
381
382 public function reverse(): parent
383 {
384 $str = clone $this;
385 $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY)));
386
387 return $str;
388 }
389
390 public function snake(): parent
391 {
392 $str = $this->camel();
393 $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8');
394
395 return $str;
396 }
397
398 public function title(bool $allWords = false): parent
399 {
400 $str = clone $this;
401
402 $limit = $allWords ? -1 : 1;
403
404 $str->string = preg_replace_callback('/\b./u', static function (array $m): string {
405 return mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8');
406 }, $str->string, $limit);
407
408 return $str;
409 }
410
411 public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
412 {
413 if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
414 throw new InvalidArgumentException('Invalid UTF-8 chars.');
415 }
416 $chars = preg_quote($chars);
417
418 $str = clone $this;
419 $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string);
420
421 return $str;
422 }
423
424 public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
425 {
426 if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
427 throw new InvalidArgumentException('Invalid UTF-8 chars.');
428 }
429 $chars = preg_quote($chars);
430
431 $str = clone $this;
432 $str->string = preg_replace("{[$chars]++$}uD", '', $str->string);
433
434 return $str;
435 }
436
437 public function trimPrefix($prefix): parent
438 {
439 if (!$this->ignoreCase) {
440 return parent::trimPrefix($prefix);
441 }
442
443 $str = clone $this;
444
445 if ($prefix instanceof \Traversable) {
446 $prefix = iterator_to_array($prefix, false);
447 } elseif ($prefix instanceof parent) {
448 $prefix = $prefix->string;
449 }
450
451 $prefix = implode('|', array_map('preg_quote', (array) $prefix));
452 $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string);
453
454 return $str;
455 }
456
457 public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
458 {
459 if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
460 throw new InvalidArgumentException('Invalid UTF-8 chars.');
461 }
462 $chars = preg_quote($chars);
463
464 $str = clone $this;
465 $str->string = preg_replace("{^[$chars]++}uD", '', $str->string);
466
467 return $str;
468 }
469
470 public function trimSuffix($suffix): parent
471 {
472 if (!$this->ignoreCase) {
473 return parent::trimSuffix($suffix);
474 }
475
476 $str = clone $this;
477
478 if ($suffix instanceof \Traversable) {
479 $suffix = iterator_to_array($suffix, false);
480 } elseif ($suffix instanceof parent) {
481 $suffix = $suffix->string;
482 }
483
484 $suffix = implode('|', array_map('preg_quote', (array) $suffix));
485 $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string);
486
487 return $str;
488 }
489
490 public function upper(): parent
491 {
492 $str = clone $this;
493 $str->string = mb_strtoupper($str->string, 'UTF-8');
494
495 if (\PHP_VERSION_ID < 70300) {
496 $str->string = str_replace(self::UPPER_FROM, self::UPPER_TO, $str->string);
497 }
498
499 return $str;
500 }
501
502 public function width(bool $ignoreAnsiDecoration = true): int
503 {
504 $width = 0;
505 $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string);
506
507 if (false !== strpos($s, "\r")) {
508 $s = str_replace(["\r\n", "\r"], "\n", $s);
509 }
510
511 if (!$ignoreAnsiDecoration) {
512 $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s);
513 }
514
515 foreach (explode("\n", $s) as $s) {
516 if ($ignoreAnsiDecoration) {
517 $s = preg_replace('/(?:\x1B(?:
518 \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E]
519 | [P\]X^_] .*? \x1B\\\\
520 | [\x41-\x7E]
521 )|[\p{Cc}\x7F]++)/xu', '', $s);
522 }
523
524 $lineWidth = $this->wcswidth($s);
525
526 if ($lineWidth > $width) {
527 $width = $lineWidth;
528 }
529 }
530
531 return $width;
532 }
533
534 /**
535 * @return static
536 */
537 private function pad(int $len, self $pad, int $type): parent
538 {
539 $sLen = $this->length();
540
541 if ($len <= $sLen) {
542 return clone $this;
543 }
544
545 $padLen = $pad->length();
546 $freeLen = $len - $sLen;
547 $len = $freeLen % $padLen;
548
549 switch ($type) {
550 case \STR_PAD_RIGHT:
551 return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : ''));
552
553 case \STR_PAD_LEFT:
554 return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : ''));
555
556 case \STR_PAD_BOTH:
557 $freeLen /= 2;
558
559 $rightLen = ceil($freeLen);
560 $len = $rightLen % $padLen;
561 $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : ''));
562
563 $leftLen = floor($freeLen);
564 $len = $leftLen % $padLen;
565
566 return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : ''));
567
568 default:
569 throw new InvalidArgumentException('Invalid padding type.');
570 }
571 }
572
573 /**
574 * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c.
575 */
576 private function wcswidth(string $string): int
577 {
578 $width = 0;
579
580 foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) {
581 $codePoint = mb_ord($c, 'UTF-8');
582
583 if (0 === $codePoint // NULL
584 || 0x034F === $codePoint // COMBINING GRAPHEME JOINER
585 || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK
586 || 0x2028 === $codePoint // LINE SEPARATOR
587 || 0x2029 === $codePoint // PARAGRAPH SEPARATOR
588 || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE
589 || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR
590 ) {
591 continue;
592 }
593
594 // Non printable characters
595 if (32 > $codePoint // C0 control characters
596 || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL
597 ) {
598 return -1;
599 }
600
601 if (null === self::$tableZero) {
602 self::$tableZero = require __DIR__.'/Resources/data/wcswidth_table_zero.php';
603 }
604
605 if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) {
606 $lbound = 0;
607 while ($ubound >= $lbound) {
608 $mid = floor(($lbound + $ubound) / 2);
609
610 if ($codePoint > self::$tableZero[$mid][1]) {
611 $lbound = $mid + 1;
612 } elseif ($codePoint < self::$tableZero[$mid][0]) {
613 $ubound = $mid - 1;
614 } else {
615 continue 2;
616 }
617 }
618 }
619
620 if (null === self::$tableWide) {
621 self::$tableWide = require __DIR__.'/Resources/data/wcswidth_table_wide.php';
622 }
623
624 if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) {
625 $lbound = 0;
626 while ($ubound >= $lbound) {
627 $mid = floor(($lbound + $ubound) / 2);
628
629 if ($codePoint > self::$tableWide[$mid][1]) {
630 $lbound = $mid + 1;
631 } elseif ($codePoint < self::$tableWide[$mid][0]) {
632 $ubound = $mid - 1;
633 } else {
634 $width += 2;
635
636 continue 2;
637 }
638 }
639 }
640
641 ++$width;
642 }
643
644 return $width;
645 }
646 }
647