Файловый менеджер - Редактировать - /home/clickysoft/public_html/jmapi5.clickysoft.net/string.tar
Назад
Inflector/FrenchInflector.php 0000644 00000013513 15021235637 0012264 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; /** * French inflector. * * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix". */ final class FrenchInflector implements InflectorInterface { /** * A list of all rules for pluralise. * * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php */ private const PLURALIZE_REGEXP = [ // First entry: regexp // Second entry: replacement // Words finishing with "s", "x" or "z" are invariables // Les mots finissant par "s", "x" ou "z" sont invariables ['/(s|x|z)$/i', '\1'], // Words finishing with "eau" are pluralized with a "x" // Les mots finissant par "eau" prennent tous un "x" au pluriel ['/(eau)$/i', '\1x'], // Words finishing with "au" are pluralized with a "x" excepted "landau" // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" ['/^(landau)$/i', '\1s'], ['/(au)$/i', '\1x'], // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" ['/^(pneu|bleu|émeu)$/i', '\1s'], ['/(eu)$/i', '\1x'], // Words finishing with "al" are pluralized with a "aux" excepted // Les mots finissant en "al" se terminent en "aux" sauf ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'], ['/al$/i', '\1aux'], // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'], // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'], // Invariable words ['/^(cinquante|soixante|mille)$/i', '\1'], // French titles ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'], ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'], ]; /** * A list of all rules for singularize. */ private const SINGULARIZE_REGEXP = [ // First entry: regexp // Second entry: replacement // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'], // Words finishing with "eau" are pluralized with a "x" // Les mots finissant par "eau" prennent tous un "x" au pluriel ['/(eau)x$/i', '\1'], // Words finishing with "al" are pluralized with a "aux" expected // Les mots finissant en "al" se terminent en "aux" sauf ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'], // Words finishing with "au" are pluralized with a "x" excepted "landau" // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" ['/(au)x$/i', '\1'], // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" ['/(eu)x$/i', '\1'], // Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'], // French titles ['/^mes(dame|demoiselle)s$/', 'ma\1'], ['/^Mes(dame|demoiselle)s$/', 'Ma\1'], ['/^mes(sieur|seigneur)s$/', 'mon\1'], ['/^Mes(sieur|seigneur)s$/', 'Mon\1'], // Default rule ['/s$/i', ''], ]; /** * A list of words which should not be inflected. * This list is only used by singularize. */ private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sans|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i'; public function singularize(string $plural): array { if ($this->isInflectedWord($plural)) { return [$plural]; } foreach (self::SINGULARIZE_REGEXP as $rule) { [$regexp, $replace] = $rule; if (1 === preg_match($regexp, $plural)) { return [preg_replace($regexp, $replace, $plural)]; } } return [$plural]; } public function pluralize(string $singular): array { if ($this->isInflectedWord($singular)) { return [$singular]; } foreach (self::PLURALIZE_REGEXP as $rule) { [$regexp, $replace] = $rule; if (1 === preg_match($regexp, $singular)) { return [preg_replace($regexp, $replace, $singular)]; } } return [$singular.'s']; } private function isInflectedWord(string $word): bool { return 1 === preg_match(self::UNINFLECTED, $word); } } Inflector/EnglishInflector.php 0000644 00000041440 15021235637 0012450 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; final class EnglishInflector implements InflectorInterface { /** * Map English plural to singular suffixes. * * @see http://english-zone.com/spelling/plurals.html */ private const PLURAL_MAP = [ // First entry: plural suffix, reversed // Second entry: length of plural suffix // Third entry: Whether the suffix may succeed a vowel // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: singular suffix, normal // bacteria (bacterium) ['airetcab', 8, true, true, 'bacterium'], // corpora (corpus) ['aroproc', 7, true, true, 'corpus'], // criteria (criterion) ['airetirc', 8, true, true, 'criterion'], // curricula (curriculum) ['alucirruc', 9, true, true, 'curriculum'], // quora (quorum) ['arouq', 5, true, true, 'quorum'], // genera (genus) ['areneg', 6, true, true, 'genus'], // media (medium) ['aidem', 5, true, true, 'medium'], // memoranda (memorandum) ['adnaromem', 9, true, true, 'memorandum'], // phenomena (phenomenon) ['anemonehp', 9, true, true, 'phenomenon'], // strata (stratum) ['atarts', 6, true, true, 'stratum'], // nebulae (nebula) ['ea', 2, true, true, 'a'], // services (service) ['secivres', 8, true, true, 'service'], // mice (mouse), lice (louse) ['eci', 3, false, true, 'ouse'], // geese (goose) ['esee', 4, false, true, 'oose'], // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) ['i', 1, true, true, 'us'], // men (man), women (woman) ['nem', 3, true, true, 'man'], // children (child) ['nerdlihc', 8, true, true, 'child'], // oxen (ox) ['nexo', 4, false, false, 'ox'], // indices (index), appendices (appendix), prices (price) ['seci', 4, false, true, ['ex', 'ix', 'ice']], // codes (code) ['sedoc', 5, false, true, 'code'], // selfies (selfie) ['seifles', 7, true, true, 'selfie'], // zombies (zombie) ['seibmoz', 7, true, true, 'zombie'], // movies (movie) ['seivom', 6, true, true, 'movie'], // names (name) ['seman', 5, true, false, 'name'], // conspectuses (conspectus), prospectuses (prospectus) ['sesutcep', 8, true, true, 'pectus'], // feet (foot) ['teef', 4, true, true, 'foot'], // geese (goose) ['eseeg', 5, true, true, 'goose'], // teeth (tooth) ['hteet', 5, true, true, 'tooth'], // news (news) ['swen', 4, true, true, 'news'], // series (series) ['seires', 6, true, true, 'series'], // babies (baby) ['sei', 3, false, true, 'y'], // accesses (access), addresses (address), kisses (kiss) ['sess', 4, true, false, 'ss'], // statuses (status) ['sesutats', 8, true, true, 'status'], // article (articles), ancle (ancles) ['sel', 3, true, true, 'le'], // analyses (analysis), ellipses (ellipsis), fungi (fungus), // neuroses (neurosis), theses (thesis), emphases (emphasis), // oases (oasis), crises (crisis), houses (house), bases (base), // atlases (atlas) ['ses', 3, true, true, ['s', 'se', 'sis']], // objectives (objective), alternative (alternatives) ['sevit', 5, true, true, 'tive'], // drives (drive) ['sevird', 6, false, true, 'drive'], // lives (life), wives (wife) ['sevi', 4, false, true, 'ife'], // moves (move) ['sevom', 5, true, true, 'move'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff) ['sev', 3, true, true, ['f', 've', 'ff']], // axes (axis), axes (ax), axes (axe) ['sexa', 4, false, false, ['ax', 'axe', 'axis']], // indexes (index), matrixes (matrix) ['sex', 3, true, false, 'x'], // quizzes (quiz) ['sezz', 4, true, false, 'z'], // bureaus (bureau) ['suae', 4, false, true, 'eau'], // fees (fee), trees (tree), employees (employee) ['see', 3, true, true, 'ee'], // edges (edge) ['segd', 4, true, true, 'dge'], // roses (rose), garages (garage), cassettes (cassette), // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), // shoes (shoe) ['se', 2, true, true, ['', 'e']], // status (status) ['sutats', 6, true, true, 'status'], // tags (tag) ['s', 1, true, true, ''], // chateaux (chateau) ['xuae', 4, false, true, 'eau'], // people (person) ['elpoep', 6, true, true, 'person'], ]; /** * Map English singular to plural suffixes. * * @see http://english-zone.com/spelling/plurals.html */ private const SINGULAR_MAP = [ // First entry: singular suffix, reversed // Second entry: length of singular suffix // Third entry: Whether the suffix may succeed a vowel // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: plural suffix, normal // axes (axis) ['sixa', 4, false, false, 'axes'], // criterion (criteria) ['airetirc', 8, false, false, 'criterion'], // nebulae (nebula) ['aluben', 6, false, false, 'nebulae'], // children (child) ['dlihc', 5, true, true, 'children'], // prices (price) ['eci', 3, false, true, 'ices'], // services (service) ['ecivres', 7, true, true, 'services'], // lives (life), wives (wife) ['efi', 3, false, true, 'ives'], // selfies (selfie) ['eifles', 6, true, true, 'selfies'], // movies (movie) ['eivom', 5, true, true, 'movies'], // lice (louse) ['esuol', 5, false, true, 'lice'], // mice (mouse) ['esuom', 5, false, true, 'mice'], // geese (goose) ['esoo', 4, false, true, 'eese'], // houses (house), bases (base) ['es', 2, true, true, 'ses'], // geese (goose) ['esoog', 5, true, true, 'geese'], // caves (cave) ['ev', 2, true, true, 'ves'], // drives (drive) ['evird', 5, false, true, 'drives'], // objectives (objective), alternative (alternatives) ['evit', 4, true, true, 'tives'], // moves (move) ['evom', 4, true, true, 'moves'], // staves (staff) ['ffats', 5, true, true, 'staves'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) ['ff', 2, true, true, 'ffs'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) ['f', 1, true, true, ['fs', 'ves']], // arches (arch) ['hc', 2, true, true, 'ches'], // bushes (bush) ['hs', 2, true, true, 'shes'], // teeth (tooth) ['htoot', 5, true, true, 'teeth'], // albums (album) ['mubla', 5, true, true, 'albums'], // quorums (quorum) ['murouq', 6, true, true, ['quora', 'quorums']], // bacteria (bacterium), curricula (curriculum), media (medium), memoranda (memorandum), phenomena (phenomenon), strata (stratum) ['mu', 2, true, true, 'a'], // men (man), women (woman) ['nam', 3, true, true, 'men'], // people (person) ['nosrep', 6, true, true, ['persons', 'people']], // criteria (criterion) ['noiretirc', 9, true, true, 'criteria'], // phenomena (phenomenon) ['nonemonehp', 10, true, true, 'phenomena'], // echoes (echo) ['ohce', 4, true, true, 'echoes'], // heroes (hero) ['oreh', 4, true, true, 'heroes'], // atlases (atlas) ['salta', 5, true, true, 'atlases'], // aliases (alias) ['saila', 5, true, true, 'aliases'], // irises (iris) ['siri', 4, true, true, 'irises'], // analyses (analysis), ellipses (ellipsis), neuroses (neurosis) // theses (thesis), emphases (emphasis), oases (oasis), // crises (crisis) ['sis', 3, true, true, 'ses'], // accesses (access), addresses (address), kisses (kiss) ['ss', 2, true, false, 'sses'], // syllabi (syllabus) ['suballys', 8, true, true, 'syllabi'], // buses (bus) ['sub', 3, true, true, 'buses'], // circuses (circus) ['suc', 3, true, true, 'cuses'], // hippocampi (hippocampus) ['supmacoppih', 11, false, false, 'hippocampi'], // campuses (campus) ['sup', 3, true, true, 'puses'], // status (status) ['sutats', 6, true, true, ['status', 'statuses']], // conspectuses (conspectus), prospectuses (prospectus) ['sutcep', 6, true, true, 'pectuses'], // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) ['su', 2, true, true, 'i'], // news (news) ['swen', 4, true, true, 'news'], // feet (foot) ['toof', 4, true, true, 'feet'], // chateaux (chateau), bureaus (bureau) ['uae', 3, false, true, ['eaus', 'eaux']], // oxen (ox) ['xo', 2, false, false, 'oxen'], // hoaxes (hoax) ['xaoh', 4, true, false, 'hoaxes'], // indices (index) ['xedni', 5, false, true, ['indicies', 'indexes']], // boxes (box) ['xo', 2, false, true, 'oxes'], // indexes (index), matrixes (matrix) ['x', 1, true, false, ['cies', 'xes']], // appendices (appendix) ['xi', 2, false, true, 'ices'], // babies (baby) ['y', 1, false, true, 'ies'], // quizzes (quiz) ['ziuq', 4, true, false, 'quizzes'], // waltzes (waltz) ['z', 1, true, true, 'zes'], ]; /** * A list of words which should not be inflected, reversed. */ private const UNINFLECTED = [ '', // data 'atad', // deer 'reed', // equipment 'tnempiuqe', // feedback 'kcabdeef', // fish 'hsif', // health 'htlaeh', // history 'yrotsih', // info 'ofni', // information 'noitamrofni', // money 'yenom', // moose 'esoom', // series 'seires', // sheep 'peehs', // species 'seiceps', // traffic 'ciffart', // aircraft 'tfarcria', // hardware 'erawdrah', ]; public function singularize(string $plural): array { $pluralRev = strrev($plural); $lowerPluralRev = strtolower($pluralRev); $pluralLength = \strlen($lowerPluralRev); // Check if the word is one which is not inflected, return early if so if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) { return [$plural]; } // The outer loop iterates over the entries of the plural table // The inner loop $j iterates over the characters of the plural suffix // in the plural table to compare them with the characters of the actual // given plural suffix foreach (self::PLURAL_MAP as $map) { $suffix = $map[0]; $suffixLength = $map[1]; $j = 0; // Compare characters in the plural table and of the suffix of the // given plural one by one while ($suffix[$j] === $lowerPluralRev[$j]) { // Let $j point to the next character ++$j; // Successfully compared the last character // Add an entry with the singular suffix to the singular array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $pluralLength) { $nextIsVowel = str_contains('aeiou', $lowerPluralRev[$j]); if (!$map[2] && $nextIsVowel) { // suffix may not succeed a vowel but next char is one break; } if (!$map[3] && !$nextIsVowel) { // suffix may not succeed a consonant but next char is one break; } } $newBase = substr($plural, 0, $pluralLength - $suffixLength); $newSuffix = $map[4]; // Check whether the first character in the plural suffix // is uppercased. If yes, uppercase the first character in // the singular suffix too $firstUpper = ctype_upper($pluralRev[$j - 1]); if (\is_array($newSuffix)) { $singulars = []; foreach ($newSuffix as $newSuffixEntry) { $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); } return $singulars; } return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; } // Suffix is longer than word if ($j === $pluralLength) { break; } } } // Assume that plural and singular is identical return [$plural]; } public function pluralize(string $singular): array { $singularRev = strrev($singular); $lowerSingularRev = strtolower($singularRev); $singularLength = \strlen($lowerSingularRev); // Check if the word is one which is not inflected, return early if so if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) { return [$singular]; } // The outer loop iterates over the entries of the singular table // The inner loop $j iterates over the characters of the singular suffix // in the singular table to compare them with the characters of the actual // given singular suffix foreach (self::SINGULAR_MAP as $map) { $suffix = $map[0]; $suffixLength = $map[1]; $j = 0; // Compare characters in the singular table and of the suffix of the // given plural one by one while ($suffix[$j] === $lowerSingularRev[$j]) { // Let $j point to the next character ++$j; // Successfully compared the last character // Add an entry with the plural suffix to the plural array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $singularLength) { $nextIsVowel = str_contains('aeiou', $lowerSingularRev[$j]); if (!$map[2] && $nextIsVowel) { // suffix may not succeed a vowel but next char is one break; } if (!$map[3] && !$nextIsVowel) { // suffix may not succeed a consonant but next char is one break; } } $newBase = substr($singular, 0, $singularLength - $suffixLength); $newSuffix = $map[4]; // Check whether the first character in the singular suffix // is uppercased. If yes, uppercase the first character in // the singular suffix too $firstUpper = ctype_upper($singularRev[$j - 1]); if (\is_array($newSuffix)) { $plurals = []; foreach ($newSuffix as $newSuffixEntry) { $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); } return $plurals; } return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; } // Suffix is longer than word if ($j === $singularLength) { break; } } } // Assume that plural is singular with a trailing `s` return [$singular.'s']; } } Inflector/InflectorInterface.php 0000644 00000001503 15021235637 0012753 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; interface InflectorInterface { /** * Returns the singular forms of a string. * * If the method can't determine the form with certainty, several possible singulars are returned. * * @return string[] */ public function singularize(string $plural): array; /** * Returns the plural forms of a string. * * If the method can't determine the form with certainty, several possible plurals are returned. * * @return string[] */ public function pluralize(string $singular): array; } AbstractString.php 0000644 00000044713 15021235637 0010224 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a string of abstract characters. * * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). * This class is the abstract type to use as a type-hint when the logic you want to * implement doesn't care about the exact variant it deals with. * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ abstract class AbstractString implements \Stringable, \JsonSerializable { public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER; public const PREG_SET_ORDER = \PREG_SET_ORDER; public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE; public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL; public const PREG_SPLIT = 0; public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY; public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE; public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE; protected $string = ''; protected $ignoreCase = false; abstract public function __construct(string $string = ''); /** * Unwraps instances of AbstractString back to strings. * * @return string[]|array */ public static function unwrap(array $values): array { foreach ($values as $k => $v) { if ($v instanceof self) { $values[$k] = $v->__toString(); } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) { $values[$k] = $v; } } return $values; } /** * Wraps (and normalizes) strings in instances of AbstractString. * * @return static[]|array */ public static function wrap(array $values): array { $i = 0; $keys = null; foreach ($values as $k => $v) { if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) { $keys ??= array_keys($values); $keys[$i] = $j; } if (\is_string($v)) { $values[$k] = new static($v); } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) { $values[$k] = $v; } ++$i; } return null !== $keys ? array_combine($keys, $values) : $values; } /** * @param string|string[] $needle */ public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = \PHP_INT_MAX; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOf($n, $offset); if (null !== $j && $j < $i) { $i = $j; $str->string = $n; } } if (\PHP_INT_MAX === $i) { return $str; } if (!$includeNeedle) { $i += $str->length(); } return $this->slice($i); } /** * @param string|string[] $needle */ public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = null; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOfLast($n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; $str->string = $n; } } if (null === $i) { return $str; } if (!$includeNeedle) { $i += $str->length(); } return $this->slice($i); } abstract public function append(string ...$suffix): static; /** * @param string|string[] $needle */ public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = \PHP_INT_MAX; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOf($n, $offset); if (null !== $j && $j < $i) { $i = $j; $str->string = $n; } } if (\PHP_INT_MAX === $i) { return $str; } if ($includeNeedle) { $i += $str->length(); } return $this->slice(0, $i); } /** * @param string|string[] $needle */ public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = null; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOfLast($n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; $str->string = $n; } } if (null === $i) { return $str; } if ($includeNeedle) { $i += $str->length(); } return $this->slice(0, $i); } /** * @return int[] */ public function bytesAt(int $offset): array { $str = $this->slice($offset, 1); return '' === $str->string ? [] : array_values(unpack('C*', $str->string)); } abstract public function camel(): static; /** * @return static[] */ abstract public function chunk(int $length = 1): array; public function collapseWhitespace(): static { $str = clone $this; $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C"); return $str; } /** * @param string|string[] $needle */ public function containsAny(string|iterable $needle): bool { return null !== $this->indexOf($needle); } /** * @param string|string[] $suffix */ public function endsWith(string|iterable $suffix): bool { if (\is_string($suffix)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($suffix as $s) { if ($this->endsWith((string) $s)) { return true; } } return false; } public function ensureEnd(string $suffix): static { if (!$this->endsWith($suffix)) { return $this->append($suffix); } $suffix = preg_quote($suffix); $regex = '{('.$suffix.')(?:'.$suffix.')++$}D'; return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1'); } public function ensureStart(string $prefix): static { $prefix = new static($prefix); if (!$this->startsWith($prefix)) { return $this->prepend($prefix); } $str = clone $this; $i = $prefixLen = $prefix->length(); while ($this->indexOf($prefix, $i) === $i) { $str = $str->slice($prefixLen); $i += $prefixLen; } return $str; } /** * @param string|string[] $string */ public function equalsTo(string|iterable $string): bool { if (\is_string($string)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($string as $s) { if ($this->equalsTo((string) $s)) { return true; } } return false; } abstract public function folded(): static; public function ignoreCase(): static { $str = clone $this; $str->ignoreCase = true; return $str; } /** * @param string|string[] $needle */ public function indexOf(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = \PHP_INT_MAX; foreach ($needle as $n) { $j = $this->indexOf((string) $n, $offset); if (null !== $j && $j < $i) { $i = $j; } } return \PHP_INT_MAX === $i ? null : $i; } /** * @param string|string[] $needle */ public function indexOfLast(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = null; foreach ($needle as $n) { $j = $this->indexOfLast((string) $n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; } } return $i; } public function isEmpty(): bool { return '' === $this->string; } abstract public function join(array $strings, ?string $lastGlue = null): static; public function jsonSerialize(): string { return $this->string; } abstract public function length(): int; abstract public function lower(): static; /** * Matches the string using a regular expression. * * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression. * * @return array All matches in a multi-dimensional array ordered according to flags */ abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array; abstract public function padBoth(int $length, string $padStr = ' '): static; abstract public function padEnd(int $length, string $padStr = ' '): static; abstract public function padStart(int $length, string $padStr = ' '): static; abstract public function prepend(string ...$prefix): static; public function repeat(int $multiplier): static { if (0 > $multiplier) { throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier)); } $str = clone $this; $str->string = str_repeat($str->string, $multiplier); return $str; } abstract public function replace(string $from, string $to): static; abstract public function replaceMatches(string $fromRegexp, string|callable $to): static; abstract public function reverse(): static; abstract public function slice(int $start = 0, ?int $length = null): static; abstract public function snake(): static; abstract public function splice(string $replacement, int $start = 0, ?int $length = null): static; /** * @return static[] */ public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (null === $flags) { throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.'); } if ($this->ignoreCase) { $delimiter .= 'i'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) { throw new RuntimeException('Splitting failed with error: '.preg_last_error_msg()); } } finally { restore_error_handler(); } $str = clone $this; if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) { foreach ($chunks as &$chunk) { $str->string = $chunk[0]; $chunk[0] = clone $str; } } else { foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } } return $chunks; } /** * @param string|string[] $prefix */ public function startsWith(string|iterable $prefix): bool { if (\is_string($prefix)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($prefix as $prefix) { if ($this->startsWith((string) $prefix)) { return true; } } return false; } abstract public function title(bool $allWords = false): static; public function toByteString(?string $toEncoding = null): ByteString { $b = new ByteString(); $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding; if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') { $b->string = $this->string; return $b; } try { $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8'); } catch (\ValueError $e) { if (!\function_exists('iconv')) { throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } $b->string = iconv('UTF-8', $toEncoding, $this->string); } return $b; } public function toCodePointString(): CodePointString { return new CodePointString($this->string); } public function toString(): string { return $this->string; } public function toUnicodeString(): UnicodeString { return new UnicodeString($this->string); } abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; /** * @param string|string[] $prefix */ public function trimPrefix($prefix): static { if (\is_array($prefix) || $prefix instanceof \Traversable) { // don't use is_iterable(), it's slow foreach ($prefix as $s) { $t = $this->trimPrefix($s); if ($t->string !== $this->string) { return $t; } } return clone $this; } $str = clone $this; if ($prefix instanceof self) { $prefix = $prefix->string; } else { $prefix = (string) $prefix; } if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) { $str->string = substr($this->string, \strlen($prefix)); } return $str; } abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; /** * @param string|string[] $suffix */ public function trimSuffix($suffix): static { if (\is_array($suffix) || $suffix instanceof \Traversable) { // don't use is_iterable(), it's slow foreach ($suffix as $s) { $t = $this->trimSuffix($s); if ($t->string !== $this->string) { return $t; } } return clone $this; } $str = clone $this; if ($suffix instanceof self) { $suffix = $suffix->string; } else { $suffix = (string) $suffix; } if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) { $str->string = substr($this->string, 0, -\strlen($suffix)); } return $str; } public function truncate(int $length, string $ellipsis = '', bool $cut = true): static { $stringLength = $this->length(); if ($stringLength <= $length) { return clone $this; } $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0; if ($length < $ellipsisLength) { $ellipsisLength = 0; } if (!$cut) { if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { return clone $this; } $length += $ellipsisLength; } $str = $this->slice(0, $length - $ellipsisLength); return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str; } abstract public function upper(): static; /** * Returns the printable length on a terminal. */ abstract public function width(bool $ignoreAnsiDecoration = true): int; public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static { $lines = '' !== $break ? $this->split($break) : [clone $this]; $chars = []; $mask = ''; if (1 === \count($lines) && '' === $lines[0]->string) { return $lines[0]; } foreach ($lines as $i => $line) { if ($i) { $chars[] = $break; $mask .= '#'; } foreach ($line->chunk() as $char) { $chars[] = $char->string; $mask .= ' ' === $char->string ? ' ' : '?'; } } $string = ''; $j = 0; $b = $i = -1; $mask = wordwrap($mask, $width, '#', $cut); while (false !== $b = strpos($mask, '#', $b + 1)) { for (++$i; $i < $b; ++$i) { $string .= $chars[$j]; unset($chars[$j++]); } if ($break === $chars[$j] || ' ' === $chars[$j]) { unset($chars[$j++]); } $string .= $break; } $str = clone $this; $str->string = $string.implode('', $chars); return $str; } public function __sleep(): array { return ['string']; } public function __clone() { $this->ignoreCase = false; } public function __toString(): string { return $this->string; } } LazyString.php 0000644 00000010555 15021235637 0007375 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; /** * A string whose value is computed lazily by a callback. * * @author Nicolas Grekas <p@tchwork.com> */ class LazyString implements \Stringable, \JsonSerializable { private \Closure|string $value; /** * @param callable|array $callback A callable or a [Closure, method] lazy-callable */ public static function fromCallable(callable|array $callback, mixed ...$arguments): static { if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) { throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); } $lazyString = new static(); $lazyString->value = static function () use (&$callback, &$arguments): string { static $value; if (null !== $arguments) { if (!\is_callable($callback)) { $callback[0] = $callback[0](); $callback[1] ??= '__invoke'; } $value = $callback(...$arguments); $callback = !\is_scalar($value) && !$value instanceof \Stringable ? self::getPrettyName($callback) : 'callable'; $arguments = null; } return $value ?? ''; }; return $lazyString; } public static function fromStringable(string|int|float|bool|\Stringable $value): static { if (\is_object($value)) { return static::fromCallable($value->__toString(...)); } $lazyString = new static(); $lazyString->value = (string) $value; return $lazyString; } /** * Tells whether the provided value can be cast to string. */ final public static function isStringable(mixed $value): bool { return \is_string($value) || $value instanceof \Stringable || \is_scalar($value); } /** * Casts scalars and stringable objects to strings. * * @throws \TypeError When the provided value is not stringable */ final public static function resolve(\Stringable|string|int|float|bool $value): string { return $value; } public function __toString(): string { if (\is_string($this->value)) { return $this->value; } try { return $this->value = ($this->value)(); } catch (\Throwable $e) { if (\TypeError::class === $e::class && __FILE__ === $e->getFile()) { $type = explode(', ', $e->getMessage()); $type = substr(array_pop($type), 0, -\strlen(' returned')); $r = new \ReflectionFunction($this->value); $callback = $r->getStaticVariables()['callback']; $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); } throw $e; } } public function __sleep(): array { $this->__toString(); return ['value']; } public function jsonSerialize(): string { return $this->__toString(); } private function __construct() { } private static function getPrettyName(callable $callback): string { if (\is_string($callback)) { return $callback; } if (\is_array($callback)) { $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0]; $method = $callback[1]; } elseif ($callback instanceof \Closure) { $r = new \ReflectionFunction($callback); if (str_contains($r->name, '{closure') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { return $r->name; } $class = $class->name; $method = $r->name; } else { $class = get_debug_type($callback); $method = '__invoke'; } return $class.'::'.$method; } } composer.json 0000644 00000002547 15021235637 0007302 0 ustar 00 { "name": "symfony/string", "type": "library", "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "keywords": ["string", "utf8", "utf-8", "grapheme", "i18n", "unicode"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "symfony/error-handler": "^5.4|^6.0|^7.0", "symfony/intl": "^6.2|^7.0", "symfony/http-client": "^5.4|^6.0|^7.0", "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "autoload": { "psr-4": { "Symfony\\Component\\String\\": "" }, "files": [ "Resources/functions.php" ], "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } CodePointString.php 0000644 00000017050 15021235637 0010337 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; /** * Represents a string of Unicode code points encoded as UTF-8. * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ class CodePointString extends AbstractUnicodeString { public function __construct(string $string = '') { if ('' !== $string && !preg_match('//u', $string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $this->string = $string; } public function append(string ...$suffix): static { $str = clone $this; $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $rx = '/('; while (65535 < $length) { $rx .= '.{65535}'; $length -= 65535; } $rx .= '.{'.$length.'})/us'; $str = clone $this; $chunks = []; foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function codePointsAt(int $offset): array { $str = $offset ? $this->slice($offset, 1) : $this; return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')]; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } if ('' === $suffix || !preg_match('//u', $suffix)) { return false; } if ($this->ignoreCase) { return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string); } return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix)); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } if ('' !== $string && $this->ignoreCase) { return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); return false === $i ? null : $i; } public function length(): int { return mb_strlen($this->string, 'UTF-8'); } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function replace(string $from, string $to): static { $str = clone $this; if ('' === $from || !preg_match('//u', $from)) { return $str; } if ('' !== $to && !preg_match('//u', $to)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } if ($this->ignoreCase) { $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string)); } else { $str->string = str_replace($from, $to, $this->string); } return $str; } public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = mb_substr($this->string, $start, $length, 'UTF-8'); return $str; } public function splice(string $replacement, int $start = 0, ?int $length = null): static { if (!preg_match('//u', $replacement)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str = clone $this; $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; } public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (1 > $limit ??= \PHP_INT_MAX) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter.'u', $limit, $flags); } if (!preg_match('//u', $delimiter)) { throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); } $str = clone $this; $chunks = $this->ignoreCase ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit) : explode($delimiter, $this->string, $limit); foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } if ('' === $prefix || !preg_match('//u', $prefix)) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8'); } return 0 === strncmp($this->string, $prefix, \strlen($prefix)); } } UnicodeString.php 0000644 00000030422 15021235637 0010037 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; /** * Represents a string of Unicode grapheme clusters encoded as UTF-8. * * A letter followed by combining characters (accents typically) form what Unicode defines * as a grapheme cluster: a character as humans mean it in written texts. This class knows * about the concept and won't split a letter apart from its combining accents. It also * ensures all string comparisons happen on their canonically-composed representation, * ignoring e.g. the order in which accents are listed when a letter has many of them. * * @see https://unicode.org/reports/tr15/ * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ class UnicodeString extends AbstractUnicodeString { public function __construct(string $string = '') { if ('' === $string || normalizer_is_normalized($this->string = $string)) { return; } if (false === $string = normalizer_normalize($string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $this->string = $string; } public function append(string ...$suffix): static { $str = clone $this; $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix)); if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $rx = '/('; while (65535 < $length) { $rx .= '\X{65535}'; $length -= 65535; } $rx .= '\X{'.$length.'})/u'; $str = clone $this; $chunks = []; foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form); if ('' === $suffix || false === $suffix) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8'); } return $suffix === grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form); if ('' !== $string && false !== $string && $this->ignoreCase) { return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); if ('' === $needle || false === $needle) { return null; } try { $i = $this->ignoreCase ? grapheme_stripos($this->string, $needle, $offset) : grapheme_strpos($this->string, $needle, $offset); } catch (\ValueError) { return null; } return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); if ('' === $needle || false === $needle) { return null; } $string = $this->string; if (0 > $offset) { // workaround https://bugs.php.net/74264 if (0 > $offset += grapheme_strlen($needle)) { $string = grapheme_substr($string, 0, $offset); } $offset = 0; } $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); return false === $i ? null : $i; } public function join(array $strings, ?string $lastGlue = null): static { $str = parent::join($strings, $lastGlue); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); return $str; } public function length(): int { return grapheme_strlen($this->string); } public function normalize(int $form = self::NFC): static { $str = clone $this; if (\in_array($form, [self::NFC, self::NFKC], true)) { normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); } elseif (!\in_array($form, [self::NFD, self::NFKD], true)) { throw new InvalidArgumentException('Unsupported normalization form.'); } elseif (!normalizer_is_normalized($str->string, $form)) { $str->string = normalizer_normalize($str->string, $form); $str->ignoreCase = null; } return $str; } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; return $str; } public function replace(string $from, string $to): static { $str = clone $this; normalizer_is_normalized($from) ?: $from = normalizer_normalize($from); if ('' !== $from && false !== $from) { $tail = $str->string; $result = ''; $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; while ('' !== $tail && false !== $i = $indexOf($tail, $from)) { $slice = grapheme_substr($tail, 0, $i); $result .= $slice.$to; $tail = substr($tail, \strlen($slice) + \strlen($from)); } $str->string = $result.$tail; if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; } return $str; } public function replaceMatches(string $fromRegexp, string|callable $to): static { $str = parent::replaceMatches($fromRegexp, $to); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); return $str; } public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647); return $str; } public function splice(string $replacement, int $start = 0, ?int $length = null): static { $str = clone $this; $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; return $str; } public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (1 > $limit ??= 2147483647) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter.'u', $limit, $flags); } normalizer_is_normalized($delimiter) ?: $delimiter = normalizer_normalize($delimiter); if (false === $delimiter) { throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); } $str = clone $this; $tail = $this->string; $chunks = []; $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) { $str->string = grapheme_substr($tail, 0, $i); $chunks[] = clone $str; $tail = substr($tail, \strlen($str->string) + \strlen($delimiter)); --$limit; } $str->string = $tail; $chunks[] = clone $str; return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form); if ('' === $prefix || false === $prefix) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8'); } return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES); } /** * @return void */ public function __wakeup() { if (!\is_string($this->string)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); } public function __clone() { if (null === $this->ignoreCase) { normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); } $this->ignoreCase = false; } } Exception/RuntimeException.php 0000644 00000000560 15021235637 0012522 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface { } Exception/InvalidArgumentException.php 0000644 00000000600 15021235637 0014163 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } Exception/ExceptionInterface.php 0000644 00000000521 15021235637 0012774 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; interface ExceptionInterface extends \Throwable { } CHANGELOG.md 0000644 00000001572 15021235637 0006366 0 ustar 00 CHANGELOG ========= 6.2 --- * Add support for emoji in `AsciiSlugger` 5.4 --- * Add `trimSuffix()` and `trimPrefix()` methods 5.3 --- * Made `AsciiSlugger` fallback to parent locale's symbolsMap 5.2.0 ----- * added a `FrenchInflector` class 5.1.0 ----- * added the `AbstractString::reverse()` method * made `AbstractString::width()` follow POSIX.1-2001 * added `LazyString` which provides memoizing stringable objects * The component is not marked as `@experimental` anymore * added the `s()` helper method to get either an `UnicodeString` or `ByteString` instance, depending of the input string UTF-8 compliancy * added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()` * added `AbstractString::containsAny()` * allow passing a string of custom characters to `ByteString::fromRandom()` 5.0.0 ----- * added the component as experimental Slugger/AsciiSlugger.php 0000644 00000015725 15021235637 0011264 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Slugger; use Symfony\Component\Intl\Transliterator\EmojiTransliterator; use Symfony\Component\String\AbstractUnicodeString; use Symfony\Component\String\UnicodeString; use Symfony\Contracts\Translation\LocaleAwareInterface; if (!interface_exists(LocaleAwareInterface::class)) { throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".'); } /** * @author Titouan Galopin <galopintitouan@gmail.com> */ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface { private const LOCALE_TO_TRANSLITERATOR_ID = [ 'am' => 'Amharic-Latin', 'ar' => 'Arabic-Latin', 'az' => 'Azerbaijani-Latin', 'be' => 'Belarusian-Latin', 'bg' => 'Bulgarian-Latin', 'bn' => 'Bengali-Latin', 'de' => 'de-ASCII', 'el' => 'Greek-Latin', 'fa' => 'Persian-Latin', 'he' => 'Hebrew-Latin', 'hy' => 'Armenian-Latin', 'ka' => 'Georgian-Latin', 'kk' => 'Kazakh-Latin', 'ky' => 'Kirghiz-Latin', 'ko' => 'Korean-Latin', 'mk' => 'Macedonian-Latin', 'mn' => 'Mongolian-Latin', 'or' => 'Oriya-Latin', 'ps' => 'Pashto-Latin', 'ru' => 'Russian-Latin', 'sr' => 'Serbian-Latin', 'sr_Cyrl' => 'Serbian-Latin', 'th' => 'Thai-Latin', 'tk' => 'Turkmen-Latin', 'uk' => 'Ukrainian-Latin', 'uz' => 'Uzbek-Latin', 'zh' => 'Han-Latin', ]; private ?string $defaultLocale; private \Closure|array $symbolsMap = [ 'en' => ['@' => 'at', '&' => 'and'], ]; private bool|string $emoji = false; /** * Cache of transliterators per locale. * * @var \Transliterator[] */ private array $transliterators = []; public function __construct(?string $defaultLocale = null, array|\Closure|null $symbolsMap = null) { $this->defaultLocale = $defaultLocale; $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; } /** * @return void */ public function setLocale(string $locale) { $this->defaultLocale = $locale; } public function getLocale(): string { return $this->defaultLocale; } /** * @param bool|string $emoji true will use the same locale, * false will disable emoji, * and a string to use a specific locale */ public function withEmoji(bool|string $emoji = true): static { if (false !== $emoji && !class_exists(EmojiTransliterator::class)) { throw new \LogicException(sprintf('You cannot use the "%s()" method as the "symfony/intl" package is not installed. Try running "composer require symfony/intl".', __METHOD__)); } $new = clone $this; $new->emoji = $emoji; return $new; } public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString { $locale ??= $this->defaultLocale; $transliterator = []; if ($locale && ('de' === $locale || str_starts_with($locale, 'de_'))) { // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl) $transliterator = ['de-ASCII']; } elseif (\function_exists('transliterator_transliterate') && $locale) { $transliterator = (array) $this->createTransliterator($locale); } if ($emojiTransliterator = $this->createEmojiTransliterator($locale)) { $transliterator[] = $emojiTransliterator; } if ($this->symbolsMap instanceof \Closure) { // If the symbols map is passed as a closure, there is no need to fallback to the parent locale // as the closure can just provide substitutions for all locales of interest. $symbolsMap = $this->symbolsMap; array_unshift($transliterator, static fn ($s) => $symbolsMap($s, $locale)); } $unicodeString = (new UnicodeString($string))->ascii($transliterator); if (\is_array($this->symbolsMap)) { $map = null; if (isset($this->symbolsMap[$locale])) { $map = $this->symbolsMap[$locale]; } else { $parent = self::getParentLocale($locale); if ($parent && isset($this->symbolsMap[$parent])) { $map = $this->symbolsMap[$parent]; } } if ($map) { foreach ($map as $char => $replace) { $unicodeString = $unicodeString->replace($char, ' '.$replace.' '); } } } return $unicodeString ->replaceMatches('/[^A-Za-z0-9]++/', $separator) ->trim($separator) ; } private function createTransliterator(string $locale): ?\Transliterator { if (\array_key_exists($locale, $this->transliterators)) { return $this->transliterators[$locale]; } // Exact locale supported, cache and return if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) { return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } // Locale not supported and no parent, fallback to any-latin if (!$parent = self::getParentLocale($locale)) { return $this->transliterators[$locale] = null; } // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) { $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null; } private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator { if (\is_string($this->emoji)) { $locale = $this->emoji; } elseif (!$this->emoji) { return null; } while (null !== $locale) { try { return EmojiTransliterator::create("emoji-$locale"); } catch (\IntlException) { $locale = self::getParentLocale($locale); } } return null; } private static function getParentLocale(?string $locale): ?string { if (!$locale) { return null; } if (false === $str = strrchr($locale, '_')) { // no parent locale return null; } return substr($locale, 0, -\strlen($str)); } } Slugger/SluggerInterface.php 0000644 00000001314 15021235637 0012121 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Slugger; use Symfony\Component\String\AbstractUnicodeString; /** * Creates a URL-friendly slug from a given string. * * @author Titouan Galopin <galopintitouan@gmail.com> */ interface SluggerInterface { /** * Creates a slug for the given string and locale, using appropriate transliteration when needed. */ public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString; } ByteString.php 0000644 00000034474 15021235637 0007367 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a binary-safe string of bytes. * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ class ByteString extends AbstractString { private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; public function __construct(string $string = '') { $this->string = $string; } /* * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03) * * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16 * * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE). * * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/) */ public static function fromRandom(int $length = 16, ?string $alphabet = null): self { if ($length <= 0) { throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length)); } $alphabet ??= self::ALPHABET_ALPHANUMERIC; $alphabetSize = \strlen($alphabet); $bits = (int) ceil(log($alphabetSize, 2.0)); if ($bits <= 0 || $bits > 56) { throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); } $ret = ''; while ($length > 0) { $urandomLength = (int) ceil(2 * $length * $bits / 8.0); $data = random_bytes($urandomLength); $unpackedData = 0; $unpackedBits = 0; for ($i = 0; $i < $urandomLength && $length > 0; ++$i) { // Unpack 8 bits $unpackedData = ($unpackedData << 8) | \ord($data[$i]); $unpackedBits += 8; // While we have enough bits to select a character from the alphabet, keep // consuming the random data for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) { $index = ($unpackedData & ((1 << $bits) - 1)); $unpackedData >>= $bits; // Unfortunately, the alphabet size is not necessarily a power of two. // Worst case, it is 2^k + 1, which means we need (k+1) bits and we // have around a 50% chance of missing as k gets larger if ($index < $alphabetSize) { $ret .= $alphabet[$index]; --$length; } } } } return new static($ret); } public function bytesAt(int $offset): array { $str = $this->string[$offset] ?? ''; return '' === $str ? [] : [\ord($str)]; } public function append(string ...$suffix): static { $str = clone $this; $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); return $str; } public function camel(): static { $str = clone $this; $parts = explode(' ', trim(ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string)))); $parts[0] = 1 !== \strlen($parts[0]) && ctype_upper($parts[0]) ? $parts[0] : lcfirst($parts[0]); $str->string = implode('', $parts); return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $str = clone $this; $chunks = []; foreach (str_split($this->string, $length) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } if ('' !== $string && $this->ignoreCase) { return 0 === strcasecmp($string, $this->string); } return $string === $this->string; } public function folded(): static { $str = clone $this; $str->string = strtolower($str->string); return $str; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); return false === $i ? null : $i; } public function isUtf8(): bool { return '' === $this->string || preg_match('//u', $this->string); } public function join(array $strings, ?string $lastGlue = null): static { $str = clone $this; $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; $str->string = implode($this->string, $strings).$tail; return $str; } public function length(): int { return \strlen($this->string); } public function lower(): static { $str = clone $this; $str->string = strtolower($str->string); return $str; } public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; if ($this->ignoreCase) { $regexp .= 'i'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { throw new RuntimeException('Matching failed with error: '.preg_last_error_msg()); } } finally { restore_error_handler(); } return $matches; } public function padBoth(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH); return $str; } public function padEnd(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT); return $str; } public function padStart(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT); return $str; } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string; return $str; } public function replace(string $from, string $to): static { $str = clone $this; if ('' !== $from) { $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string); } return $str; } public function replaceMatches(string $fromRegexp, string|callable $to): static { if ($this->ignoreCase) { $fromRegexp .= 'i'; } $replace = \is_array($to) || $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace'; set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (null === $string = $replace($fromRegexp, $to, $this->string)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && str_ends_with($k, '_ERROR')) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; $str->string = $string; return $str; } public function reverse(): static { $str = clone $this; $str->string = strrev($str->string); return $str; } public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX); return $str; } public function snake(): static { $str = $this->camel(); $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); return $str; } public function splice(string $replacement, int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; } public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (1 > $limit ??= \PHP_INT_MAX) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter, $limit, $flags); } $str = clone $this; $chunks = $this->ignoreCase ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit) : explode($delimiter, $this->string, $limit); foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); } public function title(bool $allWords = false): static { $str = clone $this; $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string); return $str; } public function toUnicodeString(?string $fromEncoding = null): UnicodeString { return new UnicodeString($this->toCodePointString($fromEncoding)->string); } public function toCodePointString(?string $fromEncoding = null): CodePointString { $u = new CodePointString(); if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) { $u->string = $this->string; return $u; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { try { $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true); } catch (InvalidArgumentException $e) { if (!\function_exists('iconv')) { throw $e; } $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string); return $u; } } finally { restore_error_handler(); } if (!$validEncoding) { throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); } $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); return $u; } public function trim(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = trim($str->string, $chars); return $str; } public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = rtrim($str->string, $chars); return $str; } public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = ltrim($str->string, $chars); return $str; } public function upper(): static { $str = clone $this; $str->string = strtoupper($str->string); return $str; } public function width(bool $ignoreAnsiDecoration = true): int { $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string); return (new CodePointString($string))->width($ignoreAnsiDecoration); } } Resources/data/wcswidth_table_zero.php 0000644 00000036135 15021235637 0014216 0 ustar 00 <?php /* * This file has been auto-generated by the Symfony String Component for internal use. * * Unicode version: 16.0.0 * Date: 2024-09-11T08:21:22+00:00 */ return [ [ 768, 879, ], [ 1155, 1159, ], [ 1160, 1161, ], [ 1425, 1469, ], [ 1471, 1471, ], [ 1473, 1474, ], [ 1476, 1477, ], [ 1479, 1479, ], [ 1552, 1562, ], [ 1611, 1631, ], [ 1648, 1648, ], [ 1750, 1756, ], [ 1759, 1764, ], [ 1767, 1768, ], [ 1770, 1773, ], [ 1809, 1809, ], [ 1840, 1866, ], [ 1958, 1968, ], [ 2027, 2035, ], [ 2045, 2045, ], [ 2070, 2073, ], [ 2075, 2083, ], [ 2085, 2087, ], [ 2089, 2093, ], [ 2137, 2139, ], [ 2199, 2207, ], [ 2250, 2273, ], [ 2275, 2306, ], [ 2362, 2362, ], [ 2364, 2364, ], [ 2369, 2376, ], [ 2381, 2381, ], [ 2385, 2391, ], [ 2402, 2403, ], [ 2433, 2433, ], [ 2492, 2492, ], [ 2497, 2500, ], [ 2509, 2509, ], [ 2530, 2531, ], [ 2558, 2558, ], [ 2561, 2562, ], [ 2620, 2620, ], [ 2625, 2626, ], [ 2631, 2632, ], [ 2635, 2637, ], [ 2641, 2641, ], [ 2672, 2673, ], [ 2677, 2677, ], [ 2689, 2690, ], [ 2748, 2748, ], [ 2753, 2757, ], [ 2759, 2760, ], [ 2765, 2765, ], [ 2786, 2787, ], [ 2810, 2815, ], [ 2817, 2817, ], [ 2876, 2876, ], [ 2879, 2879, ], [ 2881, 2884, ], [ 2893, 2893, ], [ 2901, 2902, ], [ 2914, 2915, ], [ 2946, 2946, ], [ 3008, 3008, ], [ 3021, 3021, ], [ 3072, 3072, ], [ 3076, 3076, ], [ 3132, 3132, ], [ 3134, 3136, ], [ 3142, 3144, ], [ 3146, 3149, ], [ 3157, 3158, ], [ 3170, 3171, ], [ 3201, 3201, ], [ 3260, 3260, ], [ 3263, 3263, ], [ 3270, 3270, ], [ 3276, 3277, ], [ 3298, 3299, ], [ 3328, 3329, ], [ 3387, 3388, ], [ 3393, 3396, ], [ 3405, 3405, ], [ 3426, 3427, ], [ 3457, 3457, ], [ 3530, 3530, ], [ 3538, 3540, ], [ 3542, 3542, ], [ 3633, 3633, ], [ 3636, 3642, ], [ 3655, 3662, ], [ 3761, 3761, ], [ 3764, 3772, ], [ 3784, 3790, ], [ 3864, 3865, ], [ 3893, 3893, ], [ 3895, 3895, ], [ 3897, 3897, ], [ 3953, 3966, ], [ 3968, 3972, ], [ 3974, 3975, ], [ 3981, 3991, ], [ 3993, 4028, ], [ 4038, 4038, ], [ 4141, 4144, ], [ 4146, 4151, ], [ 4153, 4154, ], [ 4157, 4158, ], [ 4184, 4185, ], [ 4190, 4192, ], [ 4209, 4212, ], [ 4226, 4226, ], [ 4229, 4230, ], [ 4237, 4237, ], [ 4253, 4253, ], [ 4957, 4959, ], [ 5906, 5908, ], [ 5938, 5939, ], [ 5970, 5971, ], [ 6002, 6003, ], [ 6068, 6069, ], [ 6071, 6077, ], [ 6086, 6086, ], [ 6089, 6099, ], [ 6109, 6109, ], [ 6155, 6157, ], [ 6159, 6159, ], [ 6277, 6278, ], [ 6313, 6313, ], [ 6432, 6434, ], [ 6439, 6440, ], [ 6450, 6450, ], [ 6457, 6459, ], [ 6679, 6680, ], [ 6683, 6683, ], [ 6742, 6742, ], [ 6744, 6750, ], [ 6752, 6752, ], [ 6754, 6754, ], [ 6757, 6764, ], [ 6771, 6780, ], [ 6783, 6783, ], [ 6832, 6845, ], [ 6846, 6846, ], [ 6847, 6862, ], [ 6912, 6915, ], [ 6964, 6964, ], [ 6966, 6970, ], [ 6972, 6972, ], [ 6978, 6978, ], [ 7019, 7027, ], [ 7040, 7041, ], [ 7074, 7077, ], [ 7080, 7081, ], [ 7083, 7085, ], [ 7142, 7142, ], [ 7144, 7145, ], [ 7149, 7149, ], [ 7151, 7153, ], [ 7212, 7219, ], [ 7222, 7223, ], [ 7376, 7378, ], [ 7380, 7392, ], [ 7394, 7400, ], [ 7405, 7405, ], [ 7412, 7412, ], [ 7416, 7417, ], [ 7616, 7679, ], [ 8400, 8412, ], [ 8413, 8416, ], [ 8417, 8417, ], [ 8418, 8420, ], [ 8421, 8432, ], [ 11503, 11505, ], [ 11647, 11647, ], [ 11744, 11775, ], [ 12330, 12333, ], [ 12441, 12442, ], [ 42607, 42607, ], [ 42608, 42610, ], [ 42612, 42621, ], [ 42654, 42655, ], [ 42736, 42737, ], [ 43010, 43010, ], [ 43014, 43014, ], [ 43019, 43019, ], [ 43045, 43046, ], [ 43052, 43052, ], [ 43204, 43205, ], [ 43232, 43249, ], [ 43263, 43263, ], [ 43302, 43309, ], [ 43335, 43345, ], [ 43392, 43394, ], [ 43443, 43443, ], [ 43446, 43449, ], [ 43452, 43453, ], [ 43493, 43493, ], [ 43561, 43566, ], [ 43569, 43570, ], [ 43573, 43574, ], [ 43587, 43587, ], [ 43596, 43596, ], [ 43644, 43644, ], [ 43696, 43696, ], [ 43698, 43700, ], [ 43703, 43704, ], [ 43710, 43711, ], [ 43713, 43713, ], [ 43756, 43757, ], [ 43766, 43766, ], [ 44005, 44005, ], [ 44008, 44008, ], [ 44013, 44013, ], [ 64286, 64286, ], [ 65024, 65039, ], [ 65056, 65071, ], [ 66045, 66045, ], [ 66272, 66272, ], [ 66422, 66426, ], [ 68097, 68099, ], [ 68101, 68102, ], [ 68108, 68111, ], [ 68152, 68154, ], [ 68159, 68159, ], [ 68325, 68326, ], [ 68900, 68903, ], [ 68969, 68973, ], [ 69291, 69292, ], [ 69372, 69375, ], [ 69446, 69456, ], [ 69506, 69509, ], [ 69633, 69633, ], [ 69688, 69702, ], [ 69744, 69744, ], [ 69747, 69748, ], [ 69759, 69761, ], [ 69811, 69814, ], [ 69817, 69818, ], [ 69826, 69826, ], [ 69888, 69890, ], [ 69927, 69931, ], [ 69933, 69940, ], [ 70003, 70003, ], [ 70016, 70017, ], [ 70070, 70078, ], [ 70089, 70092, ], [ 70095, 70095, ], [ 70191, 70193, ], [ 70196, 70196, ], [ 70198, 70199, ], [ 70206, 70206, ], [ 70209, 70209, ], [ 70367, 70367, ], [ 70371, 70378, ], [ 70400, 70401, ], [ 70459, 70460, ], [ 70464, 70464, ], [ 70502, 70508, ], [ 70512, 70516, ], [ 70587, 70592, ], [ 70606, 70606, ], [ 70608, 70608, ], [ 70610, 70610, ], [ 70625, 70626, ], [ 70712, 70719, ], [ 70722, 70724, ], [ 70726, 70726, ], [ 70750, 70750, ], [ 70835, 70840, ], [ 70842, 70842, ], [ 70847, 70848, ], [ 70850, 70851, ], [ 71090, 71093, ], [ 71100, 71101, ], [ 71103, 71104, ], [ 71132, 71133, ], [ 71219, 71226, ], [ 71229, 71229, ], [ 71231, 71232, ], [ 71339, 71339, ], [ 71341, 71341, ], [ 71344, 71349, ], [ 71351, 71351, ], [ 71453, 71453, ], [ 71455, 71455, ], [ 71458, 71461, ], [ 71463, 71467, ], [ 71727, 71735, ], [ 71737, 71738, ], [ 71995, 71996, ], [ 71998, 71998, ], [ 72003, 72003, ], [ 72148, 72151, ], [ 72154, 72155, ], [ 72160, 72160, ], [ 72193, 72202, ], [ 72243, 72248, ], [ 72251, 72254, ], [ 72263, 72263, ], [ 72273, 72278, ], [ 72281, 72283, ], [ 72330, 72342, ], [ 72344, 72345, ], [ 72752, 72758, ], [ 72760, 72765, ], [ 72767, 72767, ], [ 72850, 72871, ], [ 72874, 72880, ], [ 72882, 72883, ], [ 72885, 72886, ], [ 73009, 73014, ], [ 73018, 73018, ], [ 73020, 73021, ], [ 73023, 73029, ], [ 73031, 73031, ], [ 73104, 73105, ], [ 73109, 73109, ], [ 73111, 73111, ], [ 73459, 73460, ], [ 73472, 73473, ], [ 73526, 73530, ], [ 73536, 73536, ], [ 73538, 73538, ], [ 73562, 73562, ], [ 78912, 78912, ], [ 78919, 78933, ], [ 90398, 90409, ], [ 90413, 90415, ], [ 92912, 92916, ], [ 92976, 92982, ], [ 94031, 94031, ], [ 94095, 94098, ], [ 94180, 94180, ], [ 113821, 113822, ], [ 118528, 118573, ], [ 118576, 118598, ], [ 119143, 119145, ], [ 119163, 119170, ], [ 119173, 119179, ], [ 119210, 119213, ], [ 119362, 119364, ], [ 121344, 121398, ], [ 121403, 121452, ], [ 121461, 121461, ], [ 121476, 121476, ], [ 121499, 121503, ], [ 121505, 121519, ], [ 122880, 122886, ], [ 122888, 122904, ], [ 122907, 122913, ], [ 122915, 122916, ], [ 122918, 122922, ], [ 123023, 123023, ], [ 123184, 123190, ], [ 123566, 123566, ], [ 123628, 123631, ], [ 124140, 124143, ], [ 124398, 124399, ], [ 125136, 125142, ], [ 125252, 125258, ], [ 917760, 917999, ], ]; Resources/data/wcswidth_table_wide.php 0000644 00000031005 15021235637 0014156 0 ustar 00 <?php /* * This file has been auto-generated by the Symfony String Component for internal use. * * Unicode version: 16.0.0 * Date: 2024-09-11T08:21:22+00:00 */ return [ [ 4352, 4447, ], [ 8986, 8987, ], [ 9001, 9001, ], [ 9002, 9002, ], [ 9193, 9196, ], [ 9200, 9200, ], [ 9203, 9203, ], [ 9725, 9726, ], [ 9748, 9749, ], [ 9776, 9783, ], [ 9800, 9811, ], [ 9855, 9855, ], [ 9866, 9871, ], [ 9875, 9875, ], [ 9889, 9889, ], [ 9898, 9899, ], [ 9917, 9918, ], [ 9924, 9925, ], [ 9934, 9934, ], [ 9940, 9940, ], [ 9962, 9962, ], [ 9970, 9971, ], [ 9973, 9973, ], [ 9978, 9978, ], [ 9981, 9981, ], [ 9989, 9989, ], [ 9994, 9995, ], [ 10024, 10024, ], [ 10060, 10060, ], [ 10062, 10062, ], [ 10067, 10069, ], [ 10071, 10071, ], [ 10133, 10135, ], [ 10160, 10160, ], [ 10175, 10175, ], [ 11035, 11036, ], [ 11088, 11088, ], [ 11093, 11093, ], [ 11904, 11929, ], [ 11931, 12019, ], [ 12032, 12245, ], [ 12272, 12287, ], [ 12288, 12288, ], [ 12289, 12291, ], [ 12292, 12292, ], [ 12293, 12293, ], [ 12294, 12294, ], [ 12295, 12295, ], [ 12296, 12296, ], [ 12297, 12297, ], [ 12298, 12298, ], [ 12299, 12299, ], [ 12300, 12300, ], [ 12301, 12301, ], [ 12302, 12302, ], [ 12303, 12303, ], [ 12304, 12304, ], [ 12305, 12305, ], [ 12306, 12307, ], [ 12308, 12308, ], [ 12309, 12309, ], [ 12310, 12310, ], [ 12311, 12311, ], [ 12312, 12312, ], [ 12313, 12313, ], [ 12314, 12314, ], [ 12315, 12315, ], [ 12316, 12316, ], [ 12317, 12317, ], [ 12318, 12319, ], [ 12320, 12320, ], [ 12321, 12329, ], [ 12330, 12333, ], [ 12334, 12335, ], [ 12336, 12336, ], [ 12337, 12341, ], [ 12342, 12343, ], [ 12344, 12346, ], [ 12347, 12347, ], [ 12348, 12348, ], [ 12349, 12349, ], [ 12350, 12350, ], [ 12353, 12438, ], [ 12441, 12442, ], [ 12443, 12444, ], [ 12445, 12446, ], [ 12447, 12447, ], [ 12448, 12448, ], [ 12449, 12538, ], [ 12539, 12539, ], [ 12540, 12542, ], [ 12543, 12543, ], [ 12549, 12591, ], [ 12593, 12686, ], [ 12688, 12689, ], [ 12690, 12693, ], [ 12694, 12703, ], [ 12704, 12735, ], [ 12736, 12773, ], [ 12783, 12783, ], [ 12784, 12799, ], [ 12800, 12830, ], [ 12832, 12841, ], [ 12842, 12871, ], [ 12880, 12880, ], [ 12881, 12895, ], [ 12896, 12927, ], [ 12928, 12937, ], [ 12938, 12976, ], [ 12977, 12991, ], [ 12992, 13055, ], [ 13056, 13311, ], [ 13312, 19903, ], [ 19904, 19967, ], [ 19968, 40959, ], [ 40960, 40980, ], [ 40981, 40981, ], [ 40982, 42124, ], [ 42128, 42182, ], [ 43360, 43388, ], [ 44032, 55203, ], [ 63744, 64109, ], [ 64110, 64111, ], [ 64112, 64217, ], [ 64218, 64255, ], [ 65040, 65046, ], [ 65047, 65047, ], [ 65048, 65048, ], [ 65049, 65049, ], [ 65072, 65072, ], [ 65073, 65074, ], [ 65075, 65076, ], [ 65077, 65077, ], [ 65078, 65078, ], [ 65079, 65079, ], [ 65080, 65080, ], [ 65081, 65081, ], [ 65082, 65082, ], [ 65083, 65083, ], [ 65084, 65084, ], [ 65085, 65085, ], [ 65086, 65086, ], [ 65087, 65087, ], [ 65088, 65088, ], [ 65089, 65089, ], [ 65090, 65090, ], [ 65091, 65091, ], [ 65092, 65092, ], [ 65093, 65094, ], [ 65095, 65095, ], [ 65096, 65096, ], [ 65097, 65100, ], [ 65101, 65103, ], [ 65104, 65106, ], [ 65108, 65111, ], [ 65112, 65112, ], [ 65113, 65113, ], [ 65114, 65114, ], [ 65115, 65115, ], [ 65116, 65116, ], [ 65117, 65117, ], [ 65118, 65118, ], [ 65119, 65121, ], [ 65122, 65122, ], [ 65123, 65123, ], [ 65124, 65126, ], [ 65128, 65128, ], [ 65129, 65129, ], [ 65130, 65131, ], [ 65281, 65283, ], [ 65284, 65284, ], [ 65285, 65287, ], [ 65288, 65288, ], [ 65289, 65289, ], [ 65290, 65290, ], [ 65291, 65291, ], [ 65292, 65292, ], [ 65293, 65293, ], [ 65294, 65295, ], [ 65296, 65305, ], [ 65306, 65307, ], [ 65308, 65310, ], [ 65311, 65312, ], [ 65313, 65338, ], [ 65339, 65339, ], [ 65340, 65340, ], [ 65341, 65341, ], [ 65342, 65342, ], [ 65343, 65343, ], [ 65344, 65344, ], [ 65345, 65370, ], [ 65371, 65371, ], [ 65372, 65372, ], [ 65373, 65373, ], [ 65374, 65374, ], [ 65375, 65375, ], [ 65376, 65376, ], [ 65504, 65505, ], [ 65506, 65506, ], [ 65507, 65507, ], [ 65508, 65508, ], [ 65509, 65510, ], [ 94176, 94177, ], [ 94178, 94178, ], [ 94179, 94179, ], [ 94180, 94180, ], [ 94192, 94193, ], [ 94208, 100343, ], [ 100352, 101119, ], [ 101120, 101589, ], [ 101631, 101631, ], [ 101632, 101640, ], [ 110576, 110579, ], [ 110581, 110587, ], [ 110589, 110590, ], [ 110592, 110847, ], [ 110848, 110882, ], [ 110898, 110898, ], [ 110928, 110930, ], [ 110933, 110933, ], [ 110948, 110951, ], [ 110960, 111355, ], [ 119552, 119638, ], [ 119648, 119670, ], [ 126980, 126980, ], [ 127183, 127183, ], [ 127374, 127374, ], [ 127377, 127386, ], [ 127488, 127490, ], [ 127504, 127547, ], [ 127552, 127560, ], [ 127568, 127569, ], [ 127584, 127589, ], [ 127744, 127776, ], [ 127789, 127797, ], [ 127799, 127868, ], [ 127870, 127891, ], [ 127904, 127946, ], [ 127951, 127955, ], [ 127968, 127984, ], [ 127988, 127988, ], [ 127992, 127994, ], [ 127995, 127999, ], [ 128000, 128062, ], [ 128064, 128064, ], [ 128066, 128252, ], [ 128255, 128317, ], [ 128331, 128334, ], [ 128336, 128359, ], [ 128378, 128378, ], [ 128405, 128406, ], [ 128420, 128420, ], [ 128507, 128511, ], [ 128512, 128591, ], [ 128640, 128709, ], [ 128716, 128716, ], [ 128720, 128722, ], [ 128725, 128727, ], [ 128732, 128735, ], [ 128747, 128748, ], [ 128756, 128764, ], [ 128992, 129003, ], [ 129008, 129008, ], [ 129292, 129338, ], [ 129340, 129349, ], [ 129351, 129535, ], [ 129648, 129660, ], [ 129664, 129673, ], [ 129679, 129734, ], [ 129742, 129756, ], [ 129759, 129769, ], [ 129776, 129784, ], [ 131072, 173791, ], [ 173792, 173823, ], [ 173824, 177977, ], [ 177978, 177983, ], [ 177984, 178205, ], [ 178206, 178207, ], [ 178208, 183969, ], [ 183970, 183983, ], [ 183984, 191456, ], [ 191457, 191471, ], [ 191472, 192093, ], [ 192094, 194559, ], [ 194560, 195101, ], [ 195102, 195103, ], [ 195104, 196605, ], [ 196608, 201546, ], [ 201547, 201551, ], [ 201552, 205743, ], [ 205744, 262141, ], ]; Resources/functions.php 0000644 00000001524 15021235637 0011245 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; if (!\function_exists(u::class)) { function u(?string $string = ''): UnicodeString { return new UnicodeString($string ?? ''); } } if (!\function_exists(b::class)) { function b(?string $string = ''): ByteString { return new ByteString($string ?? ''); } } if (!\function_exists(s::class)) { /** * @return UnicodeString|ByteString */ function s(?string $string = ''): AbstractString { $string ??= ''; return preg_match('//u', $string) ? new UnicodeString($string) : new ByteString($string); } } README.md 0000644 00000001053 15021235637 0006026 0 ustar 00 String Component ================ The String component provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way. Resources --------- * [Documentation](https://symfony.com/doc/current/components/string.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) AbstractUnicodeString.php 0000644 00000062361 15021235637 0011532 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a string of abstract Unicode characters. * * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). * This class is the abstract type to use as a type-hint when the logic you want to * implement is Unicode-aware but doesn't care about code points vs grapheme clusters. * * @author Nicolas Grekas <p@tchwork.com> * * @throws ExceptionInterface */ abstract class AbstractUnicodeString extends AbstractString { public const NFC = \Normalizer::NFC; public const NFD = \Normalizer::NFD; public const NFKC = \Normalizer::NFKC; public const NFKD = \Normalizer::NFKD; // all ASCII letters sorted by typical frequency of occurrence 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"; // the subset of folded case mappings that is not in lower case mappings private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ']; private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', 'ᾶ', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', 'ῆ', 'ῆι', 'ηι', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ὼι', 'ωι', 'ώι', 'ῶ', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ']; // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ʼn', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'DŽ', 'Dž', 'dž', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', 'ᴅ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', 'ᶅ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '⅓', '⅔', '⅕', '⅖', '⅗', '⅘', '⅙', '⅚', '⅛', '⅜', '⅝', '⅞', '⅟', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '⁅', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '⦅', '⦆', '。', '、', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '⦅', '⦆']; 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', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))']; private static array $transliterators = []; private static array $tableZero; private static array $tableWide; public static function fromCodePoints(int ...$codes): static { $string = ''; foreach ($codes as $code) { if (0x80 > $code %= 0x200000) { $string .= \chr($code); } elseif (0x800 > $code) { $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } } return new static($string); } /** * Generic UTF-8 to ASCII transliteration. * * Install the intl extension for best results. * * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs() */ public function ascii(array $rules = []): self { $str = clone $this; $s = $str->string; $str->string = ''; array_unshift($rules, 'nfd'); $rules[] = 'latin-ascii'; if (\function_exists('transliterator_transliterate')) { $rules[] = 'any-latin/bgn'; } $rules[] = 'nfkd'; $rules[] = '[:nonspacing mark:] remove'; while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) { if (0 < --$i) { $str->string .= substr($s, 0, $i); $s = substr($s, $i); } if (!$rule = array_shift($rules)) { $rules = []; // An empty rule interrupts the next ones } if ($rule instanceof \Transliterator) { $s = $rule->transliterate($s); } elseif ($rule instanceof \Closure) { $s = $rule($s); } elseif ($rule) { if ('nfd' === $rule = strtolower($rule)) { normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD); } elseif ('nfkd' === $rule) { normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD); } elseif ('[:nonspacing mark:] remove' === $rule) { $s = preg_replace('/\p{Mn}++/u', '', $s); } elseif ('latin-ascii' === $rule) { $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s); } elseif ('de-ascii' === $rule) { $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s); $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); } elseif (\function_exists('transliterator_transliterate')) { if (null === $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule)) { if ('any-latin/bgn' === $rule) { $rule = 'any-latin'; $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule); } if (null === $transliterator) { throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule)); } self::$transliterators['any-latin/bgn'] = $transliterator; } $s = $transliterator->transliterate($s); } } elseif (!\function_exists('iconv')) { $s = preg_replace('/[^\x00-\x7F]/u', '?', $s); } else { $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) { $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]); if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) { throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); } return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?'); }, $s); } } $str->string .= $s; return $str; } public function camel(): static { $str = clone $this; $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) { static $i = 0; return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string))); return $str; } /** * @return int[] */ public function codePointsAt(int $offset): array { $str = $this->slice($offset, 1); if ('' === $str->string) { return []; } $codePoints = []; foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { $codePoints[] = mb_ord($c, 'UTF-8'); } return $codePoints; } public function folded(bool $compat = true): static { $str = clone $this; if (!$compat || !\defined('Normalizer::NFKC_CF')) { $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC); $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $str->string), 'UTF-8'); } else { $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF); } return $str; } public function join(array $strings, ?string $lastGlue = null): static { $str = clone $this; $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; $str->string = implode($this->string, $strings).$tail; if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function lower(): static { $str = clone $this; $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8'); return $str; } public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; if ($this->ignoreCase) { $regexp .= 'i'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { throw new RuntimeException('Matching failed with error: '.preg_last_error_msg()); } } finally { restore_error_handler(); } return $matches; } public function normalize(int $form = self::NFC): static { if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) { throw new InvalidArgumentException('Unsupported normalization form.'); } $str = clone $this; normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); return $str; } public function padBoth(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_BOTH); } public function padEnd(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_RIGHT); } public function padStart(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_LEFT); } public function replaceMatches(string $fromRegexp, string|callable $to): static { if ($this->ignoreCase) { $fromRegexp .= 'i'; } if (\is_array($to) || $to instanceof \Closure) { $replace = 'preg_replace_callback'; $to = static function (array $m) use ($to): string { $to = $to($m); if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) { throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.'); } return $to; }; } elseif ('' !== $to && !preg_match('//u', $to)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } else { $replace = 'preg_replace'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && str_ends_with($k, '_ERROR')) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; $str->string = $string; return $str; } public function reverse(): static { $str = clone $this; $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY))); return $str; } public function snake(): static { $str = $this->camel(); $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'); return $str; } public function title(bool $allWords = false): static { $str = clone $this; $limit = $allWords ? -1 : 1; $str->string = preg_replace_callback('/\b./u', static fn (array $m): string => mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'), $str->string, $limit); return $str; } public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string); return $str; } public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{[$chars]++$}uD", '', $str->string); return $str; } public function trimPrefix($prefix): static { if (!$this->ignoreCase) { return parent::trimPrefix($prefix); } $str = clone $this; if ($prefix instanceof \Traversable) { $prefix = iterator_to_array($prefix, false); } elseif ($prefix instanceof parent) { $prefix = $prefix->string; } $prefix = implode('|', array_map('preg_quote', (array) $prefix)); $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string); return $str; } public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++}uD", '', $str->string); return $str; } public function trimSuffix($suffix): static { if (!$this->ignoreCase) { return parent::trimSuffix($suffix); } $str = clone $this; if ($suffix instanceof \Traversable) { $suffix = iterator_to_array($suffix, false); } elseif ($suffix instanceof parent) { $suffix = $suffix->string; } $suffix = implode('|', array_map('preg_quote', (array) $suffix)); $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string); return $str; } public function upper(): static { $str = clone $this; $str->string = mb_strtoupper($str->string, 'UTF-8'); return $str; } public function width(bool $ignoreAnsiDecoration = true): int { $width = 0; $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string); if (str_contains($s, "\r")) { $s = str_replace(["\r\n", "\r"], "\n", $s); } if (!$ignoreAnsiDecoration) { $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s); } foreach (explode("\n", $s) as $s) { if ($ignoreAnsiDecoration) { $s = preg_replace('/(?:\x1B(?: \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E] | [P\]X^_] .*? \x1B\\\\ | [\x41-\x7E] )|[\p{Cc}\x7F]++)/xu', '', $s); } $lineWidth = $this->wcswidth($s); if ($lineWidth > $width) { $width = $lineWidth; } } return $width; } private function pad(int $len, self $pad, int $type): static { $sLen = $this->length(); if ($len <= $sLen) { return clone $this; } $padLen = $pad->length(); $freeLen = $len - $sLen; $len = $freeLen % $padLen; switch ($type) { case \STR_PAD_RIGHT: return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); case \STR_PAD_LEFT: return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); case \STR_PAD_BOTH: $freeLen /= 2; $rightLen = ceil($freeLen); $len = $rightLen % $padLen; $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : '')); $leftLen = floor($freeLen); $len = $leftLen % $padLen; return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : '')); default: throw new InvalidArgumentException('Invalid padding type.'); } } /** * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c. */ private function wcswidth(string $string): int { $width = 0; foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { $codePoint = mb_ord($c, 'UTF-8'); if (0 === $codePoint // NULL || 0x034F === $codePoint // COMBINING GRAPHEME JOINER || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK || 0x2028 === $codePoint // LINE SEPARATOR || 0x2029 === $codePoint // PARAGRAPH SEPARATOR || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR ) { continue; } // Non printable characters if (32 > $codePoint // C0 control characters || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL ) { return -1; } self::$tableZero ??= require __DIR__.'/Resources/data/wcswidth_table_zero.php'; if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) { $lbound = 0; while ($ubound >= $lbound) { $mid = floor(($lbound + $ubound) / 2); if ($codePoint > self::$tableZero[$mid][1]) { $lbound = $mid + 1; } elseif ($codePoint < self::$tableZero[$mid][0]) { $ubound = $mid - 1; } else { continue 2; } } } self::$tableWide ??= require __DIR__.'/Resources/data/wcswidth_table_wide.php'; if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) { $lbound = 0; while ($ubound >= $lbound) { $mid = floor(($lbound + $ubound) / 2); if ($codePoint > self::$tableWide[$mid][1]) { $lbound = $mid + 1; } elseif ($codePoint < self::$tableWide[$mid][0]) { $ubound = $mid - 1; } else { $width += 2; continue 2; } } } ++$width; } return $width; } } LICENSE 0000644 00000002054 15021235637 0005556 0 ustar 00 Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
| ver. 1.4 |
Github
|
.
| PHP 8.1.29 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка