v1.0 Initial commit of project

This commit is contained in:
2026-01-01 10:54:18 +01:00
commit 768cf78b57
990 changed files with 241213 additions and 0 deletions

5
vendor/symfony/string/.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
/Resources/bin/update-data.php export-ignore
/Resources/WcswidthDataGenerator.php export-ignore
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.git* export-ignore

View File

@@ -0,0 +1,8 @@
Please do not submit any Pull Requests here. They will be closed.
---
Please submit your PR here instead:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

View File

@@ -0,0 +1,20 @@
name: Close Pull Request
on:
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

3
vendor/symfony/string/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
composer.lock
phpunit.xml
vendor/

718
vendor/symfony/string/AbstractString.php vendored Normal file
View File

@@ -0,0 +1,718 @@
<?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 $string = '';
protected ?bool $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;
public function kebab(): static
{
return $this->snake()->replace('_', '-');
}
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|TruncateMode $cut = TruncateMode::Char): static
{
$stringLength = $this->length();
if ($stringLength <= $length) {
return clone $this;
}
$ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0;
if ($length < $ellipsisLength) {
$ellipsisLength = 0;
}
$desiredLength = $length;
if (TruncateMode::WordAfter === $cut || !$cut) {
if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
return clone $this;
}
$length += $ellipsisLength;
} elseif (TruncateMode::WordBefore === $cut && null !== $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
$length += $ellipsisLength;
}
$str = $this->slice(0, $length - $ellipsisLength);
if (TruncateMode::WordBefore === $cut) {
if (0 === $ellipsisLength && $desiredLength === $this->indexOf([' ', "\r", "\n", "\t"], $length)) {
return $str;
}
$str = $str->beforeLast([' ', "\r", "\n", "\t"]);
}
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;
}
}

View File

@@ -0,0 +1,664 @@
<?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;
}
/**
* @param string $locale In the format language_region (e.g. tr_TR)
*/
public function localeLower(string $locale): static
{
if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Lower')) {
$str = clone $this;
$str->string = $transliterator->transliterate($str->string);
return $str;
}
return $this->lower();
}
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;
}
/**
* @param string $locale In the format language_region (e.g. tr_TR)
*/
public function localeTitle(string $locale): static
{
if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Title')) {
$str = clone $this;
$str->string = $transliterator->transliterate($str->string);
return $str;
}
return $this->title();
}
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;
}
/**
* @param string $locale In the format language_region (e.g. tr_TR)
*/
public function localeUpper(string $locale): static
{
if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Upper')) {
$str = clone $this;
$str->string = $transliterator->transliterate($str->string);
return $str;
}
return $this->upper();
}
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;
}
private function getLocaleTransliterator(string $locale, string $id): ?\Transliterator
{
$rule = $locale.'-'.$id;
if (\array_key_exists($rule, self::$transliterators)) {
return self::$transliterators[$rule];
}
if (null !== $transliterator = self::$transliterators[$rule] = \Transliterator::create($rule)) {
return $transliterator;
}
// Try to find a parent locale (nl_BE -> nl)
if (false === $i = strpos($locale, '_')) {
return null;
}
$parentRule = substr_replace($locale, '-'.$id, $i);
// Parent locale was already cached, return and store as current locale
if (\array_key_exists($parentRule, self::$transliterators)) {
return self::$transliterators[$rule] = self::$transliterators[$parentRule];
}
// Create transliterator based on parent locale and cache the result on both initial and parent locale values
$transliterator = \Transliterator::create($parentRule);
return self::$transliterators[$rule] = self::$transliterators[$parentRule] = $transliterator;
}
}

490
vendor/symfony/string/ByteString.php vendored Normal file
View File

@@ -0,0 +1,490 @@
<?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 Random\Randomizer;
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.');
}
if (\PHP_VERSION_ID >= 80300) {
return new static((new Randomizer())->getBytesFromString($alphabet, $length));
}
$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 = 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);
}
}

51
vendor/symfony/string/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,51 @@
CHANGELOG
=========
7.2
---
* Add `TruncateMode` enum to handle more truncate methods
* Add the `AbstractString::kebab()` method
7.1
---
* Add `localeLower()`, `localeUpper()`, `localeTitle()` methods to `AbstractUnicodeString`
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

View File

@@ -0,0 +1,260 @@
<?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));
}
}

View File

@@ -0,0 +1,16 @@
<?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
{
}

View File

@@ -0,0 +1,16 @@
<?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
{
}

View File

@@ -0,0 +1,16 @@
<?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
{
}

View File

@@ -0,0 +1,586 @@
<?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']],
// fax (faxes, faxxes)
['xaf', 3, true, true, ['faxes', 'faxxes']],
// boxes (box)
['xo', 2, false, true, 'oxes'],
// indexes (index), matrixes (matrix), appendices (appendix)
['x', 1, true, false, ['ces', 'xes']],
// 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'];
}
}

View File

@@ -0,0 +1,151 @@
<?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);
}
}

View File

@@ -0,0 +1,33 @@
<?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;
}

View File

@@ -0,0 +1,126 @@
<?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 SpanishInflector implements InflectorInterface
{
/**
* A list of all rules for pluralise.
*
* @see https://www.spanishdict.com/guide/spanish-plural-noun-forms
* @see https://www.rae.es/gram%C3%A1tica/morfolog%C3%ADa/la-formaci%C3%B3n-del-plural-plurales-en-s-y-plurales-en-es-reglas-generales
*/
// First entry: regex
// Second entry: replacement
private const PLURALIZE_REGEXP = [
// Specials sí, no
['/(sí|no)$/i', '\1es'],
// Words ending with vowel must use -s (RAE 3.2a, 3.2c)
['/(a|e|i|o|u|á|é|í|ó|ú)$/i', '\1s'],
// Word ending in s or x and the previous letter is accented (RAE 3.2n)
['/ás$/i', 'ases'],
['/és$/i', 'eses'],
['/ís$/i', 'ises'],
['/ós$/i', 'oses'],
['/ús$/i', 'uses'],
// Words ending in -ión must changed to -iones
['/ión$/i', '\1iones'],
// Words ending in some consonants must use -es (RAE 3.2k)
['/(l|r|n|d|j|s|x|ch|y)$/i', '\1es'],
// Word ending in z, must changed to ces
['/(z)$/i', 'ces'],
];
/**
* A list of all rules for singularize.
*/
private const SINGULARIZE_REGEXP = [
// Specials sí, no
['/(sí|no)es$/i', '\1'],
// Words ending in -ión must changed to -iones
['/iones$/i', '\1ión'],
// Word ending in z, must changed to ces
['/ces$/i', 'z'],
// Word ending in s or x and the previous letter is accented (RAE 3.2n)
['/(\w)ases$/i', '\1ás'],
['/eses$/i', 'és'],
['/ises$/i', 'ís'],
['/(\w{2,})oses$/i', '\1ós'],
['/(\w)uses$/i', '\1ús'],
// Words ending in some consonants and -es, must be the consonants
['/(l|r|n|d|j|s|x|ch|y)e?s$/i', '\1'],
// Words ended with vowel and s, must be vowel
['/(a|e|i|o|u|á|é|ó|í|ú)s$/i', '\1'],
];
private const UNINFLECTED_RULES = [
// Words ending with pies (RAE 3.2n)
'/.*(piés)$/i',
];
private const UNINFLECTED = '/^(lunes|martes|miércoles|jueves|viernes|análisis|torax|yo|pies)$/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
{
foreach (self::UNINFLECTED_RULES as $rule) {
if (1 === preg_match($rule, $word)) {
return true;
}
}
return 1 === preg_match(self::UNINFLECTED, $word);
}
}

19
vendor/symfony/string/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
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.

145
vendor/symfony/string/LazyString.php vendored Normal file
View File

@@ -0,0 +1,145 @@
<?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 ($r->isAnonymous() || !$class = $r->getClosureCalledClass()) {
return $r->name;
}
$class = $class->name;
$method = $r->name;
} else {
$class = get_debug_type($callback);
$method = '__invoke';
}
return $class.'::'.$method;
}
}

14
vendor/symfony/string/README.md vendored Normal file
View File

@@ -0,0 +1,14 @@
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)

View File

@@ -0,0 +1,109 @@
<?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\Resources;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\String\Exception\RuntimeException;
use Symfony\Component\VarExporter\VarExporter;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @internal
*/
final class WcswidthDataGenerator
{
private HttpClientInterface $client;
public function __construct(
private string $outDir,
) {
$this->client = HttpClient::createForBaseUri('https://www.unicode.org/Public/UNIDATA/');
}
public function generate(): void
{
$this->writeWideWidthData();
$this->writeZeroWidthData();
}
private function writeWideWidthData(): void
{
if (!preg_match('/^# EastAsianWidth-(\d+\.\d+\.\d+)\.txt/', $content = $this->client->request('GET', 'EastAsianWidth.txt')->getContent(), $matches)) {
throw new RuntimeException('The Unicode version could not be determined.');
}
$version = $matches[1];
if (!preg_match_all('/^([A-H\d]{4,})(?:\.\.([A-H\d]{4,}))? +; [W|F]/m', $content, $matches, \PREG_SET_ORDER)) {
throw new RuntimeException('The wide width pattern did not match anything.');
}
$this->write('wcswidth_table_wide.php', $version, $matches);
}
private function writeZeroWidthData(): void
{
if (!preg_match('/^# DerivedGeneralCategory-(\d+\.\d+\.\d+)\.txt/', $content = $this->client->request('GET', 'extracted/DerivedGeneralCategory.txt')->getContent(), $matches)) {
throw new RuntimeException('The Unicode version could not be determined.');
}
$version = $matches[1];
if (!preg_match_all('/^([A-H\d]{4,})(?:\.\.([A-H\d]{4,}))? *; (?:Me|Mn)/m', $content, $matches, \PREG_SET_ORDER)) {
throw new RuntimeException('The zero width pattern did not match anything.');
}
$this->write('wcswidth_table_zero.php', $version, $matches);
}
private function write(string $fileName, string $version, array $rawData): void
{
$content = $this->getHeader($version).'return '.VarExporter::export($this->format($rawData)).";\n";
if (!file_put_contents($this->outDir.'/'.$fileName, $content)) {
throw new RuntimeException(\sprintf('The "%s" file could not be written.', $fileName));
}
}
private function getHeader(string $version): string
{
$date = (new \DateTimeImmutable())->format('c');
return <<<EOT
<?php
/*
* This file has been auto-generated by the Symfony String Component for internal use.
*
* Unicode version: $version
* Date: $date
*/
EOT;
}
private function format(array $rawData): array
{
$data = array_map(static function (array $row): array {
$start = $row[1];
$end = $row[2] ?? $start;
return [hexdec($start), hexdec($end)];
}, $rawData);
usort($data, static fn (array $a, array $b): int => $a[0] - $b[0]);
return $data;
}
}

View File

@@ -0,0 +1,59 @@
<?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.
*/
use Symfony\Component\String\Resources\WcswidthDataGenerator;
if ('cli' !== \PHP_SAPI) {
throw new Exception('This script must be run from the command line.');
}
error_reporting(\E_ALL);
set_error_handler(static function (int $type, string $msg, string $file, int $line): void {
throw new ErrorException($msg, 0, $type, $file, $line);
});
set_exception_handler(static function (Throwable $exception): void {
echo "\n";
$cause = $exception;
$root = true;
while (null !== $cause) {
if (!$root) {
echo "Caused by\n";
}
echo $cause::class.': '.$cause->getMessage()."\n";
echo "\n";
echo $cause->getFile().':'.$cause->getLine()."\n";
echo $cause->getTraceAsString()."\n";
$cause = $cause->getPrevious();
$root = false;
}
});
$autoload = __DIR__.'/../../vendor/autoload.php';
if (!file_exists($autoload)) {
echo wordwrap('You should run "composer install" in the component before running this script.', 75)." Aborting.\n";
exit(1);
}
require_once $autoload;
echo "Generating wcswidth tables data...\n";
(new WcswidthDataGenerator(dirname(__DIR__).'/data'))->generate();
echo "Done.\n";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
<?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);
}
}

View File

@@ -0,0 +1,207 @@
<?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\Emoji\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 \Closure|array $symbolsMap = [
'en' => ['@' => 'at', '&' => 'and'],
];
private bool|string $emoji = false;
/**
* Cache of transliterators per locale.
*
* @var \Transliterator[]
*/
private array $transliterators = [];
public function __construct(
private ?string $defaultLocale = null,
array|\Closure|null $symbolsMap = null,
) {
$this->symbolsMap = $symbolsMap ?? $this->symbolsMap;
}
public function setLocale(string $locale): void
{
$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/emoji" package is not installed. Try running "composer require symfony/emoji".', __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));
}
}

View File

@@ -0,0 +1,27 @@
<?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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,747 @@
<?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\Tests;
use Symfony\Component\String\Exception\InvalidArgumentException;
abstract class AbstractUnicodeTestCase extends AbstractAsciiTestCase
{
public static function provideWidth(): array
{
return array_merge(
parent::provideWidth(),
[
[14, '<<<END
This is a
multiline text
END'],
]
);
}
public function testCreateFromStringWithInvalidUtf8Input()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString("\xE9");
}
public function testAscii()
{
$s = static::createFromString('Dieser Wert sollte größer oder gleich');
$this->assertSame('Dieser Wert sollte grosser oder gleich', (string) $s->ascii());
$this->assertSame('Dieser Wert sollte groesser oder gleich', (string) $s->ascii(['de-ASCII']));
}
public function testAsciiClosureRule()
{
$rule = fn ($c) => str_replace('ö', 'OE', $c);
$s = static::createFromString('Dieser Wert sollte größer oder gleich');
$this->assertSame('Dieser Wert sollte grOEsser oder gleich', (string) $s->ascii([$rule]));
}
/**
* @dataProvider provideLocaleLower
*
* @requires extension intl
*/
public function testLocaleLower(string $locale, string $expected, string $origin)
{
$instance = static::createFromString($origin)->localeLower($locale);
$this->assertNotSame(static::createFromString($origin), $instance);
$this->assertEquals(static::createFromString($expected), $instance);
$this->assertSame($expected, (string) $instance);
}
/**
* @dataProvider provideLocaleUpper
*
* @requires extension intl
*/
public function testLocaleUpper(string $locale, string $expected, string $origin)
{
$instance = static::createFromString($origin)->localeUpper($locale);
$this->assertNotSame(static::createFromString($origin), $instance);
$this->assertEquals(static::createFromString($expected), $instance);
$this->assertSame($expected, (string) $instance);
}
/**
* @dataProvider provideLocaleTitle
*
* @requires extension intl
*/
public function testLocaleTitle(string $locale, string $expected, string $origin)
{
$instance = static::createFromString($origin)->localeTitle($locale);
$this->assertNotSame(static::createFromString($origin), $instance);
$this->assertEquals(static::createFromString($expected), $instance);
$this->assertSame($expected, (string) $instance);
}
public static function provideCreateFromCodePoint(): array
{
return [
['', []],
['*', [42]],
['AZ', [65, 90]],
['€', [8364]],
['€', [0x20AC]],
['Ʃ', [425]],
['Ʃ', [0x1A9]],
['☢☎❄', [0x2622, 0x260E, 0x2744]],
];
}
public static function provideBytesAt(): array
{
return array_merge(
parent::provideBytesAt(),
[
[[0xC3, 0xA4], 'Späßchen', 2],
[[0xC3, 0x9F], 'Späßchen', -5],
]
);
}
/**
* @dataProvider provideCodePointsAt
*/
public function testCodePointsAt(array $expected, string $string, int $offset, ?int $form = null)
{
if (2 !== grapheme_strlen('च्छे') && 'नमस्ते' === $string) {
$this->markTestSkipped('Skipping due to issue ICU-21661.');
}
$instance = static::createFromString($string);
$instance = $form ? $instance->normalize($form) : $instance;
$this->assertSame($expected, $instance->codePointsAt($offset));
}
public static function provideCodePointsAt(): array
{
$data = [
[[], '', 0],
[[], 'a', 1],
[[0x53], 'Späßchen', 0],
[[0xE4], 'Späßchen', 2],
[[0xDF], 'Späßchen', -5],
];
// Skip this set if we encounter an issue in PCRE2
// @see https://github.com/PCRE2Project/pcre2/issues/361
if (3 === grapheme_strlen('☢☎❄')) {
$data[] = [[0x260E], '☢☎❄', 1];
}
return $data;
}
public static function provideLength(): array
{
return [
[1, 'a'],
[1, 'ß'],
[2, 'is'],
[3, 'PHP'],
[3, '한국어'],
[4, 'Java'],
[7, 'Symfony'],
[10, 'pineapples'],
[22, 'Symfony is super cool!'],
];
}
public static function provideIndexOf(): array
{
return array_merge(
parent::provideIndexOf(),
[
[1, '한국어', '국', 0],
[1, '한국어', '국', 1],
[null, '한국어', '국', 2],
[8, 'der Straße nach Paris', 'ß', 4],
]
);
}
public static function provideIndexOfIgnoreCase(): array
{
return array_merge(
parent::provideIndexOfIgnoreCase(),
[
[3, 'DÉJÀ', 'À', 0],
[3, 'DÉJÀ', 'à', 0],
[1, 'DÉJÀ', 'É', 1],
[1, 'DÉJÀ', 'é', 1],
[1, 'aςσb', 'ΣΣ', 0],
[16, 'der Straße nach Paris', 'Paris', 0],
[8, 'der Straße nach Paris', 'ß', 4],
]
);
}
public static function provideIndexOfLast(): array
{
return array_merge(
parent::provideIndexOfLast(),
[
[null, '한국어', '', 0],
[1, '한국어', '국', 0],
[5, '한국어어어어국국', '어', 0],
// see https://bugs.php.net/bug.php?id=74264
[15, 'abcdéf12é45abcdéf', 'é', 0],
[8, 'abcdéf12é45abcdéf', 'é', -4],
]
);
}
public static function provideIndexOfLastIgnoreCase(): array
{
return array_merge(
parent::provideIndexOfLastIgnoreCase(),
[
[null, '한국어', '', 0],
[3, 'DÉJÀ', 'à', 0],
[3, 'DÉJÀ', 'À', 0],
[6, 'DÉJÀÀÀÀ', 'à', 0],
[6, 'DÉJÀÀÀÀ', 'à', 3],
[5, 'DÉJÀÀÀÀ', 'àà', 0],
[2, 'DÉJÀÀÀÀ', 'jà', 0],
[2, 'DÉJÀÀÀÀ', 'jà', -5],
[6, 'DÉJÀÀÀÀ!', 'à', -2],
// see https://bugs.php.net/bug.php?id=74264
[5, 'DÉJÀÀÀÀ', 'à', -2],
[15, 'abcdéf12é45abcdéf', 'é', 0],
[8, 'abcdéf12é45abcdéf', 'é', -4],
[1, 'aςσb', 'ΣΣ', 0],
]
);
}
public static function provideSplit(): array
{
return array_merge(
parent::provideSplit(),
[
[
'會|意|文|字|/|会|意|文|字',
'|',
[
static::createFromString('會'),
static::createFromString('意'),
static::createFromString('文'),
static::createFromString('字'),
static::createFromString('/'),
static::createFromString('会'),
static::createFromString('意'),
static::createFromString('文'),
static::createFromString('字'),
],
null,
],
[
'會|意|文|字|/|会|意|文|字',
'|',
[
static::createFromString('會'),
static::createFromString('意'),
static::createFromString('文'),
static::createFromString('字'),
static::createFromString('/|会|意|文|字'),
],
5,
],
]
);
}
public static function provideChunk(): array
{
return array_merge(
parent::provideChunk(),
[
[
'déjà',
[
static::createFromString('d'),
static::createFromString('é'),
static::createFromString('j'),
static::createFromString('à'),
],
1,
],
[
'déjà',
[
static::createFromString('dé'),
static::createFromString('jà'),
],
2,
],
]
);
}
public function testTrimWithInvalidUtf8CharList()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString('Symfony')->trim("\xE9");
}
public function testTrimStartWithInvalidUtf8CharList()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString('Symfony')->trimStart("\xE9");
}
public function testTrimEndWithInvalidUtf8CharList()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString('Symfony')->trimEnd("\xE9");
}
public static function provideLower(): array
{
return array_merge(
parent::provideLower(),
[
// French
['garçon', 'garçon'],
['garçon', 'GARÇON'],
["œuvre d'art", "Œuvre d'Art"],
// Spanish
['el niño', 'El Niño'],
// Romanian
['împărat', 'Împărat'],
// Random symbols
['déjà σσς i̇iıi', 'DÉJÀ Σσς İIıi'],
]
);
}
public static function provideLocaleLower(): array
{
return [
// Lithuanian
// Introduce an explicit dot above when lowercasing capital I's and J's
// whenever there are more accents above.
// LATIN CAPITAL LETTER I WITH OGONEK -> LATIN SMALL LETTER I WITH OGONEK
['lt', 'į', 'Į'],
// LATIN CAPITAL LETTER I WITH GRAVE -> LATIN SMALL LETTER I COMBINING DOT ABOVE
['lt', 'i̇̀', 'Ì'],
// LATIN CAPITAL LETTER I WITH ACUTE -> LATIN SMALL LETTER I COMBINING DOT ABOVE COMBINING ACUTE ACCENT
['lt', 'i̇́', 'Í'],
// LATIN CAPITAL LETTER I WITH TILDE -> LATIN SMALL LETTER I COMBINING DOT ABOVE COMBINING TILDE
['lt', 'i̇̃', 'Ĩ'],
// Turkish and Azeri
// When lowercasing, remove dot_above in the sequence I + dot_above, which will turn into 'i'.
// LATIN CAPITAL LETTER I WITH DOT ABOVE -> LATIN SMALL LETTER I
['tr', 'i', 'İ'],
['tr_TR', 'i', 'İ'],
['az', 'i', 'İ'],
// Default casing rules
// LATIN CAPITAL LETTER I WITH DOT ABOVE -> LATIN SMALL LETTER I COMBINING DOT ABOVE
['en_US', 'i̇', 'İ'],
['en', 'i̇', 'İ'],
];
}
public static function provideLocaleUpper(): array
{
return [
// Turkish and Azeri
// When uppercasing, i turns into a dotted capital I
// LATIN SMALL LETTER I -> LATIN CAPITAL LETTER I WITH DOT ABOVE
['tr', 'İ', 'i'],
['tr_TR', 'İ', 'i'],
['az', 'İ', 'i'],
// Greek
// Remove accents when uppercasing
// GREEK SMALL LETTER ALPHA WITH TONOS -> GREEK CAPITAL LETTER ALPHA
['el', 'Α', 'ά'],
['el_GR', 'Α', 'ά'],
// Default casing rules
// GREEK SMALL LETTER ALPHA WITH TONOS -> GREEK CAPITAL LETTER ALPHA WITH TONOS
['en_US', 'Ά', 'ά'],
['en', 'Ά', 'ά'],
];
}
public static function provideLocaleTitle(): array
{
return [
// Greek
// Titlecasing words, should keep the accents on the first letter
['el', 'Άδικος', 'άδικος'],
['el_GR', 'Άδικος', 'άδικος'],
['en', 'Άδικος', 'άδικος'],
// Dutch
// Title casing should treat 'ij' as one character
['nl_NL', 'IJssel', 'ijssel'],
['nl_BE', 'IJssel', 'ijssel'],
['nl', 'IJssel', 'ijssel'],
// Default casing rules
['en', 'Ijssel', 'ijssel'],
];
}
public static function provideUpper(): array
{
return array_merge(
parent::provideUpper(),
[
// French
['GARÇON', 'garçon'],
['GARÇON', 'GARÇON'],
["ŒUVRE D'ART", "Œuvre d'Art"],
// German
['ÄUSSERST', 'äußerst'],
// Spanish
['EL NIÑO', 'El Niño'],
// Romanian
['ÎMPĂRAT', 'Împărat'],
// Random symbols
['DÉJÀ ΣΣΣ İIII', 'Déjà Σσς İIıi'],
]
);
}
public static function provideTitle(): array
{
return array_merge(
parent::provideTitle(),
[
['Deja', 'deja', false],
['Σσς', 'σσς', false],
['DEJa', 'dEJa', false],
['ΣσΣ', 'σσΣ', false],
['Deja Σσς DEJa ΣσΣ', 'deja σσς dEJa σσΣ', true],
// Spanish
['Última prueba', 'última prueba', false],
['ÚLTIMA pRUEBA', 'úLTIMA pRUEBA', false],
['¡Hola spain!', '¡hola spain!', false],
['¡HOLA sPAIN!', '¡hOLA sPAIN!', false],
['¡Hola Spain!', '¡hola spain!', true],
['¡HOLA SPAIN!', '¡hOLA sPAIN!', true],
['Última Prueba', 'última prueba', true],
['ÚLTIMA PRUEBA', 'úLTIMA pRUEBA', true],
]
);
}
public static function provideSlice(): array
{
return array_merge(
parent::provideSlice(),
[
['jà', 'déjà', 2, null],
['jà', 'déjà', 2, null],
['jà', 'déjà', -2, null],
['jà', 'déjà', -2, 3],
['', 'déjà', -1, 0],
['', 'déjà', 1, -4],
['j', 'déjà', -2, -1],
['', 'déjà', -2, -2],
['', 'déjà', 5, 0],
['', 'déjà', -5, 0],
]
);
}
public static function provideAppend(): array
{
return array_merge(
parent::provideAppend(),
[
[
'Déjà Σσς',
['Déjà', ' ', 'Σσς'],
],
[
'Déjà Σσς İIıi',
['Déjà', ' Σσς', ' İIıi'],
],
]
);
}
public function testAppendInvalidUtf8String()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString('Symfony')->append("\xE9");
}
public static function providePrepend(): array
{
return array_merge(
parent::providePrepend(),
[
[
'Σσς Déjà',
['Déjà', 'Σσς '],
],
[
'İIıi Σσς Déjà',
['Déjà', 'Σσς ', 'İIıi '],
],
]
);
}
public function testPrependInvalidUtf8String()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString('Symfony')->prepend("\xE9");
}
public static function provideBeforeAfter(): array
{
return array_merge(
parent::provideBeforeAfter(),
[
['jàdéjà', 'jà', 'déjàdéjà', 0, false],
['dé', 'jà', 'déjàdéjà', 0, true],
]
);
}
public static function provideBeforeAfterIgnoreCase(): array
{
return array_merge(
parent::provideBeforeAfterIgnoreCase(),
[
['jàdéjà', 'JÀ', 'déjàdéjà', 0, false],
['dé', 'jÀ', 'déjàdéjà', 0, true],
['éjàdéjà', 'é', 'déjàdéjà', 0, false],
['d', 'é', 'déjàdéjà', 0, true],
['déjàdéjà', 'Ç', 'déjàdéjà', 0, false],
['déjàdéjà', 'Ç', 'déjàdéjà', 0, true],
]
);
}
public static function provideBeforeAfterLast(): array
{
return array_merge(
parent::provideBeforeAfterLast(),
[
['déjàdéjà', 'Ç', 'déjàdéjà', 0, false],
['déjàdéjà', 'Ç', 'déjàdéjà', 0, true],
['éjà', 'é', 'déjàdéjà', 0, false],
['déjàd', 'é', 'déjàdéjà', 0, true],
]
);
}
public static function provideBeforeAfterLastIgnoreCase(): array
{
return array_merge(
parent::provideBeforeAfterLastIgnoreCase(),
[
['déjàdéjà', 'Ç', 'déjàdéjà', 0, false],
['éjà', 'é', 'déjàdéjà', 0, false],
['éjà', 'É', 'déjàdéjà', 0, false],
]
);
}
public static function provideFolded(): array
{
return array_merge(
parent::provideFolded(),
[
['déjà', 'DéjÀ'],
['σσσ', 'Σσς'],
['iıi̇i', 'Iıİi'],
]
);
}
public static function provideReplace(): array
{
return array_merge(
parent::provideReplace(),
[
['ΣσΣ', 1, 'Σσς', 'ς', 'Σ'],
['漢字はユニコード', 0, '漢字はユニコード', 'foo', 'bar'],
['漢字ーユニコード', 1, '漢字はユニコード', 'は', 'ー'],
['This is a jamais-vu situation!', 1, 'This is a déjà-vu situation!', 'déjà', 'jamais'],
]
);
}
public static function provideReplaceMatches(): array
{
return array_merge(
parent::provideReplaceMatches(),
[
['This is a dj-vu situation!', 'This is a déjà-vu situation!', '/([à-ú])/', ''],
]
);
}
public static function provideReplaceIgnoreCase(): array
{
return array_merge(
parent::provideReplaceIgnoreCase(),
[
// σ and ς are lowercase variants for Σ
['ΣΣΣ', 3, 'σσσ', 'σ', 'Σ'],
['ΣΣΣ', 3, 'σσσ', 'ς', 'Σ'],
['Σσ', 1, 'σσσ', 'σσ', 'Σ'],
['漢字はユニコード', 0, '漢字はユニコード', 'foo', 'bar'],
['漢字ーユニコード', 1, '漢字はユニコード', 'は', 'ー'],
['This is a jamais-vu situation!', 1, 'This is a déjà-vu situation!', 'DÉjÀ', 'jamais'],
]
);
}
public function testReplaceWithInvalidUtf8Pattern()
{
$this->assertEquals('Symfony', static::createFromString('Symfony')->replace("\xE9", 'p'));
}
public function testReplaceWithInvalidUtf8PatternReplacement()
{
$this->expectException(InvalidArgumentException::class);
static::createFromString('Symfony')->replace('f', "\xE9");
}
public static function provideCamel()
{
return array_merge(
parent::provideCamel(),
[
['symfonyIstÄußerstCool', 'symfony_ist_äußerst_cool'],
]
);
}
public static function provideSnake()
{
return array_merge(
parent::provideSnake(),
[
['symfony_ist_äußerst_cool', 'symfonyIstÄußerstCool'],
]
);
}
public static function provideKebab(): array
{
return [
...parent::provideKebab(),
['symfony-ist-äußerst-cool', 'symfonyIstÄußerstCool'],
['symfony-with-emojis', 'Symfony with 😃 emojis'],
];
}
public static function provideEqualsTo()
{
return array_merge(
parent::provideEqualsTo(),
[
[true, 'äußerst', 'äußerst'],
[false, 'BÄR', 'bär'],
[false, 'Bär', 'Bar'],
]
);
}
public static function provideEqualsToIgnoreCase()
{
return array_merge(
parent::provideEqualsToIgnoreCase(),
[
[true, 'Äußerst', 'äußerst'],
[false, 'Bär', 'Bar'],
]
);
}
public static function providePadBoth(): array
{
return array_merge(
parent::providePadBoth(),
[
['äußerst', 'äußerst', 7, '+'],
['+äußerst+', 'äußerst', 9, '+'],
['äö.äöä', '.', 6, 'äö'],
]
);
}
public static function providePadEnd(): array
{
return array_merge(
parent::providePadEnd(),
[
['äußerst', 'äußerst', 7, '+'],
['äußerst+', 'äußerst', 8, '+'],
['.äöä', '.', 4, 'äö'],
]
);
}
public static function providePadStart(): array
{
return array_merge(
parent::providePadStart(),
[
['äußerst', 'äußerst', 7, '+'],
['+äußerst', 'äußerst', 8, '+'],
['äöä.', '.', 4, 'äö'],
]
);
}
public static function provideReverse()
{
return array_merge(
parent::provideReverse(),
[
['äuß⭐erst', 'tsre⭐ßuä'],
['漢字ーユニコードéèΣσς', 'ςσΣèéドーコニユー字漢'],
['नमस्ते', 'तेस्मन'],
]
);
}
}

View File

@@ -0,0 +1,102 @@
<?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\Tests;
use Symfony\Component\String\AbstractString;
use Symfony\Component\String\ByteString;
use Symfony\Component\String\Exception\InvalidArgumentException;
class ByteStringTest extends AbstractAsciiTestCase
{
protected static function createFromString(string $string): AbstractString
{
return new ByteString($string);
}
public function testFromRandom()
{
$random = ByteString::fromRandom(32);
self::assertSame(32, $random->length());
foreach ($random->chunk() as $char) {
self::assertNotNull((new ByteString('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'))->indexOf($char));
}
}
public function testFromRandomWithSpecificChars()
{
$random = ByteString::fromRandom(32, 'abc');
self::assertSame(32, $random->length());
foreach ($random->chunk() as $char) {
self::assertNotNull((new ByteString('abc'))->indexOf($char));
}
}
public function testFromRandoWithZeroLength()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('A strictly positive length is expected, "0" given.');
self::assertSame('', ByteString::fromRandom(0));
}
public function testFromRandomThrowsForNegativeLength()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('A strictly positive length is expected, "-1" given.');
ByteString::fromRandom(-1);
}
public function testFromRandomAlphabetMin()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The length of the alphabet must in the [2^1, 2^56] range.');
ByteString::fromRandom(32, 'a');
}
public static function provideBytesAt(): array
{
return array_merge(
parent::provideBytesAt(),
[
[[0xC3], 'Späßchen', 2],
[[0x61], "Spa\u{0308}ßchen", 2],
[[0xCC], "Spa\u{0308}ßchen", 3],
[[0xE0], 'नमस्ते', 6],
]
);
}
public static function provideLength(): array
{
return array_merge(
parent::provideLength(),
[
[2, 'ä'],
]
);
}
public static function provideWidth(): array
{
return array_merge(
parent::provideWidth(),
[
[10, "f\u{001b}[0moo\x80bar\xfe\xfe1"], // foo?bar??1
[13, "f\u{001b}[0moo\x80bar\xfe\xfe1", false], // f[0moo?bar??1
]
);
}
}

View File

@@ -0,0 +1,58 @@
<?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\Tests;
use Symfony\Component\String\AbstractString;
use Symfony\Component\String\CodePointString;
class CodePointStringTest extends AbstractUnicodeTestCase
{
protected static function createFromString(string $string): AbstractString
{
return new CodePointString($string);
}
public static function provideLength(): array
{
return array_merge(
parent::provideLength(),
[
// 8 instead of 5 if it were processed as a grapheme cluster
[8, 'अनुच्छेद'],
]
);
}
public static function provideBytesAt(): array
{
return array_merge(
parent::provideBytesAt(),
[
[[0x61], "Spa\u{0308}ßchen", 2],
[[0xCC, 0x88], "Spa\u{0308}ßchen", 3],
[[0xE0, 0xA5, 0x8D], 'नमस्ते', 3],
]
);
}
public static function provideCodePointsAt(): array
{
return array_merge(
parent::provideCodePointsAt(),
[
[[0x61], "Spa\u{0308}ßchen", 2],
[[0x0308], "Spa\u{0308}ßchen", 3],
[[0x094D], 'नमस्ते', 3],
]
);
}
}

View File

@@ -0,0 +1,80 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\AbstractString;
use Symfony\Component\String\ByteString;
use Symfony\Component\String\UnicodeString;
use function Symfony\Component\String\b;
use function Symfony\Component\String\s;
use function Symfony\Component\String\u;
final class FunctionsTest extends TestCase
{
/**
* @dataProvider provideSStrings
*/
public function testS(AbstractString $expected, ?string $input)
{
$this->assertEquals($expected, s($input));
}
public static function provideSStrings(): array
{
return [
[new UnicodeString(''), ''],
[new UnicodeString(''), null],
[new UnicodeString('foo'), 'foo'],
[new UnicodeString('अनुच्छेद'), 'अनुच्छेद'],
[new ByteString("b\x80ar"), "b\x80ar"],
[new ByteString("\xfe\xff"), "\xfe\xff"],
];
}
/**
* @dataProvider provideUStrings
*/
public function testU(UnicodeString $expected, ?string $input)
{
$this->assertEquals($expected, u($input));
}
public static function provideUStrings(): array
{
return [
[new UnicodeString(''), ''],
[new UnicodeString(''), null],
[new UnicodeString('foo'), 'foo'],
[new UnicodeString('अनुच्छेद'), 'अनुच्छेद'],
];
}
/**
* @dataProvider provideBStrings
*/
public function testB(ByteString $expected, ?string $input)
{
$this->assertEquals($expected, b($input));
}
public static function provideBStrings(): array
{
return [
[new ByteString(''), ''],
[new ByteString(''), null],
[new ByteString("b\x80ar"), "b\x80ar"],
[new ByteString("\xfe\xff"), "\xfe\xff"],
];
}
}

View File

@@ -0,0 +1,363 @@
<?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\Tests\Inflector;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Inflector\EnglishInflector;
class EnglishInflectorTest extends TestCase
{
public static function singularizeProvider()
{
// see http://english-zone.com/spelling/plurals.html
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
return [
['accesses', 'access'],
['addresses', 'address'],
['agendas', 'agenda'],
['albums', 'album'],
['alumnae', 'alumna'],
['alumni', 'alumnus'],
['analyses', ['analys', 'analyse', 'analysis']],
['ankles', 'ankle'],
['antennae', 'antenna'],
['antennas', 'antenna'],
['appendices', ['appendex', 'appendix', 'appendice']],
['arches', ['arch', 'arche']],
['articles', 'article'],
['atlases', ['atlas', 'atlase', 'atlasis']],
['axes', ['ax', 'axe', 'axis']],
['babies', 'baby'],
['bacteria', 'bacterium'],
['bases', ['bas', 'base', 'basis']],
['batches', ['batch', 'batche']],
['beaux', 'beau'],
['bees', 'bee'],
['boxes', 'box'],
['boys', 'boy'],
['bureaus', 'bureau'],
['bureaux', 'bureau'],
['buses', ['bus', 'buse', 'busis']],
['bushes', ['bush', 'bushe']],
['buttons', 'button'],
['calves', ['calf', 'calve', 'calff']],
['cars', 'car'],
['cassettes', ['cassett', 'cassette']],
['caves', ['caf', 'cave', 'caff']],
['chateaux', 'chateau'],
['cheeses', ['chees', 'cheese', 'cheesis']],
['children', 'child'],
['circuses', ['circus', 'circuse', 'circusis']],
['cliffs', 'cliff'],
['codes', 'code'],
['committee', 'committee'],
['corpora', 'corpus'],
['coupons', 'coupon'],
['crises', ['cris', 'crise', 'crisis']],
['criteria', 'criterion'],
['cups', 'cup'],
['curricula', 'curriculum'],
['data', 'data'],
['days', 'day'],
['discos', 'disco'],
['devices', ['devex', 'devix', 'device']],
['drives', 'drive'],
['drivers', 'driver'],
['dwarves', ['dwarf', 'dwarve', 'dwarff']],
['echoes', ['echo', 'echoe']],
['edges', 'edge'],
['elves', ['elf', 'elve', 'elff']],
['emphases', ['emphas', 'emphase', 'emphasis']],
['employees', 'employee'],
['faxes', 'fax'],
['fees', 'fee'],
['feet', 'foot'],
['feedback', 'feedback'],
['foci', 'focus'],
['focuses', ['focus', 'focuse', 'focusis']],
['formulae', 'formula'],
['formulas', 'formula'],
['conspectuses', 'conspectus'],
['fungi', 'fungus'],
['funguses', ['fungus', 'funguse', 'fungusis']],
['garages', ['garag', 'garage']],
['geese', 'goose'],
['genera', 'genus'],
['halves', ['half', 'halve', 'halff']],
['hats', 'hat'],
['heroes', ['hero', 'heroe']],
['hippopotamuses', ['hippopotamus', 'hippopotamuse', 'hippopotamusis']], // hippopotami
['hoaxes', 'hoax'],
['hooves', ['hoof', 'hoove', 'hooff']],
['houses', ['hous', 'house', 'housis']],
['indexes', 'index'],
['indices', ['index', 'indix', 'indice']],
['ions', 'ion'],
['irises', ['iris', 'irise', 'irisis']],
['kisses', 'kiss'],
['knives', 'knife'],
['lamps', 'lamp'],
['lessons', 'lesson'],
['leaves', ['leaf', 'leave', 'leaff']],
['lice', 'louse'],
['lives', 'life'],
['matrices', ['matrex', 'matrix', 'matrice']],
['matrixes', 'matrix'],
['media', 'medium'],
['memoranda', 'memorandum'],
['men', 'man'],
['mice', 'mouse'],
['moves', 'move'],
['movies', 'movie'],
['names', 'name'],
['nebulae', 'nebula'],
['neuroses', ['neuros', 'neurose', 'neurosis']],
['news', 'news'],
['oases', ['oas', 'oase', 'oasis']],
['objectives', 'objective'],
['oxen', 'ox'],
['parties', 'party'],
['people', 'person'],
['persons', 'person'],
['phenomena', 'phenomenon'],
['photos', 'photo'],
['pianos', 'piano'],
['plateaux', 'plateau'],
['poisons', 'poison'],
['poppies', 'poppy'],
['prices', ['prex', 'prix', 'price']],
['quizzes', 'quiz'],
['quora', 'quorum'],
['quorums', 'quorum'],
['radii', 'radius'],
['roofs', 'roof'],
['roses', ['ros', 'rose', 'rosis']],
['sandwiches', ['sandwich', 'sandwiche']],
['scarves', ['scarf', 'scarve', 'scarff']],
['schemas', 'schema'], // schemata
['seasons', 'season'],
['selfies', 'selfie'],
['series', 'series'],
['services', 'service'],
['sheriffs', 'sheriff'],
['shoes', ['sho', 'shoe']],
['species', 'species'],
['spies', 'spy'],
['staves', ['staf', 'stave', 'staff']],
['status', 'status'],
['statuses', 'status'],
['stories', 'story'],
['strata', 'stratum'],
['suitcases', ['suitcas', 'suitcase', 'suitcasis']],
['syllabi', 'syllabus'],
['tags', 'tag'],
['teeth', 'tooth'],
['theses', ['thes', 'these', 'thesis']],
['thieves', ['thief', 'thieve', 'thieff']],
['treasons', 'treason'],
['trees', 'tree'],
['waltzes', ['waltz', 'waltze']],
['wives', 'wife'],
['zombies', 'zombie'],
// test casing: if the first letter was uppercase, it should remain so
['Men', 'Man'],
['GrandChildren', 'GrandChild'],
['SubTrees', 'SubTree'],
// Known issues
// ['insignia', 'insigne'],
// ['insignias', 'insigne'],
// ['rattles', 'rattle'],
];
}
public static function pluralizeProvider()
{
// see http://english-zone.com/spelling/plurals.html
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
return [
['access', 'accesses'],
['address', 'addresses'],
['agenda', 'agendas'],
['aircraft', 'aircraft'],
['album', 'albums'],
['alumnus', 'alumni'],
['analysis', 'analyses'],
['ankle', 'ankles'],
['antenna', 'antennas'], // antennae
['appendix', ['appendices', 'appendixes']],
['arch', 'arches'],
['article', 'articles'],
['atlas', 'atlases'],
['axe', 'axes'],
['axis', 'axes'],
['baby', 'babies'],
['bacterium', 'bacteria'],
['base', 'bases'],
['batch', 'batches'],
['beau', ['beaus', 'beaux']],
['bee', 'bees'],
['box', 'boxes'],
['boy', 'boys'],
['bureau', ['bureaus', 'bureaux']],
['bus', 'buses'],
['bush', 'bushes'],
['button', 'buttons'],
['calf', ['calfs', 'calves']],
['campus', 'campuses'],
['car', 'cars'],
['cassette', 'cassettes'],
['cave', 'caves'],
['chateau', ['chateaus', 'chateaux']],
['cheese', 'cheeses'],
['child', 'children'],
['circus', 'circuses'],
['cliff', 'cliffs'],
['committee', 'committees'],
['coupon', 'coupons'],
['crisis', 'crises'],
['criterion', 'criteria'],
['cup', 'cups'],
['curriculum', 'curricula'],
['data', 'data'],
['day', 'days'],
['disco', 'discos'],
['device', 'devices'],
['drive', 'drives'],
['driver', 'drivers'],
['dwarf', ['dwarfs', 'dwarves']],
['echo', 'echoes'],
['edge', 'edges'],
['elf', ['elfs', 'elves']],
['emphasis', 'emphases'],
['fax', ['faxes', 'faxxes']],
['feedback', 'feedback'],
['focus', 'focuses'],
['foot', 'feet'],
['formula', 'formulas'], // formulae
['conspectus', 'conspectuses'],
['fungus', 'fungi'],
['garage', 'garages'],
['goose', 'geese'],
['half', ['halfs', 'halves']],
['hat', 'hats'],
['hero', 'heroes'],
['hippocampus', 'hippocampi'],
['hippopotamus', 'hippopotami'], // hippopotamuses
['hoax', 'hoaxes'],
['hoof', ['hoofs', 'hooves']],
['house', 'houses'],
['icon', 'icons'],
['index', ['indicies', 'indexes']],
['ion', 'ions'],
['iris', 'irises'],
['kiss', 'kisses'],
['knife', 'knives'],
['lamp', 'lamps'],
['leaf', ['leafs', 'leaves']],
['lesson', 'lessons'],
['life', 'lives'],
['louse', 'lice'],
['man', 'men'],
['matrix', ['matrices', 'matrixes']],
['medium', 'media'],
['memorandum', 'memoranda'],
['mouse', 'mice'],
['move', 'moves'],
['movie', 'movies'],
['nebula', 'nebulae'],
['neurosis', 'neuroses'],
['news', 'news'],
['oasis', 'oases'],
['objective', 'objectives'],
['ox', 'oxen'],
['party', 'parties'],
['person', ['persons', 'people']],
['phenomenon', 'phenomena'],
['photo', 'photos'],
['piano', 'pianos'],
['plateau', ['plateaus', 'plateaux']],
['poison', 'poisons'],
['poppy', 'poppies'],
['price', 'prices'],
['quiz', 'quizzes'],
['quorum', ['quora', 'quorums']],
['radius', 'radii'],
['roof', ['roofs', 'rooves']],
['rose', 'roses'],
['sandwich', 'sandwiches'],
['scarf', ['scarfs', 'scarves']],
['schema', 'schemas'], // schemata
['season', 'seasons'],
['selfie', 'selfies'],
['series', 'series'],
['service', 'services'],
['sheriff', 'sheriffs'],
['shoe', 'shoes'],
['species', 'species'],
['status', ['status', 'statuses']],
['stratum', 'strata'],
['spy', 'spies'],
['staff', 'staves'],
['story', 'stories'],
['stratum', 'strata'],
['suitcase', 'suitcases'],
['syllabus', 'syllabi'],
['tag', 'tags'],
['thief', ['thiefs', 'thieves']],
['tooth', 'teeth'],
['treason', 'treasons'],
['tree', 'trees'],
['waltz', 'waltzes'],
['wife', 'wives'],
['icon', 'icons'],
['hippocampus', 'hippocampi'],
['campus', 'campuses'],
['hardware', 'hardware'],
['alias', 'aliases'],
// test casing: if the first letter was uppercase, it should remain so
['Man', 'Men'],
['GrandChild', 'GrandChildren'],
['SubTree', 'SubTrees'],
];
}
/**
* @dataProvider singularizeProvider
*/
public function testSingularize(string $plural, $singular)
{
$this->assertSame(\is_array($singular) ? $singular : [$singular], (new EnglishInflector())->singularize($plural));
}
/**
* @dataProvider pluralizeProvider
*/
public function testPluralize(string $singular, $plural)
{
$this->assertSame(\is_array($plural) ? $plural : [$plural], (new EnglishInflector())->pluralize($singular));
}
public function testPluralizeEmptyString()
{
$plural = (new EnglishInflector())->pluralize('');
$this->assertSame([''], $plural);
}
public function testSingularizeEmptyString()
{
$singular = (new EnglishInflector())->singularize('');
$this->assertSame([''], $singular);
}
}

View File

@@ -0,0 +1,149 @@
<?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\Tests\Inflector;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Inflector\FrenchInflector;
class FrenchInflectorTest extends TestCase
{
public static function pluralizeProvider()
{
return [
// Le pluriel par défaut
['voiture', 'voitures'],
// special characters
['œuf', 'œufs'],
['oeuf', 'oeufs'],
// Les mots finissant par s, x, z sont invariables en nombre
['bois', 'bois'],
['fils', 'fils'],
['héros', 'héros'],
['nez', 'nez'],
['rictus', 'rictus'],
['sans', 'sans'],
['souris', 'souris'],
['tas', 'tas'],
['toux', 'toux'],
// Les mots finissant en eau prennent tous un x au pluriel
['eau', 'eaux'],
['sceau', 'sceaux'],
// Les mots finissant en au prennent tous un x au pluriel sauf landau
['noyau', 'noyaux'],
['landau', 'landaus'],
// Les mots finissant en eu prennent un x au pluriel sauf pneu, bleu et émeu
['pneu', 'pneus'],
['bleu', 'bleus'],
['émeu', 'émeus'],
['cheveu', 'cheveux'],
// Les mots finissant en al se terminent en aux au pluriel
['amiral', 'amiraux'],
['animal', 'animaux'],
['arsenal', 'arsenaux'],
['bocal', 'bocaux'],
['canal', 'canaux'],
['capital', 'capitaux'],
['caporal', 'caporaux'],
['cheval', 'chevaux'],
['cristal', 'cristaux'],
['général', 'généraux'],
['hopital', 'hopitaux'],
['hôpital', 'hôpitaux'],
['idéal', 'idéaux'],
['journal', 'journaux'],
['littoral', 'littoraux'],
['local', 'locaux'],
['mal', 'maux'],
['métal', 'métaux'],
['minéral', 'minéraux'],
['principal', 'principaux'],
['radical', 'radicaux'],
['terminal', 'terminaux'],
// sauf bal, carnaval, caracal, chacal, choral, corral, étal, festival, récital et val
['bal', 'bals'],
['carnaval', 'carnavals'],
['caracal', 'caracals'],
['chacal', 'chacals'],
['choral', 'chorals'],
['corral', 'corrals'],
['étal', 'étals'],
['festival', 'festivals'],
['récital', 'récitals'],
['val', 'vals'],
// Les noms terminés en -ail prennent un s au pluriel.
['portail', 'portails'],
['rail', 'rails'],
// SAUF aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail qui font leur pluriel en -aux
['aspirail', 'aspiraux'],
['bail', 'baux'],
['corail', 'coraux'],
['émail', 'émaux'],
['fermail', 'fermaux'],
['soupirail', 'soupiraux'],
['travail', 'travaux'],
['vantail', 'vantaux'],
['vitrail', 'vitraux'],
// Les noms terminés en -ou prennent un s au pluriel.
['trou', 'trous'],
['fou', 'fous'],
// SAUF Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel
['bijou', 'bijoux'],
['caillou', 'cailloux'],
['chou', 'choux'],
['genou', 'genoux'],
['hibou', 'hiboux'],
['joujou', 'joujoux'],
['pou', 'poux'],
// Inflected word
['cinquante', 'cinquante'],
['soixante', 'soixante'],
['mille', 'mille'],
// Titles
['monsieur', 'messieurs'],
['madame', 'mesdames'],
['mademoiselle', 'mesdemoiselles'],
['monseigneur', 'messeigneurs'],
];
}
/**
* @dataProvider pluralizeProvider
*/
public function testSingularize(string $singular, string $plural)
{
$this->assertSame([$singular], (new FrenchInflector())->singularize($plural));
// test casing: if the first letter was uppercase, it should remain so
$this->assertSame([ucfirst($singular)], (new FrenchInflector())->singularize(ucfirst($plural)));
}
/**
* @dataProvider pluralizeProvider
*/
public function testPluralize(string $singular, string $plural)
{
$this->assertSame([$plural], (new FrenchInflector())->pluralize($singular));
// test casing: if the first letter was uppercase, it should remain so
$this->assertSame([ucfirst($plural)], (new FrenchInflector())->pluralize(ucfirst($singular)));
}
}

View File

@@ -0,0 +1,158 @@
<?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\Tests\Inflector;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Inflector\SpanishInflector;
class SpanishInflectorTest extends TestCase
{
public static function singularizeProvider(): array
{
return [
// vowels (RAE 3.2a, 3.2c)
['peras', 'pera'],
['especies', 'especie'],
['álcalis', 'álcali'],
['códigos', 'código'],
['espíritus', 'espíritu'],
// accented (RAE 3.2a, 3.2c)
['papás', 'papá'],
['cafés', 'café'],
['isrealís', 'isrealí'],
['burós', 'buró'],
['tisús', 'tisú'],
// ending in -ión
['aviones', 'avión'],
['camiones', 'camión'],
// ending in some letters (RAE 3.2k)
['amores', 'amor'],
['antifaces', 'antifaz'],
['atriles', 'atril'],
['fácsimiles', 'fácsimil'],
['vides', 'vid'],
['reyes', 'rey'],
['relojes', 'reloj'],
['faxes', 'fax'],
['sándwiches', 'sándwich'],
['cánones', 'cánon'],
// (RAE 3.2n)
['adioses', 'adiós'],
['aguarrases', 'aguarrás'],
['arneses', 'arnés'],
['autobuses', 'autobús'],
['kermeses', 'kermés'],
['palmareses', 'palmarés'],
['toses', 'tos'],
// Special
['síes', 'sí'],
['noes', 'no'],
];
}
public static function pluralizeProvider(): array
{
return [
// vowels (RAE 3.2a, 3.2c)
['pera', 'peras'],
['especie', 'especies'],
['álcali', 'álcalis'],
['código', 'códigos'],
['espíritu', 'espíritus'],
// accented (RAE 3.2a, 3.2c)
['papá', 'papás'],
['café', 'cafés'],
['isrealí', 'isrealís'],
['buró', 'burós'],
['tisú', 'tisús'],
// ending in -ión
['avión', 'aviones'],
['camión', 'camiones'],
// ending in some letters (RAE 3.2k)
['amor', 'amores'],
['antifaz', 'antifaces'],
['atril', 'atriles'],
['fácsimil', 'fácsimiles'],
['vid', 'vides'],
['rey', 'reyes'],
['reloj', 'relojes'],
['fax', 'faxes'],
['sándwich', 'sándwiches'],
['cánon', 'cánones'],
// (RAE 3.2n)
['adiós', 'adioses'],
['aguarrás', 'aguarrases'],
['arnés', 'arneses'],
['autobús', 'autobuses'],
['kermés', 'kermeses'],
['palmarés', 'palmareses'],
['tos', 'toses'],
// Specials
['sí', 'síes'],
['no', 'noes'],
];
}
public static function uninflectedProvider(): array
{
return [
['lunes'],
['rodapiés'],
['reposapiés'],
['miércoles'],
['pies'],
];
}
/**
* @dataProvider singularizeProvider
*/
public function testSingularize(string $plural, $singular)
{
$this->assertSame(
\is_array($singular) ? $singular : [$singular],
(new SpanishInflector())->singularize($plural)
);
}
/**
* @dataProvider pluralizeProvider
*/
public function testPluralize(string $singular, $plural)
{
$this->assertSame(
\is_array($plural) ? $plural : [$plural],
(new SpanishInflector())->pluralize($singular)
);
}
/**
* @dataProvider uninflectedProvider
*/
public function testUninflected(string $word)
{
$this->assertSame(
[$word],
(new SpanishInflector())->pluralize($word)
);
}
}

View File

@@ -0,0 +1,111 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\String\LazyString;
class LazyStringTest extends TestCase
{
public function testLazyString()
{
$count = 0;
$s = LazyString::fromCallable(function () use (&$count) {
return ++$count;
});
$this->assertSame(0, $count);
$this->assertSame('1', (string) $s);
$this->assertSame(1, $count);
}
/**
* @runInSeparateProcess
*/
public function testReturnTypeError()
{
ErrorHandler::register();
$s = LazyString::fromCallable(fn () => []);
$this->expectException(\TypeError::class);
$this->expectExceptionMessageMatches('{^Return value of .*\{closure.*\}\(\) passed to '.preg_quote(LazyString::class).'::fromCallable\(\) must be of the type string, array returned\.$}');
(string) $s;
}
public function testLazyCallable()
{
$count = 0;
$s = LazyString::fromCallable([function () use (&$count) {
return new class($count) {
private int $count;
public function __construct(int &$count)
{
$this->count = &$count;
}
public function __invoke()
{
return ++$this->count;
}
};
}]);
$this->assertSame(0, $count);
$this->assertSame('1', (string) $s);
$this->assertSame(1, $count);
$this->assertSame('1', (string) $s); // ensure the value is memoized
$this->assertSame(1, $count);
}
public function testFromStringable()
{
$this->assertInstanceOf(LazyString::class, LazyString::fromStringable('abc'));
$this->assertSame('abc', (string) LazyString::fromStringable('abc'));
$this->assertSame('1', (string) LazyString::fromStringable(true));
$this->assertSame('', (string) LazyString::fromStringable(false));
$this->assertSame('123', (string) LazyString::fromStringable(123));
$this->assertSame('123.456', (string) LazyString::fromStringable(123.456));
$this->assertStringContainsString('hello', (string) LazyString::fromStringable(new \Exception('hello')));
}
public function testResolve()
{
$this->assertSame('abc', LazyString::resolve('abc'));
$this->assertSame('1', LazyString::resolve(true));
$this->assertSame('', LazyString::resolve(false));
$this->assertSame('123', LazyString::resolve(123));
$this->assertSame('123.456', LazyString::resolve(123.456));
$this->assertStringContainsString('hello', LazyString::resolve(new \Exception('hello')));
}
public function testIsStringable()
{
$this->assertTrue(LazyString::isStringable('abc'));
$this->assertTrue(LazyString::isStringable(true));
$this->assertTrue(LazyString::isStringable(false));
$this->assertTrue(LazyString::isStringable(123));
$this->assertTrue(LazyString::isStringable(123.456));
$this->assertTrue(LazyString::isStringable(new \Exception('hello')));
}
public function testIsNotStringable()
{
$this->assertFalse(LazyString::isStringable(null));
$this->assertFalse(LazyString::isStringable([]));
$this->assertFalse(LazyString::isStringable(\STDIN));
$this->assertFalse(LazyString::isStringable(new \stdClass()));
}
}

View File

@@ -0,0 +1,109 @@
<?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\Tests\Slugger;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Slugger\AsciiSlugger;
class AsciiSluggerTest extends TestCase
{
/**
* @dataProvider provideSlugTests
*/
public function testSlug(string $expected, string $string, string $separator = '-', ?string $locale = null)
{
$slugger = new AsciiSlugger();
$this->assertSame($expected, (string) $slugger->slug($string, $separator, $locale));
}
public static function provideSlugTests(): iterable
{
yield ['', ''];
yield ['foo', ' foo '];
yield ['foo-bar', 'foo bar'];
yield ['foo-bar', 'foo@bar', '-'];
yield ['foo-at-bar', 'foo@bar', '-', 'en'];
yield ['e-a', 'é$!à'];
yield ['e_a', 'é$!à', '_'];
yield ['a', 'ä'];
yield ['a', 'ä', '-', 'fr'];
yield ['ae', 'ä', '-', 'de'];
yield ['ae', 'ä', '-', 'de_fr']; // Ensure we get the parent locale
yield [\function_exists('transliterator_transliterate') ? 'g' : '', 'ғ', '-'];
yield [\function_exists('transliterator_transliterate') ? 'gh' : '', 'ғ', '-', 'uz'];
yield [\function_exists('transliterator_transliterate') ? 'gh' : '', 'ғ', '-', 'uz_fr']; // Ensure we get the parent locale
}
/**
* @dataProvider provideSlugEmojiTests
*
* @requires extension intl
*/
public function testSlugEmoji(string $expected, string $string, ?string $locale, string|bool $emoji = true)
{
$slugger = new AsciiSlugger();
$slugger = $slugger->withEmoji($emoji);
$this->assertSame($expected, (string) $slugger->slug($string, '-', $locale));
}
public static function provideSlugEmojiTests(): iterable
{
yield [
'un-chat-qui-sourit-chat-noir-et-un-tete-de-lion-vont-au-parc-national',
'un 😺, 🐈‍⬛, et un 🦁 vont au 🏞️',
'fr',
];
yield [
'a-grinning-cat-black-cat-and-a-lion-go-to-national-park-smiling-face-with-heart-eyes-party-popper-yellow-heart',
'a 😺, 🐈‍⬛, and a 🦁 go to 🏞️... 😍 🎉 💛',
'en',
];
yield [
'a-and-a-go-to',
'a 😺, 🐈‍⬛, and a 🦁 go to 🏞️... 😍 🎉 💛',
null,
];
yield [
'a-smiley-cat-black-cat-and-a-lion-face-go-to-national-park-heart-eyes-tada-yellow-heart',
'a 😺, 🐈‍⬛, and a 🦁 go to 🏞️... 😍 🎉 💛',
null,
'slack',
];
yield [
'a-smiley-cat-black-cat-and-a-lion-go-to-national-park-heart-eyes-tada-yellow-heart',
'a 😺, 🐈‍⬛, and a 🦁 go to 🏞️... 😍 🎉 💛',
null,
'github',
];
yield [
'a-smiley-cat-black-cat-and-a-lion-go-to-national-park-heart-eyes-tada-yellow-heart',
'a 😺, 🐈‍⬛, and a 🦁 go to 🏞️... 😍 🎉 💛',
'en',
'github',
];
yield [
'un-chat-qui-sourit-chat-noir-et-un-tete-de-lion-vont-au-parc-national',
'un 😺, 🐈‍⬛, et un 🦁 vont au 🏞️',
'fr_XX', // Fallback on parent locale
];
yield [
'un-et-un-vont-au',
'un 😺, 🐈‍⬛, et un 🦁 vont au 🏞️',
'undefined_locale', // Behaves the same as if emoji support is disabled
];
}
}

View File

@@ -0,0 +1,110 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Slugger\AsciiSlugger;
class SluggerTest extends TestCase
{
/**
* @requires extension intl
*
* @dataProvider provideSlug
*/
public function testSlug(string $string, string $locale, string $expectedSlug)
{
$slugger = new AsciiSlugger($locale);
$this->assertSame($expectedSlug, (string) $slugger->slug($string));
}
public static function provideSlug(): array
{
return [
['Стойността трябва да бъде лъжа', 'bg', 'Stoinostta-tryabva-da-bude-luzha'],
['You & I', 'en', 'You-and-I'],
['symfony@symfony.com', 'en', 'symfony-at-symfony-com'],
['Dieser Wert sollte größer oder gleich', 'de', 'Dieser-Wert-sollte-groesser-oder-gleich'],
['Dieser Wert sollte größer oder gleich', 'de_AT', 'Dieser-Wert-sollte-groesser-oder-gleich'],
['Αυτή η τιμή πρέπει να είναι ψευδής', 'el', 'Avti-i-timi-prepi-na-inai-psevdhis'],
['该变量的值应为', 'zh', 'gai-bian-liang-de-zhi-ying-wei'],
['該變數的值應為', 'zh_TW', 'gai-bian-shu-de-zhi-ying-wei'],
['Wôrķšƥáçè ~~sèťtïñğš~~', 'C', 'Workspace-settings'],
];
}
public function testSeparatorWithoutLocale()
{
$slugger = new AsciiSlugger();
$this->assertSame('hello-world', (string) $slugger->slug('hello world'));
$this->assertSame('hello_world', (string) $slugger->slug('hello world', '_'));
}
public function testSlugCharReplacementLocaleConstruct()
{
$slugger = new AsciiSlugger('fr', ['fr' => ['&' => 'et', '@' => 'chez']]);
$slug = (string) $slugger->slug('toi & moi avec cette adresse slug@test.fr', '_');
$this->assertSame('toi_et_moi_avec_cette_adresse_slug_chez_test_fr', $slug);
}
public function testSlugCharReplacementLocaleMethod()
{
$slugger = new AsciiSlugger(null, ['es' => ['&' => 'y', '@' => 'en senal']]);
$slug = (string) $slugger->slug('yo & tu a esta dirección slug@test.es', '_', 'es');
$this->assertSame('yo_y_tu_a_esta_direccion_slug_en_senal_test_es', $slug);
}
public function testSlugCharReplacementLocaleConstructWithoutSymbolsMap()
{
$slugger = new AsciiSlugger('en');
$slug = (string) $slugger->slug('you & me with this address slug@test.uk', '_');
$this->assertSame('you_and_me_with_this_address_slug_at_test_uk', $slug);
}
public function testSlugCharReplacementParentLocaleConstructWithoutSymbolsMap()
{
$slugger = new AsciiSlugger('en_GB');
$slug = (string) $slugger->slug('you & me with this address slug@test.uk', '_');
$this->assertSame('you_and_me_with_this_address_slug_at_test_uk', $slug);
}
public function testSlugCharReplacementParentLocaleConstruct()
{
$slugger = new AsciiSlugger('fr_FR', ['fr' => ['&' => 'et', '@' => 'chez']]);
$slug = (string) $slugger->slug('toi & moi avec cette adresse slug@test.fr', '_');
$this->assertSame('toi_et_moi_avec_cette_adresse_slug_chez_test_fr', $slug);
}
public function testSlugCharReplacementParentLocaleMethod()
{
$slugger = new AsciiSlugger(null, ['es' => ['&' => 'y', '@' => 'en senal']]);
$slug = (string) $slugger->slug('yo & tu a esta dirección slug@test.es', '_', 'es_ES');
$this->assertSame('yo_y_tu_a_esta_direccion_slug_en_senal_test_es', $slug);
}
public function testSlugClosure()
{
$slugger = new AsciiSlugger(null, function ($s, $locale) {
$this->assertSame('foo', $locale);
return str_replace('❤️', 'love', $s);
});
$this->assertSame('love', (string) $slugger->slug('❤️', '-', 'foo'));
}
}

View File

@@ -0,0 +1,266 @@
<?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\Tests;
use Symfony\Component\String\AbstractString;
use Symfony\Component\String\UnicodeString;
class UnicodeStringTest extends AbstractUnicodeTestCase
{
protected static function createFromString(string $string): AbstractString
{
return new UnicodeString($string);
}
public static function provideWrap(): array
{
return array_merge(
parent::provideWrap(),
[
[
['Käse' => static::createFromString('köstlich'), 'fromage' => static::createFromString('délicieux')],
["Ka\u{0308}se" => "ko\u{0308}stlich", 'fromage' => 'délicieux'],
],
[
['a' => 1, 'ä' => ['ö' => 2, 'ü' => 3]],
['a' => 1, "a\u{0308}" => ["o\u{0308}" => 2, 'ü' => 3]],
],
]
);
}
public static function provideLength(): array
{
return array_merge(
parent::provideLength(),
[
// 5 letters + 3 combining marks
[5, 'अनुच्छेद'],
]
);
}
public static function provideSplit(): array
{
return array_merge(
parent::provideSplit(),
[
[
'अ.नु.च्.छे.द',
'.',
[
static::createFromString('अ'),
static::createFromString('नु'),
static::createFromString('च्'),
static::createFromString('छे'),
static::createFromString('द'),
],
null,
],
]
);
}
public static function provideChunk(): array
{
return array_merge(
parent::provideChunk(),
[
[
'अनुच्छेद',
[
static::createFromString('अ'),
static::createFromString('नु'),
static::createFromString('च्'),
static::createFromString('छे'),
static::createFromString('द'),
],
1,
],
]
);
}
public static function provideBytesAt(): array
{
return array_merge(
parent::provideBytesAt(),
[
[[0xC3, 0xA4], "Spa\u{0308}ßchen", 2],
[[0x61, 0xCC, 0x88], "Spa\u{0308}ßchen", 2, UnicodeString::NFD],
[[0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D], 'नमस्ते', 2],
]
);
}
public static function provideCodePointsAt(): array
{
return array_merge(
parent::provideCodePointsAt(),
[
[[0xE4], "Spa\u{0308}ßchen", 2],
[[0x61, 0x0308], "Spa\u{0308}ßchen", 2, UnicodeString::NFD],
[[0x0938, 0x094D], 'नमस्ते', 2],
]
);
}
public static function provideLower(): array
{
return array_merge(
parent::provideLower(),
[
// Hindi
['अनुच्छेद', 'अनुच्छेद'],
]
);
}
public static function provideUpper(): array
{
return array_merge(
parent::provideUpper(),
[
// Hindi
['अनुच्छेद', 'अनुच्छेद'],
]
);
}
public static function provideAppend(): array
{
return array_merge(
parent::provideAppend(),
[
[
'तद्भव देशज',
['तद्भव', ' ', 'देशज'],
],
[
'तद्भव देशज विदेशी',
['तद्भव', ' देशज', ' विदेशी'],
],
]
);
}
public static function providePrepend(): array
{
return array_merge(
parent::providePrepend(),
[
[
'देशज तद्भव',
['तद्भव', 'देशज '],
],
[
'विदेशी देशज तद्भव',
['तद्भव', 'देशज ', 'विदेशी '],
],
]
);
}
public static function provideBeforeAfter(): array
{
return array_merge(
parent::provideBeforeAfter(),
[
['द foo अनुच्छेद', 'द', 'अनुच्छेद foo अनुच्छेद', 0, false],
['अनुच्छे', 'द', 'अनुच्छेद foo अनुच्छेद', 0, true],
]
);
}
public static function provideBeforeAfterIgnoreCase(): array
{
return array_merge(
parent::provideBeforeAfterIgnoreCase(),
[
['दछेच्नुअ', 'छेछे', 'दछेच्नुअ', 0, false],
['दछेच्नुअ', 'छेछे', 'दछेच्नुअ', 0, true],
['छेच्नुअ', 'छे', 'दछेच्नुअ', 0, false],
['द', 'छे', 'दछेच्नुअ', 0, true],
]
);
}
public static function provideBeforeAfterLast(): array
{
return array_merge(
parent::provideBeforeAfterLast(),
[
['दछेच्नुअ-दछेच्नु-अदछेच्नु', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 0, false],
['दछेच्नुअ-दछेच्नु-अदछेच्नु', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 0, true],
['-दछेच्नु', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', 0, false],
['दछेच्नुअ-दछेच्नु-अद', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', 0, true],
]
);
}
public static function provideBeforeAfterLastIgnoreCase(): array
{
return array_merge(
parent::provideBeforeAfterLastIgnoreCase(),
[
['दछेच्नुअ-दछेच्नु-अदछेच्नु', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 0, false],
['दछेच्नुअ-दछेच्नु-अदछेच्नु', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 0, true],
['-दछेच्नु', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', 0, false],
['दछेच्नुअ-दछेच्नु-अद', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', 0, true],
]
);
}
public static function provideReplace(): array
{
return array_merge(
parent::provideReplace(),
[
['Das Innenministerium', 1, 'Das Außenministerium', 'Auß', 'Inn'],
['दछेच्नुद-दछेच्नु-ददछेच्नु', 2, 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 'अ', 'द'],
]
);
}
public static function provideReplaceIgnoreCase(): array
{
return array_merge(
parent::provideReplaceIgnoreCase(),
[
['Das Aussenministerium', 1, 'Das Außenministerium', 'auß', 'Auss'],
['दछेच्नुद-दछेच्नु-ददछेच्नु', 2, 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 'अ', 'द'],
]
);
}
public static function provideStartsWith()
{
return array_merge(
parent::provideStartsWith(),
[
[false, "cle\u{0301} prive\u{0301}e", 'cle', UnicodeString::NFD],
[true, "cle\u{0301} prive\u{0301}e", 'clé', UnicodeString::NFD],
]
);
}
public static function provideEndsWith()
{
return array_merge(
parent::provideEndsWith(),
[
[false, "cle\u{0301} prive\u{0301}e", 'ee', UnicodeString::NFD],
[true, "cle\u{0301} prive\u{0301}e", 'ée', UnicodeString::NFD],
]
);
}
}

42
vendor/symfony/string/TruncateMode.php vendored Normal file
View File

@@ -0,0 +1,42 @@
<?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;
enum TruncateMode
{
/**
* Will cut exactly at given length.
*
* Length: 14
* Source: Lorem ipsum dolor sit amet
* Output: Lorem ipsum do
*/
case Char;
/**
* Returns the string up to the last complete word containing the specified length.
*
* Length: 14
* Source: Lorem ipsum dolor sit amet
* Output: Lorem ipsum
*/
case WordBefore;
/**
* Returns the string up to the complete word after or at the given length.
*
* Length: 14
* Source: Lorem ipsum dolor sit amet
* Output: Lorem ipsum dolor
*/
case WordAfter;
}

382
vendor/symfony/string/UnicodeString.php vendored Normal file
View File

@@ -0,0 +1,382 @@
<?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)) : $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);
}
public function __wakeup(): void
{
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;
}
}

30
vendor/symfony/string/phpunit.xml.dist vendored Normal file
View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony String Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>./</directory>
</include>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</coverage>
</phpunit>