PluginProbe
Packeta / trunk
Packeta vtrunk
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / utils / src / Utils / Strings.php

Strings.php in Packeta trunk, at deps/nette/utils/src/Utils/Strings.php

455 lines 20.4 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 Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7 declare (strict_types=1);
8 namespace Packetery\Nette\Utils;
9
10 use Packetery\Nette;
11 use function is_array, is_object, strlen;
12 /**
13 * String tools library.
14 */
15 class Strings
16 {
17 use \Packetery\Nette\StaticClass;
18 public const TRIM_CHARACTERS = " \t\n\r\x00\v ";
19 /**
20 * Checks if the string is valid in UTF-8 encoding.
21 */
22 public static function checkEncoding(string $s) : bool
23 {
24 return $s === self::fixEncoding($s);
25 }
26 /**
27 * Removes all invalid UTF-8 characters from a string.
28 */
29 public static function fixEncoding(string $s) : string
30 {
31 // removes xD800-xDFFF, x110000 and higher
32 return \htmlspecialchars_decode(\htmlspecialchars($s, \ENT_NOQUOTES | \ENT_IGNORE, 'UTF-8'), \ENT_NOQUOTES);
33 }
34 /**
35 * Returns a specific character in UTF-8 from code point (number in range 0x0000..D7FF or 0xE000..10FFFF).
36 * @throws \Packetery\Nette\InvalidArgumentException if code point is not in valid range
37 */
38 public static function chr(int $code) : string
39 {
40 if ($code < 0 || $code >= 0xd800 && $code <= 0xdfff || $code > 0x10ffff) {
41 throw new \Packetery\Nette\InvalidArgumentException('Code point must be in range 0x0 to 0xD7FF or 0xE000 to 0x10FFFF.');
42 } elseif (!\extension_loaded('iconv')) {
43 throw new \Packetery\Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
44 }
45 return \iconv('UTF-32BE', 'UTF-8//IGNORE', \pack('N', $code));
46 }
47 /**
48 * Starts the $haystack string with the prefix $needle?
49 */
50 public static function startsWith(string $haystack, string $needle) : bool
51 {
52 return \strncmp($haystack, $needle, strlen($needle)) === 0;
53 }
54 /**
55 * Ends the $haystack string with the suffix $needle?
56 */
57 public static function endsWith(string $haystack, string $needle) : bool
58 {
59 return $needle === '' || \substr($haystack, -strlen($needle)) === $needle;
60 }
61 /**
62 * Does $haystack contain $needle?
63 */
64 public static function contains(string $haystack, string $needle) : bool
65 {
66 return \strpos($haystack, $needle) !== \false;
67 }
68 /**
69 * Returns a part of UTF-8 string specified by starting position and length. If start is negative,
70 * the returned string will start at the start'th character from the end of string.
71 */
72 public static function substring(string $s, int $start, ?int $length = null) : string
73 {
74 if (\function_exists('mb_substr')) {
75 return \mb_substr($s, $start, $length, 'UTF-8');
76 // MB is much faster
77 } elseif (!\extension_loaded('iconv')) {
78 throw new \Packetery\Nette\NotSupportedException(__METHOD__ . '() requires extension ICONV or MBSTRING, neither is loaded.');
79 } elseif ($length === null) {
80 $length = self::length($s);
81 } elseif ($start < 0 && $length < 0) {
82 $start += self::length($s);
83 // unifies iconv_substr behavior with mb_substr
84 }
85 return \iconv_substr($s, $start, $length, 'UTF-8');
86 }
87 /**
88 * Removes control characters, normalizes line breaks to `\n`, removes leading and trailing blank lines,
89 * trims end spaces on lines, normalizes UTF-8 to the normal form of NFC.
90 */
91 public static function normalize(string $s) : string
92 {
93 // convert to compressed normal form (NFC)
94 if (\class_exists('Normalizer', \false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== \false) {
95 $s = $n;
96 }
97 $s = self::normalizeNewLines($s);
98 // remove control characters; leave \t + \n
99 $s = self::pcre('preg_replace', ['#[\\x00-\\x08\\x0B-\\x1F\\x7F-\\x9F]+#u', '', $s]);
100 // right trim
101 $s = self::pcre('preg_replace', ['#[\\t ]+$#m', '', $s]);
102 // leading and trailing blank lines
103 $s = \trim($s, "\n");
104 return $s;
105 }
106 /**
107 * Standardize line endings to unix-like.
108 */
109 public static function normalizeNewLines(string $s) : string
110 {
111 return \str_replace(["\r\n", "\r"], "\n", $s);
112 }
113 /**
114 * Converts UTF-8 string to ASCII, ie removes diacritics etc.
115 */
116 public static function toAscii(string $s) : string
117 {
118 $iconv = \defined('ICONV_IMPL') ? \trim(\ICONV_IMPL, '"\'') : null;
119 static $transliterator = null;
120 if ($transliterator === null) {
121 if (\class_exists('Transliterator', \false)) {
122 $transliterator = \Transliterator::create('Any-Latin; Latin-ASCII');
123 } else {
124 \trigger_error(__METHOD__ . "(): it is recommended to enable PHP extensions 'intl'.", \E_USER_NOTICE);
125 $transliterator = \false;
126 }
127 }
128 // remove control characters and check UTF-8 validity
129 $s = self::pcre('preg_replace', ['#[^\\x09\\x0A\\x0D\\x20-\\x7E\\xA0-\\x{2FF}\\x{370}-\\x{10FFFF}]#u', '', $s]);
130 // transliteration (by Transliterator and iconv) is not optimal, replace some characters directly
131 $s = \strtr($s, ["" => '"', "" => '"', "" => '"', "" => "'", "" => "'", "" => "'", "°" => '^', "Я" => 'Ya', "я" => 'ya', "Ю" => 'Yu', "ю" => 'yu', "Ä" => 'Ae', "Ö" => 'Oe', "Ü" => 'Ue', "" => 'Ss', "ä" => 'ae', "ö" => 'oe', "ü" => 'ue', "ß" => 'ss']);
132 // „ “ ” ‚ ‘ ’ ° Я я Ю ю Ä Ö Ü ẞ ä ö ü ß
133 if ($iconv !== 'libiconv') {
134 $s = \strtr($s, ["®" => '(R)', "©" => '(c)', "" => '...', "«" => '<<', "»" => '>>', "£" => 'lb', "¥" => 'yen', "²" => '^2', "³" => '^3', "µ" => 'u', "¹" => '^1', "º" => 'o', "¿" => '?', "ˊ" => "'", "ˍ" => '_', "˝" => '"', "" => '', "" => 'EUR', "" => 'TM', "" => 'e', "" => '<-', "" => '^', "" => '->', "" => 'V', "" => '<->']);
135 // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ ` € ™ ℮ ← ↑ → ↓ ↔
136 }
137 if ($transliterator) {
138 $s = $transliterator->transliterate($s);
139 // use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ
140 if ($iconv === 'glibc') {
141 $s = \strtr($s, '?', "\x01");
142 // temporarily hide ? to distinguish them from the garbage that iconv creates
143 $s = \iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
144 $s = \str_replace(['?', "\x01"], ['', '?'], $s);
145 // remove garbage and restore ? characters
146 } elseif ($iconv === 'libiconv') {
147 $s = \iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
148 } else {
149 // null or 'unknown' (#216)
150 $s = self::pcre('preg_replace', ['#[^\\x00-\\x7F]++#', '', $s]);
151 // remove non-ascii chars
152 }
153 } elseif ($iconv === 'glibc' || $iconv === 'libiconv') {
154 // temporarily hide these characters to distinguish them from the garbage that iconv creates
155 $s = \strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06");
156 if ($iconv === 'glibc') {
157 // glibc implementation is very limited. transliterate into Windows-1250 and then into ASCII, so most Eastern European characters are preserved
158 $s = \iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s);
159 $s = \strtr($s, "\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96\xa0\x8b\x97\x9b\xa6\xad\xb7", 'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.');
160 $s = self::pcre('preg_replace', ['#[^\\x00-\\x7F]++#', '', $s]);
161 } else {
162 $s = \iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
163 }
164 // remove garbage that iconv creates during transliteration (eg Ý -> Y')
165 $s = \str_replace(['`', "'", '"', '^', '~', '?'], '', $s);
166 // restore temporarily hidden characters
167 $s = \strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?');
168 } else {
169 $s = self::pcre('preg_replace', ['#[^\\x00-\\x7F]++#', '', $s]);
170 // remove non-ascii chars
171 }
172 return $s;
173 }
174 /**
175 * Modifies the UTF-8 string to the form used in the URL, ie removes diacritics and replaces all characters
176 * except letters of the English alphabet and numbers with a hyphens.
177 */
178 public static function webalize(string $s, ?string $charlist = null, bool $lower = \true) : string
179 {
180 $s = self::toAscii($s);
181 if ($lower) {
182 $s = \strtolower($s);
183 }
184 $s = self::pcre('preg_replace', ['#[^a-z0-9' . ($charlist !== null ? \preg_quote($charlist, '#') : '') . ']+#i', '-', $s]);
185 $s = \trim($s, '-');
186 return $s;
187 }
188 /**
189 * Truncates a UTF-8 string to given maximal length, while trying not to split whole words. Only if the string is truncated,
190 * an ellipsis (or something else set with third argument) is appended to the string.
191 */
192 public static function truncate(string $s, int $maxLen, string $append = "") : string
193 {
194 if (self::length($s) > $maxLen) {
195 $maxLen -= self::length($append);
196 if ($maxLen < 1) {
197 return $append;
198 } elseif ($matches = self::match($s, '#^.{1,' . $maxLen . '}(?=[\\s\\x00-/:-@\\[-`{-~])#us')) {
199 return $matches[0] . $append;
200 } else {
201 return self::substring($s, 0, $maxLen) . $append;
202 }
203 }
204 return $s;
205 }
206 /**
207 * Indents a multiline text from the left. Second argument sets how many indentation chars should be used,
208 * while the indent itself is the third argument (*tab* by default).
209 */
210 public static function indent(string $s, int $level = 1, string $chars = "\t") : string
211 {
212 if ($level > 0) {
213 $s = self::replace($s, '#(?:^|[\\r\\n]+)(?=[^\\r\\n])#', '$0' . \str_repeat($chars, $level));
214 }
215 return $s;
216 }
217 /**
218 * Converts all characters of UTF-8 string to lower case.
219 */
220 public static function lower(string $s) : string
221 {
222 return \mb_strtolower($s, 'UTF-8');
223 }
224 /**
225 * Converts the first character of a UTF-8 string to lower case and leaves the other characters unchanged.
226 */
227 public static function firstLower(string $s) : string
228 {
229 return self::lower(self::substring($s, 0, 1)) . self::substring($s, 1);
230 }
231 /**
232 * Converts all characters of a UTF-8 string to upper case.
233 */
234 public static function upper(string $s) : string
235 {
236 return \mb_strtoupper($s, 'UTF-8');
237 }
238 /**
239 * Converts the first character of a UTF-8 string to upper case and leaves the other characters unchanged.
240 */
241 public static function firstUpper(string $s) : string
242 {
243 return self::upper(self::substring($s, 0, 1)) . self::substring($s, 1);
244 }
245 /**
246 * Converts the first character of every word of a UTF-8 string to upper case and the others to lower case.
247 */
248 public static function capitalize(string $s) : string
249 {
250 return \mb_convert_case($s, \MB_CASE_TITLE, 'UTF-8');
251 }
252 /**
253 * Compares two UTF-8 strings or their parts, without taking character case into account. If length is null, whole strings are compared,
254 * if it is negative, the corresponding number of characters from the end of the strings is compared,
255 * otherwise the appropriate number of characters from the beginning is compared.
256 */
257 public static function compare(string $left, string $right, ?int $length = null) : bool
258 {
259 if (\class_exists('Normalizer', \false)) {
260 $left = \Normalizer::normalize($left, \Normalizer::FORM_D);
261 // form NFD is faster
262 $right = \Normalizer::normalize($right, \Normalizer::FORM_D);
263 // form NFD is faster
264 }
265 if ($length < 0) {
266 $left = self::substring($left, $length, -$length);
267 $right = self::substring($right, $length, -$length);
268 } elseif ($length !== null) {
269 $left = self::substring($left, 0, $length);
270 $right = self::substring($right, 0, $length);
271 }
272 return self::lower($left) === self::lower($right);
273 }
274 /**
275 * Finds the common prefix of strings or returns empty string if the prefix was not found.
276 * @param string[] $strings
277 */
278 public static function findPrefix(array $strings) : string
279 {
280 $first = \array_shift($strings);
281 for ($i = 0; $i < strlen($first); $i++) {
282 foreach ($strings as $s) {
283 if (!isset($s[$i]) || $first[$i] !== $s[$i]) {
284 while ($i && $first[$i - 1] >= "\x80" && $first[$i] >= "\x80" && $first[$i] < "\xc0") {
285 $i--;
286 }
287 return \substr($first, 0, $i);
288 }
289 }
290 }
291 return $first;
292 }
293 /**
294 * Returns number of characters (not bytes) in UTF-8 string.
295 * That is the number of Unicode code points which may differ from the number of graphemes.
296 */
297 public static function length(string $s) : int
298 {
299 return \function_exists('mb_strlen') ? \mb_strlen($s, 'UTF-8') : strlen(\utf8_decode($s));
300 }
301 /**
302 * Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string.
303 */
304 public static function trim(string $s, string $charlist = self::TRIM_CHARACTERS) : string
305 {
306 $charlist = \preg_quote($charlist, '#');
307 return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', '');
308 }
309 /**
310 * Pads a UTF-8 string to given length by prepending the $pad string to the beginning.
311 */
312 public static function padLeft(string $s, int $length, string $pad = ' ') : string
313 {
314 $length = \max(0, $length - self::length($s));
315 $padLen = self::length($pad);
316 return \str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s;
317 }
318 /**
319 * Pads UTF-8 string to given length by appending the $pad string to the end.
320 */
321 public static function padRight(string $s, int $length, string $pad = ' ') : string
322 {
323 $length = \max(0, $length - self::length($s));
324 $padLen = self::length($pad);
325 return $s . \str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen);
326 }
327 /**
328 * Reverses UTF-8 string.
329 */
330 public static function reverse(string $s) : string
331 {
332 if (!\extension_loaded('iconv')) {
333 throw new \Packetery\Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
334 }
335 return \iconv('UTF-32LE', 'UTF-8', \strrev(\iconv('UTF-8', 'UTF-32BE', $s)));
336 }
337 /**
338 * Returns part of $haystack before $nth occurence of $needle or returns null if the needle was not found.
339 * Negative value means searching from the end.
340 */
341 public static function before(string $haystack, string $needle, int $nth = 1) : ?string
342 {
343 $pos = self::pos($haystack, $needle, $nth);
344 return $pos === null ? null : \substr($haystack, 0, $pos);
345 }
346 /**
347 * Returns part of $haystack after $nth occurence of $needle or returns null if the needle was not found.
348 * Negative value means searching from the end.
349 */
350 public static function after(string $haystack, string $needle, int $nth = 1) : ?string
351 {
352 $pos = self::pos($haystack, $needle, $nth);
353 return $pos === null ? null : \substr($haystack, $pos + strlen($needle));
354 }
355 /**
356 * Returns position in characters of $nth occurence of $needle in $haystack or null if the $needle was not found.
357 * Negative value of `$nth` means searching from the end.
358 */
359 public static function indexOf(string $haystack, string $needle, int $nth = 1) : ?int
360 {
361 $pos = self::pos($haystack, $needle, $nth);
362 return $pos === null ? null : self::length(\substr($haystack, 0, $pos));
363 }
364 /**
365 * Returns position in characters of $nth occurence of $needle in $haystack or null if the needle was not found.
366 */
367 private static function pos(string $haystack, string $needle, int $nth = 1) : ?int
368 {
369 if (!$nth) {
370 return null;
371 } elseif ($nth > 0) {
372 if ($needle === '') {
373 return 0;
374 }
375 $pos = 0;
376 while (($pos = \strpos($haystack, $needle, $pos)) !== \false && --$nth) {
377 $pos++;
378 }
379 } else {
380 $len = strlen($haystack);
381 if ($needle === '') {
382 return $len;
383 } elseif ($len === 0) {
384 return null;
385 }
386 $pos = $len - 1;
387 while (($pos = \strrpos($haystack, $needle, $pos - $len)) !== \false && ++$nth) {
388 $pos--;
389 }
390 }
391 return Helpers::falseToNull($pos);
392 }
393 /**
394 * Splits a string into array by the regular expression. Parenthesized expression in the delimiter are captured.
395 * Parameter $flags can be any combination of PREG_SPLIT_NO_EMPTY and PREG_OFFSET_CAPTURE flags.
396 */
397 public static function split(string $subject, string $pattern, int $flags = 0) : array
398 {
399 return self::pcre('preg_split', [$pattern, $subject, -1, $flags | \PREG_SPLIT_DELIM_CAPTURE]);
400 }
401 /**
402 * Checks if given string matches a regular expression pattern and returns an array with first found match and each subpattern.
403 * Parameter $flags can be any combination of PREG_OFFSET_CAPTURE and PREG_UNMATCHED_AS_NULL flags.
404 */
405 public static function match(string $subject, string $pattern, int $flags = 0, int $offset = 0) : ?array
406 {
407 if ($offset > strlen($subject)) {
408 return null;
409 }
410 return self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset]) ? $m : null;
411 }
412 /**
413 * Finds all occurrences matching regular expression pattern and returns a two-dimensional array. Result is array of matches (ie uses by default PREG_SET_ORDER).
414 * Parameter $flags can be any combination of PREG_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL and PREG_PATTERN_ORDER flags.
415 */
416 public static function matchAll(string $subject, string $pattern, int $flags = 0, int $offset = 0) : array
417 {
418 if ($offset > strlen($subject)) {
419 return [];
420 }
421 self::pcre('preg_match_all', [$pattern, $subject, &$m, $flags & \PREG_PATTERN_ORDER ? $flags : $flags | \PREG_SET_ORDER, $offset]);
422 return $m;
423 }
424 /**
425 * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`.
426 * @param string|array $pattern
427 * @param string|callable $replacement
428 */
429 public static function replace(string $subject, $pattern, $replacement = '', int $limit = -1) : string
430 {
431 if (is_object($replacement) || is_array($replacement)) {
432 if (!\is_callable($replacement, \false, $textual)) {
433 throw new \Packetery\Nette\InvalidStateException("Callback '{$textual}' is not callable.");
434 }
435 return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit]);
436 } elseif (is_array($pattern) && \is_string(\key($pattern))) {
437 $replacement = \array_values($pattern);
438 $pattern = \array_keys($pattern);
439 }
440 return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]);
441 }
442 /** @internal */
443 public static function pcre(string $func, array $args)
444 {
445 $res = Callback::invokeSafe($func, $args, function (string $message) use($args) : void {
446 // compile-time error, not detectable by preg_last_error
447 throw new RegexpException($message . ' in pattern: ' . \implode(' or ', (array) $args[0]));
448 });
449 if (($code = \preg_last_error()) && ($res === null || !\in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], \true))) {
450 throw new RegexpException((RegexpException::MESSAGES[$code] ?? 'Unknown error') . ' (pattern: ' . \implode(' or ', (array) $args[0]) . ')', $code);
451 }
452 return $res;
453 }
454 }
455