Str.php 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Stringable;
  12. use Closure;
  13. use Countable;
  14. use DateTimeInterface;
  15. use Hyperf\Collection\Arr;
  16. use Hyperf\Collection\Collection;
  17. use Hyperf\Macroable\Macroable;
  18. use InvalidArgumentException;
  19. use JsonException;
  20. use Ramsey\Uuid\Uuid;
  21. use Ramsey\Uuid\UuidInterface;
  22. use RuntimeException;
  23. use Symfony\Component\Uid\Ulid;
  24. use function Hyperf\Collection\collect;
  25. /**
  26. * Most of the methods in this file come from illuminate/support,
  27. * thanks Laravel Team provide such a useful class.
  28. */
  29. class Str
  30. {
  31. use Macroable;
  32. /**
  33. * Get a new stringable object from the given string.
  34. *
  35. * @param string $string
  36. * @return Stringable
  37. */
  38. public static function of($string)
  39. {
  40. return new Stringable($string);
  41. }
  42. /**
  43. * Return the remainder of a string after a given value.
  44. *
  45. * @param string $subject
  46. * @param string $search
  47. * @return string
  48. */
  49. public static function after($subject, $search)
  50. {
  51. return $search === '' ? $subject : array_reverse(explode($search, $subject, 2))[0];
  52. }
  53. /**
  54. * Return the remainder of a string after the last occurrence of a given value.
  55. *
  56. * @param string $subject
  57. * @param string $search
  58. * @return string
  59. */
  60. public static function afterLast($subject, $search)
  61. {
  62. if ($search === '') {
  63. return $subject;
  64. }
  65. $position = strrpos($subject, (string) $search);
  66. if ($position === false) {
  67. return $subject;
  68. }
  69. return substr($subject, $position + strlen($search));
  70. }
  71. /**
  72. * Convert the given string to APA-style title case.
  73. *
  74. * See: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case
  75. *
  76. * @param string $value
  77. * @return string
  78. */
  79. public static function apa($value)
  80. {
  81. if (trim($value) === '') {
  82. return $value;
  83. }
  84. $minorWords = [
  85. 'and', 'as', 'but', 'for', 'if', 'nor', 'or', 'so', 'yet', 'a', 'an',
  86. 'the', 'at', 'by', 'for', 'in', 'of', 'off', 'on', 'per', 'to', 'up', 'via',
  87. ];
  88. $endPunctuation = ['.', '!', '?', ':', '—', ','];
  89. $words = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY);
  90. $words[0] = ucfirst(mb_strtolower($words[0]));
  91. for ($i = 0; $i < count($words); ++$i) {
  92. $lowercaseWord = mb_strtolower($words[$i]);
  93. if (str_contains($lowercaseWord, '-')) {
  94. $hyphenatedWords = explode('-', $lowercaseWord);
  95. $hyphenatedWords = array_map(function ($part) use ($minorWords) {
  96. return (in_array($part, $minorWords) && mb_strlen($part) <= 3) ? $part : ucfirst($part);
  97. }, $hyphenatedWords);
  98. $words[$i] = implode('-', $hyphenatedWords);
  99. } else {
  100. if (in_array($lowercaseWord, $minorWords)
  101. && mb_strlen($lowercaseWord) <= 3
  102. && ! ($i === 0 || in_array(mb_substr($words[$i - 1], -1), $endPunctuation))) {
  103. $words[$i] = $lowercaseWord;
  104. } else {
  105. $words[$i] = ucfirst($lowercaseWord);
  106. }
  107. }
  108. }
  109. return implode(' ', $words);
  110. }
  111. /**
  112. * Transliterate a UTF-8 value to ASCII.
  113. *
  114. * @param string $value
  115. * @param string $language
  116. * @return string
  117. */
  118. public static function ascii($value, $language = 'en')
  119. {
  120. $languageSpecific = static::languageSpecificCharsArray($language);
  121. if (! is_null($languageSpecific)) {
  122. $value = str_replace($languageSpecific[0], $languageSpecific[1], $value);
  123. }
  124. foreach (static::charsArray() as $key => $val) {
  125. $value = str_replace($val, (string) $key, $value);
  126. }
  127. return preg_replace('/[^\x20-\x7E]/u', '', $value);
  128. }
  129. /**
  130. * Get the portion of a string before a given value.
  131. *
  132. * @param string $subject
  133. * @param string $search
  134. * @return string
  135. */
  136. public static function before($subject, $search)
  137. {
  138. return $search === '' ? $subject : explode($search, $subject)[0];
  139. }
  140. /**
  141. * Get the portion of a string before the last occurrence of a given value.
  142. *
  143. * @param string $subject
  144. * @param string $search
  145. * @return string
  146. */
  147. public static function beforeLast($subject, $search)
  148. {
  149. if ($search === '') {
  150. return $subject;
  151. }
  152. $pos = mb_strrpos($subject, $search);
  153. if ($pos === false) {
  154. return $subject;
  155. }
  156. return static::substr($subject, 0, $pos);
  157. }
  158. /**
  159. * Get the portion of a string between two given values.
  160. *
  161. * @param string $subject
  162. * @param string $from
  163. * @param string $to
  164. * @return string
  165. */
  166. public static function between($subject, $from, $to)
  167. {
  168. if ($from === '' || $to === '') {
  169. return $subject;
  170. }
  171. return static::beforeLast(static::after($subject, $from), $to);
  172. }
  173. /**
  174. * Convert a value to camel case.
  175. *
  176. * @param string $value
  177. * @return string
  178. */
  179. public static function camel($value)
  180. {
  181. return lcfirst(static::studly($value));
  182. }
  183. /**
  184. * Get the character at the specified index.
  185. *
  186. * @param string $subject
  187. * @param int $index
  188. * @return null|string
  189. */
  190. public static function charAt($subject, $index)
  191. {
  192. $length = mb_strlen($subject);
  193. if ($index < 0 ? $index < -$length : $index > $length - 1) {
  194. return null;
  195. }
  196. return mb_substr($subject, $index, 1);
  197. }
  198. /**
  199. * Determine if a given string contains a given substring.
  200. *
  201. * @param array|string $needles
  202. */
  203. public static function contains(string $haystack, mixed $needles, bool $ignoreCase = false): bool
  204. {
  205. if ($ignoreCase) {
  206. return static::containsIgnoreCase($haystack, $needles);
  207. }
  208. foreach ((array) $needles as $needle) {
  209. $needle = (string) $needle;
  210. if ($needle !== '' && str_contains($haystack, $needle)) {
  211. return true;
  212. }
  213. }
  214. return false;
  215. }
  216. /**
  217. * Determine if a given string contains a given substring regardless of case sensitivity.
  218. *
  219. * @param array|string $needles
  220. */
  221. public static function containsIgnoreCase(string $haystack, $needles): bool
  222. {
  223. foreach ((array) $needles as $needle) {
  224. $needle = (string) $needle;
  225. if ($needle !== '' && stripos($haystack, $needle) !== false) {
  226. return true;
  227. }
  228. }
  229. return false;
  230. }
  231. /**
  232. * Determine if a given string contains all array values.
  233. *
  234. * @param string[] $needles
  235. * @return bool
  236. */
  237. public static function containsAll(string $haystack, array $needles, bool $ignoreCase = false)
  238. {
  239. foreach ($needles as $needle) {
  240. if (! static::contains($haystack, $needle, $ignoreCase)) {
  241. return false;
  242. }
  243. }
  244. return true;
  245. }
  246. /**
  247. * Determine if a given string ends with a given substring.
  248. *
  249. * @param array|string $needles
  250. * @return bool
  251. */
  252. public static function endsWith(string $haystack, $needles)
  253. {
  254. foreach ((array) $needles as $needle) {
  255. $needle = (string) $needle;
  256. if ($needle !== '' && str_ends_with($haystack, $needle)) {
  257. return true;
  258. }
  259. }
  260. return false;
  261. }
  262. /**
  263. * Cap a string with a single instance of a given value.
  264. *
  265. * @param string $value
  266. * @param string $cap
  267. * @return string
  268. */
  269. public static function finish($value, $cap)
  270. {
  271. $quoted = preg_quote($cap, '/');
  272. return preg_replace('/(?:' . $quoted . ')+$/u', '', $value) . $cap;
  273. }
  274. /**
  275. * Determine if a given string matches a given pattern.
  276. *
  277. * @param array|string $pattern
  278. * @param string $value
  279. * @return bool
  280. */
  281. public static function is($pattern, $value)
  282. {
  283. $patterns = Arr::wrap($pattern);
  284. if (empty($patterns)) {
  285. return false;
  286. }
  287. foreach ($patterns as $pattern) {
  288. // If the given value is an exact match we can of course return true right
  289. // from the beginning. Otherwise, we will translate asterisks and do an
  290. // actual pattern match against the two strings to see if they match.
  291. if ($pattern == $value) {
  292. return true;
  293. }
  294. $pattern = preg_quote($pattern, '#');
  295. // Asterisks are translated into zero-or-more regular expression wildcards
  296. // to make it convenient to check if the strings starts with the given
  297. // pattern such as "library/*", making any string check convenient.
  298. $pattern = str_replace('\*', '.*', $pattern);
  299. if (preg_match('#^' . $pattern . '\z#u', $value) === 1) {
  300. return true;
  301. }
  302. }
  303. return false;
  304. }
  305. /**
  306. * Determine if a given string is 7 bit ASCII.
  307. */
  308. public static function isAscii(string $value): bool
  309. {
  310. if ($value == '') {
  311. return true;
  312. }
  313. return ! preg_match('/[^\x09\x0A\x0D\x20-\x7E]/', $value);
  314. }
  315. /**
  316. * Convert a string to kebab case.
  317. *
  318. * @param string $value
  319. * @return string
  320. */
  321. public static function kebab($value)
  322. {
  323. return static::snake($value, '-');
  324. }
  325. /**
  326. * Return the length of the given string.
  327. *
  328. * @param string $value
  329. * @param string $encoding
  330. * @return int
  331. */
  332. public static function length($value, $encoding = null)
  333. {
  334. return mb_strlen($value, $encoding);
  335. }
  336. /**
  337. * Limit the number of characters in a string.
  338. *
  339. * @param string $value
  340. * @param int $limit
  341. * @param string $end
  342. * @return string
  343. */
  344. public static function limit($value, $limit = 100, $end = '...')
  345. {
  346. if (mb_strwidth($value, 'UTF-8') <= $limit) {
  347. return $value;
  348. }
  349. return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')) . $end;
  350. }
  351. /**
  352. * Convert the given string to lower-case.
  353. *
  354. * @param string $value
  355. * @return string
  356. */
  357. public static function lower($value)
  358. {
  359. return mb_strtolower($value, 'UTF-8');
  360. }
  361. /**
  362. * Limit the number of words in a string.
  363. */
  364. public static function words(string $value, int $words = 100, string $end = '...'): string
  365. {
  366. preg_match('/^\s*+(?:\S++\s*+){1,' . $words . '}/u', $value, $matches);
  367. if (! isset($matches[0]) || static::length($value) === static::length($matches[0])) {
  368. return $value;
  369. }
  370. return rtrim($matches[0]) . $end;
  371. }
  372. /**
  373. * Get the string matching the given pattern.
  374. *
  375. * @param string $pattern
  376. * @param string $subject
  377. * @return string
  378. */
  379. public static function match($pattern, $subject)
  380. {
  381. preg_match($pattern, $subject, $matches);
  382. if (! $matches) {
  383. return '';
  384. }
  385. return $matches[1] ?? $matches[0];
  386. }
  387. /**
  388. * Determine if a given string matches a given pattern.
  389. *
  390. * @param iterable<string>|string $patterns
  391. * @param string $value
  392. * @return bool
  393. */
  394. public static function isMatch($patterns, $value)
  395. {
  396. $value = (string) $value;
  397. if (! is_iterable($patterns)) {
  398. $patterns = [$patterns];
  399. }
  400. foreach ($patterns as $pattern) {
  401. $pattern = (string) $pattern;
  402. if (preg_match($pattern, $value) === 1) {
  403. return true;
  404. }
  405. }
  406. return false;
  407. }
  408. /**
  409. * Get the string matching the given pattern.
  410. *
  411. * @param string $pattern
  412. * @param string $subject
  413. * @return Collection
  414. */
  415. public static function matchAll($pattern, $subject)
  416. {
  417. preg_match_all($pattern, $subject, $matches);
  418. if (empty($matches[0])) {
  419. return collect();
  420. }
  421. return collect($matches[1] ?? $matches[0]);
  422. }
  423. /**
  424. * Pad both sides of a string with another.
  425. *
  426. * @param string $value
  427. * @param int $length
  428. * @param string $pad
  429. * @return string
  430. */
  431. public static function padBoth($value, $length, $pad = ' ')
  432. {
  433. return str_pad($value, strlen($value) - mb_strlen($value) + $length, $pad, STR_PAD_BOTH);
  434. }
  435. /**
  436. * Pad the left side of a string with another.
  437. *
  438. * @param string $value
  439. * @param int $length
  440. * @param string $pad
  441. * @return string
  442. */
  443. public static function padLeft($value, $length, $pad = ' ')
  444. {
  445. return str_pad($value, strlen($value) - mb_strlen($value) + $length, $pad, STR_PAD_LEFT);
  446. }
  447. /**
  448. * Pad the right side of a string with another.
  449. *
  450. * @param string $value
  451. * @param int $length
  452. * @param string $pad
  453. * @return string
  454. */
  455. public static function padRight($value, $length, $pad = ' ')
  456. {
  457. return str_pad($value, strlen($value) - mb_strlen($value) + $length, $pad, STR_PAD_RIGHT);
  458. }
  459. /**
  460. * Parse a Class@method style callback into class and method.
  461. *
  462. * @param null|string $default
  463. */
  464. public static function parseCallback(string $callback, $default = null): array
  465. {
  466. return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default];
  467. }
  468. /**
  469. * Pluralize the last word of an English, studly caps case string.
  470. * @param mixed $count
  471. */
  472. public static function pluralStudly(string $value, $count = 2): string
  473. {
  474. $parts = preg_split('/(.)(?=[A-Z])/u', $value, -1, PREG_SPLIT_DELIM_CAPTURE);
  475. $lastWord = array_pop($parts);
  476. return implode('', $parts) . self::plural($lastWord, $count);
  477. }
  478. /**
  479. * Get the plural form of an English word.
  480. */
  481. public static function plural(string $value, array|Countable|int $count = 2): string
  482. {
  483. return Pluralizer::plural($value, $count);
  484. }
  485. /**
  486. * Find the multi-byte safe position of the first occurrence of a given substring in a string.
  487. */
  488. public static function position(string $haystack, string $needle, int $offset = 0, ?string $encoding = null): false|int
  489. {
  490. return mb_strpos($haystack, (string) $needle, $offset, $encoding);
  491. }
  492. /**
  493. * Generate a more truly "random" alpha-numeric string.
  494. */
  495. public static function random(int $length = 16): string
  496. {
  497. $string = '';
  498. while (($len = strlen($string)) < $length) {
  499. $size = $length - $len;
  500. $bytes = random_bytes($size);
  501. $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);
  502. }
  503. return $string;
  504. }
  505. /**
  506. * Repeat the given string.
  507. *
  508. * @return string
  509. */
  510. public static function repeat(string $string, int $times)
  511. {
  512. return str_repeat($string, $times);
  513. }
  514. /**
  515. * Replace a given value in the string sequentially with an array.
  516. *
  517. * @param string[] $replace
  518. */
  519. public static function replaceArray(string $search, array $replace, string $subject): string
  520. {
  521. foreach ($replace as $value) {
  522. $subject = static::replaceFirst($search, (string) $value, $subject);
  523. }
  524. return $subject;
  525. }
  526. /**
  527. * Replace the given value in the given string.
  528. *
  529. * @param string|string[] $search
  530. * @param string|string[] $replace
  531. * @param string|string[] $subject
  532. * @return string
  533. */
  534. public static function replace($search, $replace, $subject)
  535. {
  536. return str_replace($search, $replace, $subject);
  537. }
  538. /**
  539. * Replace the first occurrence of a given value in the string.
  540. */
  541. public static function replaceFirst(string $search, string $replace, string $subject): string
  542. {
  543. if ($search == '') {
  544. return $subject;
  545. }
  546. $position = strpos($subject, $search);
  547. if ($position !== false) {
  548. return substr_replace($subject, $replace, $position, strlen($search));
  549. }
  550. return $subject;
  551. }
  552. /**
  553. * Replace the last occurrence of a given value in the string.
  554. */
  555. public static function replaceLast(string $search, string $replace, string $subject): string
  556. {
  557. if ($search == '') {
  558. return $subject;
  559. }
  560. $position = strrpos($subject, $search);
  561. if ($position !== false) {
  562. return substr_replace($subject, $replace, $position, strlen($search));
  563. }
  564. return $subject;
  565. }
  566. /**
  567. * Remove any occurrence of the given string in the subject.
  568. *
  569. * @param array<string>|string $search
  570. * @param string $subject
  571. * @param bool $caseSensitive
  572. * @return string
  573. */
  574. public static function remove($search, $subject, $caseSensitive = true)
  575. {
  576. return $caseSensitive
  577. ? str_replace($search, '', $subject)
  578. : str_ireplace($search, '', $subject);
  579. }
  580. /**
  581. * Begin a string with a single instance of a given value.
  582. */
  583. public static function start(string $value, string $prefix): string
  584. {
  585. $quoted = preg_quote($prefix, '/');
  586. return $prefix . preg_replace('/^(?:' . $quoted . ')+/u', '', $value);
  587. }
  588. /**
  589. * Strip HTML and PHP tags from the given string.
  590. *
  591. * @param null|string|string[] $allowedTags
  592. */
  593. public static function stripTags(string $value, $allowedTags = null): string
  594. {
  595. return strip_tags($value, $allowedTags);
  596. }
  597. /**
  598. * Convert the given string to upper-case.
  599. */
  600. public static function upper(string $value): string
  601. {
  602. return mb_strtoupper($value, 'UTF-8');
  603. }
  604. /**
  605. * Convert the given string to title case.
  606. */
  607. public static function title(string $value): string
  608. {
  609. return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
  610. }
  611. /**
  612. * Convert the given string to proper case for each word.
  613. */
  614. public static function headline(string $value): string
  615. {
  616. $parts = explode(' ', $value);
  617. $parts = count($parts) > 1
  618. ? array_map([static::class, 'title'], $parts)
  619. : array_map([static::class, 'title'], static::ucsplit(implode('_', $parts)));
  620. $collapsed = static::replace(['-', '_', ' '], '_', implode('_', $parts));
  621. return implode(' ', array_filter(explode('_', $collapsed)));
  622. }
  623. /**
  624. * Get the singular form of an English word.
  625. */
  626. public static function singular(string $value): string
  627. {
  628. return Pluralizer::singular($value);
  629. }
  630. /**
  631. * Generate a URL friendly "slug" from a given string.
  632. * @param mixed $dictionary
  633. */
  634. public static function slug(string $title, string $separator = '-', ?string $language = 'en', $dictionary = ['@' => 'at']): string
  635. {
  636. $title = $language ? static::ascii($title, $language) : $title;
  637. // Convert all dashes/underscores into separator
  638. $flip = $separator === '-' ? '_' : '-';
  639. $title = preg_replace('![' . preg_quote($flip) . ']+!u', $separator, $title);
  640. // Replace dictionary words
  641. foreach ($dictionary as $key => $value) {
  642. $dictionary[$key] = $separator . $value . $separator;
  643. }
  644. $title = str_replace(array_keys($dictionary), array_values($dictionary), $title);
  645. // Remove all characters that are not the separator, letters, numbers, or whitespace.
  646. $title = preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', mb_strtolower($title));
  647. // Replace all separator characters and whitespace by a single separator
  648. $title = preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title);
  649. return trim($title, $separator);
  650. }
  651. /**
  652. * Convert a string to snake case.
  653. */
  654. public static function snake(string $value, string $delimiter = '_'): string
  655. {
  656. if (! ctype_lower($value)) {
  657. $value = preg_replace('/\s+/u', '', ucwords($value));
  658. $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value));
  659. }
  660. return $value;
  661. }
  662. /**
  663. * Determine if a given string starts with a given substring.
  664. *
  665. * @param array|string $needles
  666. */
  667. public static function startsWith(string $haystack, $needles): bool
  668. {
  669. foreach ((array) $needles as $needle) {
  670. $needle = (string) $needle;
  671. if ($needle !== '' && str_starts_with($haystack, $needle)) {
  672. return true;
  673. }
  674. }
  675. return false;
  676. }
  677. /**
  678. * Convert a value to studly caps case.
  679. */
  680. public static function studly(string $value, string $gap = ''): string
  681. {
  682. $value = ucwords(str_replace(['-', '_'], ' ', $value));
  683. return str_replace(' ', $gap, $value);
  684. }
  685. /**
  686. * Returns the portion of string specified by the start and length parameters.
  687. *
  688. * @param string $string
  689. * @param int $start
  690. * @param null|int $length
  691. * @return string
  692. */
  693. public static function substr($string, $start, $length = null)
  694. {
  695. return mb_substr($string, $start, $length, 'UTF-8');
  696. }
  697. /**
  698. * Returns the number of substring occurrences.
  699. *
  700. * @param string $haystack
  701. * @param string $needle
  702. * @param int $offset
  703. * @param null|int $length
  704. * @return int
  705. */
  706. public static function substrCount($haystack, $needle, $offset = 0, $length = null)
  707. {
  708. if (! is_null($length)) {
  709. return substr_count($haystack, $needle, $offset, $length);
  710. }
  711. return substr_count($haystack, $needle, $offset);
  712. }
  713. /**
  714. * Make a string's first character uppercase.
  715. */
  716. public static function ucfirst(string $string): string
  717. {
  718. return static::upper(static::substr($string, 0, 1)) . static::substr($string, 1);
  719. }
  720. /**
  721. * Replaces the first or the last ones chars from a string by a given char.
  722. *
  723. * @param int $offset if is negative it starts from the end
  724. * @param string $replacement default is *
  725. * @return string
  726. */
  727. public static function mask(string $string, int $offset = 0, int $length = 0, string $replacement = '*')
  728. {
  729. if ($length < 0) {
  730. throw new InvalidArgumentException('The length must equal or greater than zero.');
  731. }
  732. $stringLength = mb_strlen($string);
  733. $absOffset = abs($offset);
  734. if ($absOffset >= $stringLength) {
  735. return $string;
  736. }
  737. $hiddenLength = $length ?: $stringLength - $absOffset;
  738. if ($offset >= 0) {
  739. return mb_substr($string, 0, $offset) . str_repeat($replacement, $hiddenLength) . mb_substr($string, $offset + $hiddenLength);
  740. }
  741. return mb_substr($string, 0, max($stringLength - $hiddenLength - $absOffset, 0)) . str_repeat($replacement, $hiddenLength) . mb_substr($string, $offset);
  742. }
  743. /**
  744. * Determine if a given value is a valid ULID.
  745. *
  746. * @param mixed $value
  747. */
  748. public static function isUlid($value): bool
  749. {
  750. if (! is_string($value)) {
  751. return false;
  752. }
  753. if (strlen($value) !== 26) {
  754. return false;
  755. }
  756. if (strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz') !== 26) {
  757. return false;
  758. }
  759. return $value[0] <= '7';
  760. }
  761. /**
  762. * Generate a ULID.
  763. */
  764. public static function ulid(?DateTimeInterface $time = null): Ulid
  765. {
  766. if (! class_exists(Ulid::class)) {
  767. throw new RuntimeException('The "symfony/uid" package is required to use the "ulid" method. Please run "composer require symfony/uid".');
  768. }
  769. return new Ulid(Ulid::generate($time));
  770. }
  771. /**
  772. * Determine if a given value is a valid URL.
  773. *
  774. * @param string $value
  775. */
  776. public static function isUrl($value, array $protocols = []): bool
  777. {
  778. if (! is_string($value)) {
  779. return false;
  780. }
  781. $protocolList = empty($protocols)
  782. ? 'aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ark|attachment|aw|barion|beshare|bitcoin|bitcoincash|blob|bolo|browserext|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\+tcp|coap\+ws|coaps|coaps\+tcp|coaps\+ws|com-eventbrite-attendee|content|conti|crid|cvs|dab|data|dav|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|dpp|drm|drop|dtn|dvb|ed2k|elsi|example|facetime|fax|feed|feedready|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gizmoproject|go|gopher|graph|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|leaptofrogans|lorawan|lvlt|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|mongodb|moz|ms-access|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-lockscreencomponent-config|ms-media-stream-id|ms-mixedrealitycapture|ms-mobileplans|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|pack|palm|paparazzi|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|s3|secondlife|service|session|sftp|sgn|shttp|sieve|simpleledger|sip|sips|skype|smb|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|tg|things|thismessage|tip|tn3270|tool|ts3server|turn|turns|tv|udp|unreal|urn|ut2004|v-event|vemmi|ventrilo|videotex|vnc|view-source|wais|webcal|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s'
  783. : implode('|', $protocols);
  784. /*
  785. * This pattern is derived from Symfony\Component\Validator\Constraints\UrlValidator (5.0.7).
  786. *
  787. * (c) Fabien Potencier <fabien@symfony.com> http://symfony.com
  788. */
  789. $pattern = '~^
  790. (DEFAULT_PROTOCOLS):// # protocol
  791. (((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+)@)? # basic auth
  792. (
  793. ([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
  794. | # or
  795. \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
  796. | # or
  797. \[
  798. (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
  799. \] # an IPv6 address
  800. )
  801. (:[0-9]+)? # a port (optional)
  802. (?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})* )* # a path
  803. (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%[0-9A-Fa-f]{2})* )? # a query (optional)
  804. (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})* )? # a fragment (optional)
  805. $~ixu';
  806. return preg_match(str_replace('DEFAULT_PROTOCOLS', $protocolList, $pattern), $value) > 0;
  807. }
  808. /**
  809. * Determine if a given value is a valid UUID.
  810. *
  811. * @param mixed $value
  812. */
  813. public static function isUuid($value): bool
  814. {
  815. if (! is_string($value)) {
  816. return false;
  817. }
  818. return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0;
  819. }
  820. /**
  821. * Generate a UUID (version 4).
  822. */
  823. public static function uuid(): UuidInterface
  824. {
  825. if (! class_exists(Uuid::class)) {
  826. throw new RuntimeException('The "ramsey/uuid" package is required to use the "uuid" method. Please run "composer require ramsey/uuid".');
  827. }
  828. return Uuid::uuid4();
  829. }
  830. /**
  831. * Generate a time-ordered UUID.
  832. */
  833. public static function orderedUuid(?DateTimeInterface $time = null): UuidInterface
  834. {
  835. if (! class_exists(Uuid::class)) {
  836. throw new RuntimeException('The "ramsey/uuid" package is required to use the "orderedUuid" method. Please run "composer require ramsey/uuid".');
  837. }
  838. return Uuid::uuid7($time);
  839. }
  840. /**
  841. * Get the smallest possible portion of a string between two given values.
  842. *
  843. * @param string $subject
  844. * @param string $from
  845. * @param string $to
  846. * @return string
  847. */
  848. public static function betweenFirst($subject, $from, $to)
  849. {
  850. if ($from === '' || $to === '') {
  851. return $subject;
  852. }
  853. return Str::before(Str::after($subject, $from), $to);
  854. }
  855. /**
  856. * @param string $value
  857. */
  858. public static function classNamespace($value): string
  859. {
  860. if ($pos = strrpos($value, '\\')) {
  861. return substr($value, 0, $pos);
  862. }
  863. return '';
  864. }
  865. /**
  866. * Convert the case of a string.
  867. */
  868. public static function convertCase(string $string, int $mode = MB_CASE_FOLD, ?string $encoding = 'UTF-8'): string
  869. {
  870. return mb_convert_case($string, $mode, $encoding);
  871. }
  872. /**
  873. * Extracts an excerpt from text that matches the first instance of a phrase.
  874. *
  875. * @param string $text
  876. * @param string $phrase
  877. * @param array $options
  878. * @return null|string
  879. */
  880. public static function excerpt($text, $phrase = '', $options = [])
  881. {
  882. $radius = $options['radius'] ?? 100;
  883. $omission = $options['omission'] ?? '...';
  884. preg_match('/^(.*?)(' . preg_quote((string) $phrase) . ')(.*)$/iu', (string) $text, $matches);
  885. if (empty($matches)) {
  886. return null;
  887. }
  888. $startStr = ltrim($matches[1]);
  889. $start = Str::of(mb_substr($matches[1], max(mb_strlen($startStr, 'UTF-8') - $radius, 0), $radius, 'UTF-8'))->ltrim();
  890. $start = $start->unless(
  891. (fn ($startWithRadius) => $startWithRadius->exactly($startStr))($start),
  892. fn ($startWithRadius) => $startWithRadius->prepend($omission),
  893. );
  894. $endStr = rtrim($matches[3]);
  895. $end = Str::of(mb_substr($endStr, 0, $radius, 'UTF-8'))->rtrim();
  896. $end = $end->unless(
  897. (fn ($endWithRadius) => $endWithRadius->exactly($endStr))($end),
  898. fn ($endWithRadius) => $endWithRadius->append($omission),
  899. );
  900. return $start->append($matches[2], (string) $end)->__toString();
  901. }
  902. /**
  903. * Determine if a given value is valid JSON.
  904. *
  905. * @param mixed $value
  906. */
  907. public static function isJson($value): bool
  908. {
  909. if (! is_string($value)) {
  910. return false;
  911. }
  912. if (function_exists('json_validate')) {
  913. return json_validate($value, 512);
  914. }
  915. try {
  916. json_decode($value, true, 512, JSON_THROW_ON_ERROR);
  917. } catch (JsonException $e) {
  918. return false;
  919. }
  920. return true;
  921. }
  922. /**
  923. * Make a string's first character lowercase.
  924. *
  925. * @param string $string
  926. */
  927. public static function lcfirst($string): string
  928. {
  929. return Str::lower(Str::substr($string, 0, 1)) . Str::substr($string, 1);
  930. }
  931. /**
  932. * Generate a random, secure password.
  933. *
  934. * @param int $length
  935. * @param bool $letters
  936. * @param bool $numbers
  937. * @param bool $symbols
  938. * @param bool $spaces
  939. * @return string
  940. */
  941. public static function password($length = 32, $letters = true, $numbers = true, $symbols = true, $spaces = false)
  942. {
  943. return (new Collection())
  944. ->when($letters, fn ($c) => $c->merge([
  945. 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
  946. 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
  947. 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
  948. 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
  949. 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
  950. ]))
  951. ->when($numbers, fn ($c) => $c->merge([
  952. '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  953. ]))
  954. ->when($symbols, fn ($c) => $c->merge([
  955. '~', '!', '#', '$', '%', '^', '&', '*', '(', ')', '-',
  956. '_', '.', ',', '<', '>', '?', '/', '\\', '{', '}', '[',
  957. ']', '|', ':', ';',
  958. ]))
  959. ->when($spaces, fn ($c) => $c->merge([' ']))
  960. ->pipe(fn ($c) => Collection::times($length, fn () => $c[random_int(0, $c->count() - 1)]))
  961. ->implode('');
  962. }
  963. /**
  964. * Replace the first occurrence of the given value if it appears at the start of the string.
  965. *
  966. * @param string $search
  967. * @param string $replace
  968. * @param string $subject
  969. * @return string
  970. */
  971. public static function replaceStart($search, $replace, $subject)
  972. {
  973. $search = (string) $search;
  974. if ($search === '') {
  975. return $subject;
  976. }
  977. if (static::startsWith($subject, $search)) {
  978. return static::replaceFirst($search, $replace, $subject);
  979. }
  980. return $subject;
  981. }
  982. /**
  983. * Replace the last occurrence of a given value if it appears at the end of the string.
  984. *
  985. * @param string $search
  986. * @param string $replace
  987. * @param string $subject
  988. * @return string
  989. */
  990. public static function replaceEnd($search, $replace, $subject)
  991. {
  992. $search = (string) $search;
  993. if ($search === '') {
  994. return $subject;
  995. }
  996. if (static::endsWith($subject, $search)) {
  997. return static::replaceLast($search, $replace, $subject);
  998. }
  999. return $subject;
  1000. }
  1001. /**
  1002. * Replace the patterns matching the given regular expression.
  1003. *
  1004. * @param string $pattern
  1005. * @param Closure|string $replace
  1006. * @param array|string $subject
  1007. * @param int $limit
  1008. * @return null|string|string[]
  1009. */
  1010. public static function replaceMatches($pattern, $replace, $subject, $limit = -1)
  1011. {
  1012. if ($replace instanceof Closure) {
  1013. return preg_replace_callback($pattern, $replace, $subject, $limit);
  1014. }
  1015. return preg_replace($pattern, $replace, $subject, $limit);
  1016. }
  1017. /**
  1018. * @param string $value
  1019. */
  1020. public static function reverse($value): string
  1021. {
  1022. return implode(array_reverse(mb_str_split($value)));
  1023. }
  1024. /**
  1025. * Remove all whitespace from both ends of a string.
  1026. *
  1027. * @param string $value
  1028. * @param null|string $charlist
  1029. * @return string
  1030. */
  1031. public static function trim($value, $charlist = null)
  1032. {
  1033. if ($charlist === null) {
  1034. return preg_replace('~^[\s\x{FEFF}\x{200B}\x{200E}]+|[\s\x{FEFF}\x{200B}\x{200E}]+$~u', '', $value) ?? trim($value);
  1035. }
  1036. return trim($value, $charlist);
  1037. }
  1038. /**
  1039. * Remove all whitespace from the beginning of a string.
  1040. *
  1041. * @param string $value
  1042. * @param null|string $charlist
  1043. * @return string
  1044. */
  1045. public static function ltrim($value, $charlist = null)
  1046. {
  1047. if ($charlist === null) {
  1048. return preg_replace('~^[\s\x{FEFF}\x{200B}\x{200E}]+~u', '', $value) ?? ltrim($value);
  1049. }
  1050. return ltrim($value, $charlist);
  1051. }
  1052. /**
  1053. * Remove all whitespace from the end of a string.
  1054. *
  1055. * @param string $value
  1056. * @param null|string $charlist
  1057. * @return string
  1058. */
  1059. public static function rtrim($value, $charlist = null)
  1060. {
  1061. if ($charlist === null) {
  1062. return preg_replace('~[\s\x{FEFF}\x{200B}\x{200E}]+$~u', '', $value) ?? rtrim($value);
  1063. }
  1064. return rtrim($value, $charlist);
  1065. }
  1066. /**
  1067. * Remove all "extra" blank space from the given string.
  1068. *
  1069. * @param string $value
  1070. */
  1071. public static function squish($value): null|array|string
  1072. {
  1073. return preg_replace('~(\s|\x{3164}|\x{1160})+~u', ' ', static::trim($value));
  1074. }
  1075. /**
  1076. * Replace text within a portion of a string.
  1077. *
  1078. * @param string|string[] $string
  1079. * @param string|string[] $replace
  1080. * @param int|int[] $offset
  1081. * @param null|int|int[] $length
  1082. * @return string|string[]
  1083. */
  1084. public static function substrReplace($string, $replace, $offset = 0, $length = null): array|string
  1085. {
  1086. if ($length === null) {
  1087. $length = strlen($string);
  1088. }
  1089. return substr_replace($string, $replace, $offset, $length);
  1090. }
  1091. /**
  1092. * Swap multiple keywords in a string with other keywords.
  1093. *
  1094. * @param string $subject
  1095. * @return string
  1096. */
  1097. public static function swap(array $map, $subject): array|string
  1098. {
  1099. return str_replace(array_keys($map), array_values($map), $subject);
  1100. }
  1101. /**
  1102. * Take the first or last {$limit} characters of a string.
  1103. */
  1104. public static function take(string $string, int $limit): string
  1105. {
  1106. if ($limit < 0) {
  1107. return static::substr($string, $limit);
  1108. }
  1109. return static::substr($string, 0, $limit);
  1110. }
  1111. /**
  1112. * Convert the given string to Base64 encoding.
  1113. *
  1114. * @param string $string
  1115. */
  1116. public static function toBase64($string): string
  1117. {
  1118. return base64_encode($string);
  1119. }
  1120. /**
  1121. * Split a string into pieces by uppercase characters.
  1122. *
  1123. * @param string $string
  1124. * @return bool|string[]
  1125. */
  1126. public static function ucsplit($string): array|bool
  1127. {
  1128. return preg_split('/(?=\p{Lu})/u', $string, -1, PREG_SPLIT_NO_EMPTY);
  1129. }
  1130. /**
  1131. * Unwrap the string with the given strings.
  1132. *
  1133. * @param string $value
  1134. * @param string $before
  1135. * @param null|string $after
  1136. */
  1137. public static function unwrap($value, $before, $after = null): string
  1138. {
  1139. if (static::startsWith($value, $before)) {
  1140. $value = static::substr($value, static::length($before));
  1141. }
  1142. if (static::endsWith($value, $after ??= $before)) {
  1143. $value = static::substr($value, 0, -static::length($after));
  1144. }
  1145. return $value;
  1146. }
  1147. /**
  1148. * Get the number of words a string contains.
  1149. *
  1150. * @param string $string
  1151. */
  1152. public static function wordCount($string): array|int
  1153. {
  1154. return str_word_count($string);
  1155. }
  1156. /**
  1157. * Wrap the string with the given strings.
  1158. *
  1159. * @param string $value
  1160. * @param string $before
  1161. * @param null|string $after
  1162. */
  1163. public static function wrap($value, $before, $after = null): string
  1164. {
  1165. return $before . $value . ($after ??= $before);
  1166. }
  1167. /**
  1168. * Wrap a string to a given number of characters.
  1169. *
  1170. * @param string $string
  1171. * @param int $characters
  1172. * @param string $break
  1173. * @param bool $cutLongWords
  1174. */
  1175. public static function wordWrap($string, $characters = 75, $break = "\n", $cutLongWords = false): string
  1176. {
  1177. return wordwrap($string, $characters, $break, $cutLongWords);
  1178. }
  1179. /**
  1180. * Remove all non-numeric characters from a string.
  1181. */
  1182. public static function numbers(array|string $value): array|string
  1183. {
  1184. return preg_replace('/[^0-9]/', '', $value);
  1185. }
  1186. /**
  1187. * Decode the given Base64 encoded string.
  1188. */
  1189. public static function fromBase64(string $string, bool $strict = false): false|string
  1190. {
  1191. return base64_decode($string, $strict);
  1192. }
  1193. /**
  1194. * Returns the replacements for the ascii method.
  1195. * Note: Adapted from Stringy\Stringy.
  1196. *
  1197. * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt
  1198. */
  1199. protected static function charsArray(): array
  1200. {
  1201. static $charsArray;
  1202. if (isset($charsArray)) {
  1203. return $charsArray;
  1204. }
  1205. return $charsArray = [
  1206. '0' => ['°', '₀', '۰', '0'],
  1207. '1' => ['¹', '₁', '۱', '1'],
  1208. '2' => ['²', '₂', '۲', '2'],
  1209. '3' => ['³', '₃', '۳', '3'],
  1210. '4' => ['⁴', '₄', '۴', '٤', '4'],
  1211. '5' => ['⁵', '₅', '۵', '٥', '5'],
  1212. '6' => ['⁶', '₆', '۶', '٦', '6'],
  1213. '7' => ['⁷', '₇', '۷', '7'],
  1214. '8' => ['⁸', '₈', '۸', '8'],
  1215. '9' => ['⁹', '₉', '۹', '9'],
  1216. 'a' => [
  1217. 'à',
  1218. 'á',
  1219. 'ả',
  1220. 'ã',
  1221. 'ạ',
  1222. 'ă',
  1223. 'ắ',
  1224. 'ằ',
  1225. 'ẳ',
  1226. 'ẵ',
  1227. 'ặ',
  1228. 'â',
  1229. 'ấ',
  1230. 'ầ',
  1231. 'ẩ',
  1232. 'ẫ',
  1233. 'ậ',
  1234. 'ā',
  1235. 'ą',
  1236. 'å',
  1237. 'α',
  1238. 'ά',
  1239. 'ἀ',
  1240. 'ἁ',
  1241. 'ἂ',
  1242. 'ἃ',
  1243. 'ἄ',
  1244. 'ἅ',
  1245. 'ἆ',
  1246. 'ἇ',
  1247. 'ᾀ',
  1248. 'ᾁ',
  1249. 'ᾂ',
  1250. 'ᾃ',
  1251. 'ᾄ',
  1252. 'ᾅ',
  1253. 'ᾆ',
  1254. 'ᾇ',
  1255. 'ὰ',
  1256. 'ά',
  1257. 'ᾰ',
  1258. 'ᾱ',
  1259. 'ᾲ',
  1260. 'ᾳ',
  1261. 'ᾴ',
  1262. 'ᾶ',
  1263. 'ᾷ',
  1264. 'а',
  1265. 'أ',
  1266. 'အ',
  1267. 'ာ',
  1268. 'ါ',
  1269. 'ǻ',
  1270. 'ǎ',
  1271. 'ª',
  1272. 'ა',
  1273. 'अ',
  1274. 'ا',
  1275. 'a',
  1276. 'ä',
  1277. ],
  1278. 'b' => ['б', 'β', 'ب', 'ဗ', 'ბ', 'b'],
  1279. 'c' => ['ç', 'ć', 'č', 'ĉ', 'ċ', 'c'],
  1280. 'd' => ['ď', 'ð', 'đ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ᵭ', 'ᶁ', 'ᶑ', 'д', 'δ', 'د', 'ض', 'ဍ', 'ဒ', 'დ', 'd'],
  1281. 'e' => [
  1282. 'é',
  1283. 'è',
  1284. 'ẻ',
  1285. 'ẽ',
  1286. 'ẹ',
  1287. 'ê',
  1288. 'ế',
  1289. 'ề',
  1290. 'ể',
  1291. 'ễ',
  1292. 'ệ',
  1293. 'ë',
  1294. 'ē',
  1295. 'ę',
  1296. 'ě',
  1297. 'ĕ',
  1298. 'ė',
  1299. 'ε',
  1300. 'έ',
  1301. 'ἐ',
  1302. 'ἑ',
  1303. 'ἒ',
  1304. 'ἓ',
  1305. 'ἔ',
  1306. 'ἕ',
  1307. 'ὲ',
  1308. 'έ',
  1309. 'е',
  1310. 'ё',
  1311. 'э',
  1312. 'є',
  1313. 'ə',
  1314. 'ဧ',
  1315. 'ေ',
  1316. 'ဲ',
  1317. 'ე',
  1318. 'ए',
  1319. 'إ',
  1320. 'ئ',
  1321. 'e',
  1322. ],
  1323. 'f' => ['ф', 'φ', 'ف', 'ƒ', 'ფ', 'f'],
  1324. 'g' => ['ĝ', 'ğ', 'ġ', 'ģ', 'г', 'ґ', 'γ', 'ဂ', 'გ', 'گ', 'g'],
  1325. 'h' => ['ĥ', 'ħ', 'η', 'ή', 'ح', 'ه', 'ဟ', 'ှ', 'ჰ', 'h'],
  1326. 'i' => [
  1327. 'í',
  1328. 'ì',
  1329. 'ỉ',
  1330. 'ĩ',
  1331. 'ị',
  1332. 'î',
  1333. 'ï',
  1334. 'ī',
  1335. 'ĭ',
  1336. 'į',
  1337. 'ı',
  1338. 'ι',
  1339. 'ί',
  1340. 'ϊ',
  1341. 'ΐ',
  1342. 'ἰ',
  1343. 'ἱ',
  1344. 'ἲ',
  1345. 'ἳ',
  1346. 'ἴ',
  1347. 'ἵ',
  1348. 'ἶ',
  1349. 'ἷ',
  1350. 'ὶ',
  1351. 'ί',
  1352. 'ῐ',
  1353. 'ῑ',
  1354. 'ῒ',
  1355. 'ΐ',
  1356. 'ῖ',
  1357. 'ῗ',
  1358. 'і',
  1359. 'ї',
  1360. 'и',
  1361. 'ဣ',
  1362. 'ိ',
  1363. 'ီ',
  1364. 'ည်',
  1365. 'ǐ',
  1366. 'ი',
  1367. 'इ',
  1368. 'ی',
  1369. 'i',
  1370. ],
  1371. 'j' => ['ĵ', 'ј', 'Ј', 'ჯ', 'ج', 'j'],
  1372. 'k' => ['ķ', 'ĸ', 'к', 'κ', 'Ķ', 'ق', 'ك', 'က', 'კ', 'ქ', 'ک', 'k'],
  1373. 'l' => ['ł', 'ľ', 'ĺ', 'ļ', 'ŀ', 'л', 'λ', 'ل', 'လ', 'ლ', 'l'],
  1374. 'm' => ['м', 'μ', 'م', 'မ', 'მ', 'm'],
  1375. 'n' => ['ñ', 'ń', 'ň', 'ņ', 'ʼn', 'ŋ', 'ν', 'н', 'ن', 'န', 'ნ', 'n'],
  1376. 'o' => [
  1377. 'ó',
  1378. 'ò',
  1379. 'ỏ',
  1380. 'õ',
  1381. 'ọ',
  1382. 'ô',
  1383. 'ố',
  1384. 'ồ',
  1385. 'ổ',
  1386. 'ỗ',
  1387. 'ộ',
  1388. 'ơ',
  1389. 'ớ',
  1390. 'ờ',
  1391. 'ở',
  1392. 'ỡ',
  1393. 'ợ',
  1394. 'ø',
  1395. 'ō',
  1396. 'ő',
  1397. 'ŏ',
  1398. 'ο',
  1399. 'ὀ',
  1400. 'ὁ',
  1401. 'ὂ',
  1402. 'ὃ',
  1403. 'ὄ',
  1404. 'ὅ',
  1405. 'ὸ',
  1406. 'ό',
  1407. 'о',
  1408. 'و',
  1409. 'θ',
  1410. 'ို',
  1411. 'ǒ',
  1412. 'ǿ',
  1413. 'º',
  1414. 'ო',
  1415. 'ओ',
  1416. 'o',
  1417. 'ö',
  1418. ],
  1419. 'p' => ['п', 'π', 'ပ', 'პ', 'پ', 'p'],
  1420. 'q' => ['ყ', 'q'],
  1421. 'r' => ['ŕ', 'ř', 'ŗ', 'р', 'ρ', 'ر', 'რ', 'r'],
  1422. 's' => ['ś', 'š', 'ş', 'с', 'σ', 'ș', 'ς', 'س', 'ص', 'စ', 'ſ', 'ს', 's'],
  1423. 't' => ['ť', 'ţ', 'т', 'τ', 'ț', 'ت', 'ط', 'ဋ', 'တ', 'ŧ', 'თ', 'ტ', 't'],
  1424. 'u' => [
  1425. 'ú',
  1426. 'ù',
  1427. 'ủ',
  1428. 'ũ',
  1429. 'ụ',
  1430. 'ư',
  1431. 'ứ',
  1432. 'ừ',
  1433. 'ử',
  1434. 'ữ',
  1435. 'ự',
  1436. 'û',
  1437. 'ū',
  1438. 'ů',
  1439. 'ű',
  1440. 'ŭ',
  1441. 'ų',
  1442. 'µ',
  1443. 'у',
  1444. 'ဉ',
  1445. 'ု',
  1446. 'ူ',
  1447. 'ǔ',
  1448. 'ǖ',
  1449. 'ǘ',
  1450. 'ǚ',
  1451. 'ǜ',
  1452. 'უ',
  1453. 'उ',
  1454. 'u',
  1455. 'ў',
  1456. 'ü',
  1457. ],
  1458. 'v' => ['в', 'ვ', 'ϐ', 'v'],
  1459. 'w' => ['ŵ', 'ω', 'ώ', 'ဝ', 'ွ', 'w'],
  1460. 'x' => ['χ', 'ξ', 'x'],
  1461. 'y' => ['ý', 'ỳ', 'ỷ', 'ỹ', 'ỵ', 'ÿ', 'ŷ', 'й', 'ы', 'υ', 'ϋ', 'ύ', 'ΰ', 'ي', 'ယ', 'y'],
  1462. 'z' => ['ź', 'ž', 'ż', 'з', 'ζ', 'ز', 'ဇ', 'ზ', 'z'],
  1463. 'aa' => ['ع', 'आ', 'آ'],
  1464. 'ae' => ['æ', 'ǽ'],
  1465. 'ai' => ['ऐ'],
  1466. 'ch' => ['ч', 'ჩ', 'ჭ', 'چ'],
  1467. 'dj' => ['ђ', 'đ'],
  1468. 'dz' => ['џ', 'ძ'],
  1469. 'ei' => ['ऍ'],
  1470. 'gh' => ['غ', 'ღ'],
  1471. 'ii' => ['ई'],
  1472. 'ij' => ['ij'],
  1473. 'kh' => ['х', 'خ', 'ხ'],
  1474. 'lj' => ['љ'],
  1475. 'nj' => ['њ'],
  1476. 'oe' => ['ö', 'œ', 'ؤ'],
  1477. 'oi' => ['ऑ'],
  1478. 'oii' => ['ऒ'],
  1479. 'ps' => ['ψ'],
  1480. 'sh' => ['ш', 'შ', 'ش'],
  1481. 'shch' => ['щ'],
  1482. 'ss' => ['ß'],
  1483. 'sx' => ['ŝ'],
  1484. 'th' => ['þ', 'ϑ', 'ث', 'ذ', 'ظ'],
  1485. 'ts' => ['ц', 'ც', 'წ'],
  1486. 'ue' => ['ü'],
  1487. 'uu' => ['ऊ'],
  1488. 'ya' => ['я'],
  1489. 'yu' => ['ю'],
  1490. 'zh' => ['ж', 'ჟ', 'ژ'],
  1491. '(c)' => ['©'],
  1492. 'A' => [
  1493. 'Á',
  1494. 'À',
  1495. 'Ả',
  1496. 'Ã',
  1497. 'Ạ',
  1498. 'Ă',
  1499. 'Ắ',
  1500. 'Ằ',
  1501. 'Ẳ',
  1502. 'Ẵ',
  1503. 'Ặ',
  1504. 'Â',
  1505. 'Ấ',
  1506. 'Ầ',
  1507. 'Ẩ',
  1508. 'Ẫ',
  1509. 'Ậ',
  1510. 'Å',
  1511. 'Ā',
  1512. 'Ą',
  1513. 'Α',
  1514. 'Ά',
  1515. 'Ἀ',
  1516. 'Ἁ',
  1517. 'Ἂ',
  1518. 'Ἃ',
  1519. 'Ἄ',
  1520. 'Ἅ',
  1521. 'Ἆ',
  1522. 'Ἇ',
  1523. 'ᾈ',
  1524. 'ᾉ',
  1525. 'ᾊ',
  1526. 'ᾋ',
  1527. 'ᾌ',
  1528. 'ᾍ',
  1529. 'ᾎ',
  1530. 'ᾏ',
  1531. 'Ᾰ',
  1532. 'Ᾱ',
  1533. 'Ὰ',
  1534. 'Ά',
  1535. 'ᾼ',
  1536. 'А',
  1537. 'Ǻ',
  1538. 'Ǎ',
  1539. 'A',
  1540. 'Ä',
  1541. ],
  1542. 'B' => ['Б', 'Β', 'ब', 'B'],
  1543. 'C' => ['Ç', 'Ć', 'Č', 'Ĉ', 'Ċ', 'C'],
  1544. 'D' => ['Ď', 'Ð', 'Đ', 'Ɖ', 'Ɗ', 'Ƌ', 'ᴅ', 'ᴆ', 'Д', 'Δ', 'D'],
  1545. 'E' => [
  1546. 'É',
  1547. 'È',
  1548. 'Ẻ',
  1549. 'Ẽ',
  1550. 'Ẹ',
  1551. 'Ê',
  1552. 'Ế',
  1553. 'Ề',
  1554. 'Ể',
  1555. 'Ễ',
  1556. 'Ệ',
  1557. 'Ë',
  1558. 'Ē',
  1559. 'Ę',
  1560. 'Ě',
  1561. 'Ĕ',
  1562. 'Ė',
  1563. 'Ε',
  1564. 'Έ',
  1565. 'Ἐ',
  1566. 'Ἑ',
  1567. 'Ἒ',
  1568. 'Ἓ',
  1569. 'Ἔ',
  1570. 'Ἕ',
  1571. 'Έ',
  1572. 'Ὲ',
  1573. 'Е',
  1574. 'Ё',
  1575. 'Э',
  1576. 'Є',
  1577. 'Ə',
  1578. 'E',
  1579. ],
  1580. 'F' => ['Ф', 'Φ', 'F'],
  1581. 'G' => ['Ğ', 'Ġ', 'Ģ', 'Г', 'Ґ', 'Γ', 'G'],
  1582. 'H' => ['Η', 'Ή', 'Ħ', 'H'],
  1583. 'I' => [
  1584. 'Í',
  1585. 'Ì',
  1586. 'Ỉ',
  1587. 'Ĩ',
  1588. 'Ị',
  1589. 'Î',
  1590. 'Ï',
  1591. 'Ī',
  1592. 'Ĭ',
  1593. 'Į',
  1594. 'İ',
  1595. 'Ι',
  1596. 'Ί',
  1597. 'Ϊ',
  1598. 'Ἰ',
  1599. 'Ἱ',
  1600. 'Ἳ',
  1601. 'Ἴ',
  1602. 'Ἵ',
  1603. 'Ἶ',
  1604. 'Ἷ',
  1605. 'Ῐ',
  1606. 'Ῑ',
  1607. 'Ὶ',
  1608. 'Ί',
  1609. 'И',
  1610. 'І',
  1611. 'Ї',
  1612. 'Ǐ',
  1613. 'ϒ',
  1614. 'I',
  1615. ],
  1616. 'J' => ['J'],
  1617. 'K' => ['К', 'Κ', 'K'],
  1618. 'L' => ['Ĺ', 'Ł', 'Л', 'Λ', 'Ļ', 'Ľ', 'Ŀ', 'ल', 'L'],
  1619. 'M' => ['М', 'Μ', 'M'],
  1620. 'N' => ['Ń', 'Ñ', 'Ň', 'Ņ', 'Ŋ', 'Н', 'Ν', 'N'],
  1621. 'O' => [
  1622. 'Ó',
  1623. 'Ò',
  1624. 'Ỏ',
  1625. 'Õ',
  1626. 'Ọ',
  1627. 'Ô',
  1628. 'Ố',
  1629. 'Ồ',
  1630. 'Ổ',
  1631. 'Ỗ',
  1632. 'Ộ',
  1633. 'Ơ',
  1634. 'Ớ',
  1635. 'Ờ',
  1636. 'Ở',
  1637. 'Ỡ',
  1638. 'Ợ',
  1639. 'Ø',
  1640. 'Ō',
  1641. 'Ő',
  1642. 'Ŏ',
  1643. 'Ο',
  1644. 'Ό',
  1645. 'Ὀ',
  1646. 'Ὁ',
  1647. 'Ὂ',
  1648. 'Ὃ',
  1649. 'Ὄ',
  1650. 'Ὅ',
  1651. 'Ὸ',
  1652. 'Ό',
  1653. 'О',
  1654. 'Θ',
  1655. 'Ө',
  1656. 'Ǒ',
  1657. 'Ǿ',
  1658. 'O',
  1659. 'Ö',
  1660. ],
  1661. 'P' => ['П', 'Π', 'P'],
  1662. 'Q' => ['Q'],
  1663. 'R' => ['Ř', 'Ŕ', 'Р', 'Ρ', 'Ŗ', 'R'],
  1664. 'S' => ['Ş', 'Ŝ', 'Ș', 'Š', 'Ś', 'С', 'Σ', 'S'],
  1665. 'T' => ['Ť', 'Ţ', 'Ŧ', 'Ț', 'Т', 'Τ', 'T'],
  1666. 'U' => [
  1667. 'Ú',
  1668. 'Ù',
  1669. 'Ủ',
  1670. 'Ũ',
  1671. 'Ụ',
  1672. 'Ư',
  1673. 'Ứ',
  1674. 'Ừ',
  1675. 'Ử',
  1676. 'Ữ',
  1677. 'Ự',
  1678. 'Û',
  1679. 'Ū',
  1680. 'Ů',
  1681. 'Ű',
  1682. 'Ŭ',
  1683. 'Ų',
  1684. 'У',
  1685. 'Ǔ',
  1686. 'Ǖ',
  1687. 'Ǘ',
  1688. 'Ǚ',
  1689. 'Ǜ',
  1690. 'U',
  1691. 'Ў',
  1692. 'Ü',
  1693. ],
  1694. 'V' => ['В', 'V'],
  1695. 'W' => ['Ω', 'Ώ', 'Ŵ', 'W'],
  1696. 'X' => ['Χ', 'Ξ', 'X'],
  1697. 'Y' => ['Ý', 'Ỳ', 'Ỷ', 'Ỹ', 'Ỵ', 'Ÿ', 'Ῠ', 'Ῡ', 'Ὺ', 'Ύ', 'Ы', 'Й', 'Υ', 'Ϋ', 'Ŷ', 'Y'],
  1698. 'Z' => ['Ź', 'Ž', 'Ż', 'З', 'Ζ', 'Z'],
  1699. 'AE' => ['Æ', 'Ǽ'],
  1700. 'Ch' => ['Ч'],
  1701. 'Dj' => ['Ђ'],
  1702. 'Dz' => ['Џ'],
  1703. 'Gx' => ['Ĝ'],
  1704. 'Hx' => ['Ĥ'],
  1705. 'Ij' => ['IJ'],
  1706. 'Jx' => ['Ĵ'],
  1707. 'Kh' => ['Х'],
  1708. 'Lj' => ['Љ'],
  1709. 'Nj' => ['Њ'],
  1710. 'Oe' => ['Œ'],
  1711. 'Ps' => ['Ψ'],
  1712. 'Sh' => ['Ш'],
  1713. 'Shch' => ['Щ'],
  1714. 'Ss' => ['ẞ'],
  1715. 'Th' => ['Þ'],
  1716. 'Ts' => ['Ц'],
  1717. 'Ya' => ['Я'],
  1718. 'Yu' => ['Ю'],
  1719. 'Zh' => ['Ж'],
  1720. ' ' => [
  1721. "\xC2\xA0",
  1722. "\xE2\x80\x80",
  1723. "\xE2\x80\x81",
  1724. "\xE2\x80\x82",
  1725. "\xE2\x80\x83",
  1726. "\xE2\x80\x84",
  1727. "\xE2\x80\x85",
  1728. "\xE2\x80\x86",
  1729. "\xE2\x80\x87",
  1730. "\xE2\x80\x88",
  1731. "\xE2\x80\x89",
  1732. "\xE2\x80\x8A",
  1733. "\xE2\x80\xAF",
  1734. "\xE2\x81\x9F",
  1735. "\xE3\x80\x80",
  1736. "\xEF\xBE\xA0",
  1737. ],
  1738. ];
  1739. }
  1740. /**
  1741. * Returns the language specific replacements for the ascii method.
  1742. * Note: Adapted from Stringy\Stringy.
  1743. *
  1744. * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt
  1745. * @return null|array
  1746. */
  1747. protected static function languageSpecificCharsArray(string $language)
  1748. {
  1749. static $languageSpecific;
  1750. if (! isset($languageSpecific)) {
  1751. $languageSpecific = [
  1752. 'bg' => [
  1753. ['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'],
  1754. ['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'],
  1755. ],
  1756. 'de' => [
  1757. ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü'],
  1758. ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'],
  1759. ],
  1760. ];
  1761. }
  1762. return $languageSpecific[$language] ?? null;
  1763. }
  1764. }