PluginProbe
WP-Stateless – Google Cloud Storage / 3.2.0
WP-Stateless – Google Cloud Storage v3.2.0
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / vendor / symfony / string / AbstractUnicodeString.php

AbstractUnicodeString.php in WP-Stateless – Google Cloud Storage 3.2.0, at vendor/symfony/string/AbstractUnicodeString.php

602 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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', 'i̇', 'ʼ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
76 /**
77 * @return static
78 */
79 public static function fromCodePoints(int ...$codes): self
80 {
81 $string = '';
82
83 foreach ($codes as $code) {
84 if (0x80 > $code %= 0x200000) {
85 $string .= \chr($code);
86 } elseif (0x800 > $code) {
87 $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
88 } elseif (0x10000 > $code) {
89 $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
90 } else {
91 $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
92 }
93 }
94
95 return new static($string);
96 }
97
98 /**
99 * Generic UTF-8 to ASCII transliteration.
100 *
101 * Install the intl extension for best results.
102 *
103 * @param string[]|\Transliterator[] $rules See "*-Latin" rules from Transliterator::listIDs()
104 */
105 public function ascii(array $rules = []): self
106 {
107 $str = clone $this;
108 $s = $str->string;
109 $str->string = '';
110
111 array_unshift($rules, 'nfd');
112 $rules[] = 'latin-ascii';
113
114 if (\function_exists('transliterator_transliterate')) {
115 $rules[] = 'any-latin/bgn';
116 }
117
118 $rules[] = 'nfkd';
119 $rules[] = '[:nonspacing mark:] remove';
120
121 while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) {
122 if (0 < --$i) {
123 $str->string .= substr($s, 0, $i);
124 $s = substr($s, $i);
125 }
126
127 if (!$rule = array_shift($rules)) {
128 $rules = []; // An empty rule interrupts the next ones
129 }
130
131 if ($rule instanceof \Transliterator) {
132 $s = $rule->transliterate($s);
133 } elseif ($rule) {
134 if ('nfd' === $rule = strtolower($rule)) {
135 normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD);
136 } elseif ('nfkd' === $rule) {
137 normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD);
138 } elseif ('[:nonspacing mark:] remove' === $rule) {
139 $s = preg_replace('/\p{Mn}++/u', '', $s);
140 } elseif ('latin-ascii' === $rule) {
141 $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s);
142 } elseif ('de-ascii' === $rule) {
143 $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s);
144 $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);
145 } elseif (\function_exists('transliterator_transliterate')) {
146 if (null === $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule)) {
147 if ('any-latin/bgn' === $rule) {
148 $rule = 'any-latin';
149 $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule);
150 }
151
152 if (null === $transliterator) {
153 throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule));
154 }
155
156 self::$transliterators['any-latin/bgn'] = $transliterator;
157 }
158
159 $s = $transliterator->transliterate($s);
160 }
161 } elseif (!\function_exists('iconv')) {
162 $s = preg_replace('/[^\x00-\x7F]/u', '?', $s);
163 } elseif (ICONV_IMPL === 'glibc') {
164 $s = iconv('UTF-8', 'ASCII//TRANSLIT', $s);
165 } else {
166 $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) {
167 if ('' === $c = (string) iconv('UTF-8', 'ASCII//IGNORE//TRANSLIT', $c[0])) {
168 throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class));
169 }
170
171 return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : (\strlen($c) ? $c : '?');
172 }, $s);
173 }
174 }
175
176 $str->string .= $s;
177
178 return $str;
179 }
180
181 public function camel(): parent
182 {
183 $str = clone $this;
184 $str->string = str_replace(' ', '', preg_replace_callback('/\b./u', static function ($m) use (&$i) {
185 return 1 === ++$i ? ('İ' === $m[0] ? '' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], MB_CASE_TITLE, 'UTF-8');
186 }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string)));
187
188 return $str;
189 }
190
191 /**
192 * @return int[]
193 */
194 public function codePointsAt(int $offset): array
195 {
196 $str = $this->slice($offset, 1);
197
198 if ('' === $str->string) {
199 return [];
200 }
201
202 $codePoints = [];
203
204 foreach (preg_split('//u', $str->string, -1, PREG_SPLIT_NO_EMPTY) as $c) {
205 $codePoints[] = mb_ord($c, 'UTF-8');
206 }
207
208 return $codePoints;
209 }
210
211 public function folded(bool $compat = true): parent
212 {
213 $str = clone $this;
214
215 if (!$compat || \PHP_VERSION_ID < 70300 || !\defined('Normalizer::NFKC_CF')) {
216 $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC);
217 $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $this->string), 'UTF-8');
218 } else {
219 $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF);
220 }
221
222 return $str;
223 }
224
225 public function join(array $strings, string $lastGlue = null): parent
226 {
227 $str = clone $this;
228
229 $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : '';
230 $str->string = implode($this->string, $strings).$tail;
231
232 if (!preg_match('//u', $str->string)) {
233 throw new InvalidArgumentException('Invalid UTF-8 string.');
234 }
235
236 return $str;
237 }
238
239 public function lower(): parent
240 {
241 $str = clone $this;
242 $str->string = mb_strtolower(str_replace('İ', '', $str->string), 'UTF-8');
243
244 return $str;
245 }
246
247 public function match(string $regexp, int $flags = 0, int $offset = 0): array
248 {
249 $match = ((PREG_PATTERN_ORDER | PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match';
250
251 if ($this->ignoreCase) {
252 $regexp .= 'i';
253 }
254
255 set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
256
257 try {
258 if (false === $match($regexp.'u', $this->string, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset)) {
259 $lastError = preg_last_error();
260
261 foreach (get_defined_constants(true)['pcre'] as $k => $v) {
262 if ($lastError === $v && '_ERROR' === substr($k, -6)) {
263 throw new RuntimeException('Matching failed with '.$k.'.');
264 }
265 }
266
267 throw new RuntimeException('Matching failed with unknown error code.');
268 }
269 } finally {
270 restore_error_handler();
271 }
272
273 return $matches;
274 }
275
276 /**
277 * @return static
278 */
279 public function normalize(int $form = self::NFC): self
280 {
281 if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) {
282 throw new InvalidArgumentException('Unsupported normalization form.');
283 }
284
285 $str = clone $this;
286 normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form);
287
288 return $str;
289 }
290
291 public function padBoth(int $length, string $padStr = ' '): parent
292 {
293 if ('' === $padStr || !preg_match('//u', $padStr)) {
294 throw new InvalidArgumentException('Invalid UTF-8 string.');
295 }
296
297 $pad = clone $this;
298 $pad->string = $padStr;
299
300 return $this->pad($length, $pad, STR_PAD_BOTH);
301 }
302
303 public function padEnd(int $length, string $padStr = ' '): parent
304 {
305 if ('' === $padStr || !preg_match('//u', $padStr)) {
306 throw new InvalidArgumentException('Invalid UTF-8 string.');
307 }
308
309 $pad = clone $this;
310 $pad->string = $padStr;
311
312 return $this->pad($length, $pad, STR_PAD_RIGHT);
313 }
314
315 public function padStart(int $length, string $padStr = ' '): parent
316 {
317 if ('' === $padStr || !preg_match('//u', $padStr)) {
318 throw new InvalidArgumentException('Invalid UTF-8 string.');
319 }
320
321 $pad = clone $this;
322 $pad->string = $padStr;
323
324 return $this->pad($length, $pad, STR_PAD_LEFT);
325 }
326
327 public function replaceMatches(string $fromRegexp, $to): parent
328 {
329 if ($this->ignoreCase) {
330 $fromRegexp .= 'i';
331 }
332
333 if (\is_array($to) || $to instanceof \Closure) {
334 if (!\is_callable($to)) {
335 throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class));
336 }
337
338 $replace = 'preg_replace_callback';
339 $to = static function (array $m) use ($to): string {
340 $to = $to($m);
341
342 if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) {
343 throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.');
344 }
345
346 return $to;
347 };
348 } elseif ('' !== $to && !preg_match('//u', $to)) {
349 throw new InvalidArgumentException('Invalid UTF-8 string.');
350 } else {
351 $replace = 'preg_replace';
352 }
353
354 set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
355
356 try {
357 if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) {
358 $lastError = preg_last_error();
359
360 foreach (get_defined_constants(true)['pcre'] as $k => $v) {
361 if ($lastError === $v && '_ERROR' === substr($k, -6)) {
362 throw new RuntimeException('Matching failed with '.$k.'.');
363 }
364 }
365
366 throw new RuntimeException('Matching failed with unknown error code.');
367 }
368 } finally {
369 restore_error_handler();
370 }
371
372 $str = clone $this;
373 $str->string = $string;
374
375 return $str;
376 }
377
378 public function reverse(): parent
379 {
380 $str = clone $this;
381 $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)));
382
383 return $str;
384 }
385
386 public function snake(): parent
387 {
388 $str = $this->camel()->title();
389 $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');
390
391 return $str;
392 }
393
394 public function title(bool $allWords = false): parent
395 {
396 $str = clone $this;
397
398 $limit = $allWords ? -1 : 1;
399
400 $str->string = preg_replace_callback('/\b./u', static function (array $m): string {
401 return mb_convert_case($m[0], MB_CASE_TITLE, 'UTF-8');
402 }, $str->string, $limit);
403
404 return $str;
405 }
406
407 public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
408 {
409 if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
410 throw new InvalidArgumentException('Invalid UTF-8 chars.');
411 }
412 $chars = preg_quote($chars);
413
414 $str = clone $this;
415 $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string);
416
417 return $str;
418 }
419
420 public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
421 {
422 if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
423 throw new InvalidArgumentException('Invalid UTF-8 chars.');
424 }
425 $chars = preg_quote($chars);
426
427 $str = clone $this;
428 $str->string = preg_replace("{[$chars]++$}uD", '', $str->string);
429
430 return $str;
431 }
432
433 public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
434 {
435 if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
436 throw new InvalidArgumentException('Invalid UTF-8 chars.');
437 }
438 $chars = preg_quote($chars);
439
440 $str = clone $this;
441 $str->string = preg_replace("{^[$chars]++}uD", '', $str->string);
442
443 return $str;
444 }
445
446 public function upper(): parent
447 {
448 $str = clone $this;
449 $str->string = mb_strtoupper($str->string, 'UTF-8');
450
451 if (\PHP_VERSION_ID < 70300) {
452 $str->string = str_replace(self::UPPER_FROM, self::UPPER_TO, $str->string);
453 }
454
455 return $str;
456 }
457
458 public function width(bool $ignoreAnsiDecoration = true): int
459 {
460 $width = 0;
461 $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string);
462
463 if (false !== strpos($s, "\r")) {
464 $s = str_replace(["\r\n", "\r"], "\n", $s);
465 }
466
467 if (!$ignoreAnsiDecoration) {
468 $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s);
469 }
470
471 foreach (explode("\n", $s) as $s) {
472 if ($ignoreAnsiDecoration) {
473 $s = preg_replace('/(?:\x1B(?:
474 \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [0x40-\x7E]
475 | [P\]X^_] .*? \x1B\\\\
476 | [\x41-\x7E]
477 )|[\p{Cc}\x7F]++)/xu', '', $s);
478 }
479
480 // Non printable characters have been dropped, so wcswidth cannot logically return -1.
481 $width += $this->wcswidth($s);
482 }
483
484 return $width;
485 }
486
487 /**
488 * @return static
489 */
490 private function pad(int $len, self $pad, int $type): parent
491 {
492 $sLen = $this->length();
493
494 if ($len <= $sLen) {
495 return clone $this;
496 }
497
498 $padLen = $pad->length();
499 $freeLen = $len - $sLen;
500 $len = $freeLen % $padLen;
501
502 switch ($type) {
503 case STR_PAD_RIGHT:
504 return $this->append(str_repeat($pad->string, $freeLen / $padLen).($len ? $pad->slice(0, $len) : ''));
505
506 case STR_PAD_LEFT:
507 return $this->prepend(str_repeat($pad->string, $freeLen / $padLen).($len ? $pad->slice(0, $len) : ''));
508
509 case STR_PAD_BOTH:
510 $freeLen /= 2;
511
512 $rightLen = ceil($freeLen);
513 $len = $rightLen % $padLen;
514 $str = $this->append(str_repeat($pad->string, $rightLen / $padLen).($len ? $pad->slice(0, $len) : ''));
515
516 $leftLen = floor($freeLen);
517 $len = $leftLen % $padLen;
518
519 return $str->prepend(str_repeat($pad->string, $leftLen / $padLen).($len ? $pad->slice(0, $len) : ''));
520
521 default:
522 throw new InvalidArgumentException('Invalid padding type.');
523 }
524 }
525
526 /**
527 * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c.
528 */
529 private function wcswidth(string $string): int
530 {
531 $width = 0;
532
533 foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $c) {
534 $codePoint = mb_ord($c, 'UTF-8');
535
536 if (0 === $codePoint // NULL
537 || 0x034F === $codePoint // COMBINING GRAPHEME JOINER
538 || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK
539 || 0x2028 === $codePoint // LINE SEPARATOR
540 || 0x2029 === $codePoint // PARAGRAPH SEPARATOR
541 || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE
542 || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR
543 ) {
544 continue;
545 }
546
547 // Non printable characters
548 if (32 > $codePoint // C0 control characters
549 || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL
550 ) {
551 return -1;
552 }
553
554 static $tableZero;
555 if (null === $tableZero) {
556 $tableZero = require __DIR__.'/Resources/data/wcswidth_table_zero.php';
557 }
558
559 if ($codePoint >= $tableZero[0][0] && $codePoint <= $tableZero[$ubound = \count($tableZero) - 1][1]) {
560 $lbound = 0;
561 while ($ubound >= $lbound) {
562 $mid = floor(($lbound + $ubound) / 2);
563
564 if ($codePoint > $tableZero[$mid][1]) {
565 $lbound = $mid + 1;
566 } elseif ($codePoint < $tableZero[$mid][0]) {
567 $ubound = $mid - 1;
568 } else {
569 continue 2;
570 }
571 }
572 }
573
574 static $tableWide;
575 if (null === $tableWide) {
576 $tableWide = require __DIR__.'/Resources/data/wcswidth_table_wide.php';
577 }
578
579 if ($codePoint >= $tableWide[0][0] && $codePoint <= $tableWide[$ubound = \count($tableWide) - 1][1]) {
580 $lbound = 0;
581 while ($ubound >= $lbound) {
582 $mid = floor(($lbound + $ubound) / 2);
583
584 if ($codePoint > $tableWide[$mid][1]) {
585 $lbound = $mid + 1;
586 } elseif ($codePoint < $tableWide[$mid][0]) {
587 $ubound = $mid - 1;
588 } else {
589 $width += 2;
590
591 continue 2;
592 }
593 }
594 }
595
596 ++$width;
597 }
598
599 return $width;
600 }
601 }
602