diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index c16f692cfb37..8dda8ce0732e 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -120,10 +120,10 @@ private function outputAllNamespaces(array $params): array private function truncate(string $string, int $max): string { - $length = strlen($string); + $length = mb_strlen($string); if ($length > $max) { - return substr($string, 0, $max - 3) . '...'; + return mb_substr($string, 0, $max - 3) . '...'; } return $string; diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 170964f532cc..9eccc6e285f0 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -44,35 +44,40 @@ function word_limiter(string $str, int $limit = 100, string $endChar = '…' /** * Character Limiter * - * Limits the string based on the character count. Preserves complete words + * Limits the string based on the character count. Preserves complete words * so the character count may not be exactly as specified. * * @param string $endChar the end character. Usually an ellipsis */ - function character_limiter(string $str, int $n = 500, string $endChar = '…'): string + function character_limiter(string $string, int $limit = 500, string $endChar = '…'): string { - if (mb_strlen($str) < $n) { - return $str; + if (mb_strlen($string) < $limit) { + return $string; } // a bit complicated, but faster than preg_replace with \s+ - $str = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $str)); + $string = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $string)); + $stringLength = mb_strlen($string); - if (mb_strlen($str) <= $n) { - return $str; + if ($stringLength <= $limit) { + return $string; } - $out = ''; + $output = ''; + $outputLength = 0; + $words = explode(' ', trim($string)); - foreach (explode(' ', trim($str)) as $val) { - $out .= $val . ' '; - if (mb_strlen($out) >= $n) { - $out = trim($out); + foreach ($words as $word) { + $output .= $word . ' '; + $outputLength = mb_strlen($output); + + if ($outputLength >= $limit) { + $output = trim($output); break; } } - return (mb_strlen($out) === mb_strlen($str)) ? $out : $out . $endChar; + return ($outputLength === $stringLength) ? $output : $output . $endChar; } } @@ -712,38 +717,44 @@ function alternator(...$args): string function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string { if (isset($phrase)) { - $phrasePos = stripos($text, $phrase); - $phraseLen = strlen($phrase); + $phrasePosition = mb_stripos($text, $phrase); + $phraseLength = mb_strlen($phrase); } else { - $phrasePos = $radius / 2; - $phraseLen = 1; + $phrasePosition = $radius / 2; + $phraseLength = 1; } - $pre = explode(' ', substr($text, 0, $phrasePos)); - $pos = explode(' ', substr($text, $phrasePos + $phraseLen)); + $beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition)); + $afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength)); - $prev = ' '; - $post = ' '; - $count = 0; + $firstPartOutput = ' '; + $endPartOutput = ' '; + $count = 0; - foreach (array_reverse($pre) as $e) { - if ((strlen($e) + $count + 1) < $radius) { - $prev = ' ' . $e . $prev; + foreach (array_reverse($beforeWords) as $beforeWord) { + $beforeWordLength = mb_strlen($beforeWord); + + if (($beforeWordLength + $count + 1) < $radius) { + $firstPartOutput = ' ' . $beforeWord . $firstPartOutput; } - $count = ++$count + strlen($e); + + $count = ++$count + $beforeWordLength; } $count = 0; - foreach ($pos as $s) { - if ((strlen($s) + $count + 1) < $radius) { - $post .= $s . ' '; + foreach ($afterWords as $afterWord) { + $afterWordLength = mb_strlen($afterWord); + + if (($afterWordLength + $count + 1) < $radius) { + $endPartOutput .= $afterWord . ' '; } - $count = ++$count + strlen($s); + + $count = ++$count + $afterWordLength; } $ellPre = $phrase !== null ? $ellipsis : ''; - return str_replace(' ', ' ', $ellPre . $prev . $phrase . $post . $ellipsis); + return str_replace(' ', ' ', $ellPre . $firstPartOutput . $phrase . $endPartOutput . $ellipsis); } } diff --git a/system/View/View.php b/system/View/View.php index b54472ed0921..aaf807a9dc50 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -339,7 +339,7 @@ public function renderString(string $view, ?array $options = null, ?bool $saveDa */ public function excerpt(string $string, int $length = 20): string { - return (strlen($string) > $length) ? substr($string, 0, $length - 3) . '...' : $string; + return (mb_strlen($string) > $length) ? mb_substr($string, 0, $length - 3) . '...' : $string; } /** diff --git a/tests/system/Commands/Utilities/NamespacesTest.php b/tests/system/Commands/Utilities/NamespacesTest.php index ffdfa0ddcf8d..81bf5fb3f959 100644 --- a/tests/system/Commands/Utilities/NamespacesTest.php +++ b/tests/system/Commands/Utilities/NamespacesTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Commands\Utilities; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\ReflectionHelper; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -24,6 +25,7 @@ final class NamespacesTest extends CIUnitTestCase { use StreamFilterTrait; + use ReflectionHelper; protected function setUp(): void { @@ -84,4 +86,14 @@ public function testNamespacesCommandAllNamespaces(): void str_replace(' ', '', $this->getBuffer()) ); } + + public function testTruncateNamespaces(): void + { + $commandObject = new Namespaces(service('logger'), service('commands')); + $truncateRunner = $this->getPrivateMethodInvoker($commandObject, 'truncate'); + + $this->assertSame('App\Controllers\...', $truncateRunner('App\Controllers\Admin', 19)); + // multibyte namespace + $this->assertSame('App\Контроллеры\...', $truncateRunner('App\Контроллеры\Админ', 19)); + } } diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index ff0ecacf8746..2d6e73b9ef9b 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -24,7 +24,8 @@ #[Group('Others')] final class TextHelperTest extends CIUnitTestCase { - private string $_long_string = 'Once upon a time, a framework had no tests. It sad. So some nice people began to write tests. The more time that went on, the happier it became. Everyone was happy.'; + private string $longString = 'Once upon a time, a framework had no tests. It sad. So some nice people began to write tests. The more time that went on, the happier it became. Everyone was happy.'; + private string $mbLongString = 'Давным-давно во фреймворке не было тестов. Это печально. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем счастливее становилось. Все были счастливы.'; protected function setUp(): void { @@ -165,19 +166,29 @@ public function testIncrementString(): void public function testWordLimiter(): void { - $this->assertSame('Once upon a time,…', word_limiter($this->_long_string, 4)); - $this->assertSame('Once upon a time,…', word_limiter($this->_long_string, 4, '…')); + $this->assertSame('Once upon a time,…', word_limiter($this->longString, 4)); + $this->assertSame('Once upon a time,…', word_limiter($this->longString, 4, '…')); $this->assertSame('', word_limiter('', 4)); - $this->assertSame('Once upon a…', word_limiter($this->_long_string, 3, '…')); + $this->assertSame('Once upon a…', word_limiter($this->longString, 3, '…')); $this->assertSame('Once upon a time', word_limiter('Once upon a time', 4, '…')); + + $this->assertSame('Давным-давно во фреймворке не было тестов.…', word_limiter($this->mbLongString, 6)); + $this->assertSame('Давным-давно во фреймворке не было тестов.…', word_limiter($this->mbLongString, 6, '…')); + $this->assertSame('Давным-давно во фреймворке…', word_limiter($this->mbLongString, 3, '…')); + $this->assertSame('Давным-давно во фреймворке не было тестов.', word_limiter('Давным-давно во фреймворке не было тестов.', 6, '…')); } public function testCharacterLimiter(): void { - $this->assertSame('Once upon a time, a…', character_limiter($this->_long_string, 20)); - $this->assertSame('Once upon a time, a…', character_limiter($this->_long_string, 20, '…')); + $this->assertSame('Once upon a time, a…', character_limiter($this->longString, 20)); + $this->assertSame('Once upon a time, a…', character_limiter($this->longString, 20, '…')); $this->assertSame('Short', character_limiter('Short', 20)); $this->assertSame('Short', character_limiter('Short', 5)); + + $this->assertSame('Давным-давно во фреймворке не было тестов.…', character_limiter($this->mbLongString, 41)); + $this->assertSame('Давным-давно во фреймворке не было тестов.…', character_limiter($this->mbLongString, 41, '…')); + $this->assertSame('Короткий', character_limiter('Короткий', 20)); + $this->assertSame('Короткий', character_limiter('Короткий', 8)); } public function testAsciiToEntities(): void @@ -391,17 +402,25 @@ public function testDefaultWordWrapCharlim(): void public function testExcerpt(): void { - $string = $this->_long_string; + $string = $this->longString; $result = ' Once upon a time, a framework had no tests. It sad So some nice people began to write tests. The more time that went on, the happier it became. ...'; - $this->assertSame(excerpt($string), $result); + $this->assertSame($result, excerpt($string)); + + $multibyteResult = ' Давным-давно во фреймворке не было тестов. Это печ льно. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем ...'; + + $this->assertSame($multibyteResult, excerpt($this->mbLongString)); } public function testExcerptRadius(): void { - $string = $this->_long_string; + $string = $this->longString; $phrase = 'began'; $result = '... people began to ...'; $this->assertSame(excerpt($string, $phrase, 10), $result); + + $multibyteResult = '... Это печально . И вот ...'; + + $this->assertSame($multibyteResult, excerpt($this->mbLongString, 'печально', 10)); } public function testAlternator(): void diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 206cdc222d27..b15e603408d8 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -405,4 +405,12 @@ public function testRenderSectionSavingData(): void $view->setVar('testString', 'Hello World'); $this->assertStringContainsString($expected, $view->render('extend_reuse_section')); } + + public function testViewExcerpt(): void + { + $view = new View($this->config, $this->viewsDir, $this->loader); + + $this->assertSame('CodeIgniter is a PHP full-stack web framework...', $view->excerpt('CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure.', 48)); + $this->assertSame('CodeIgniter - это полнофункциональный веб-фреймворк...', $view->excerpt('CodeIgniter - это полнофункциональный веб-фреймворк на PHP, который является легким, быстрым, гибким и безопасным.', 54)); + } } diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index db4e8abaa07b..2d1ca114f56d 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -168,6 +168,7 @@ Method Signature Changes - **Time:** The first parameter type of the ``createFromTimestamp()`` has been changed from ``int`` to ``int|float``, and the return type ``static`` has been added. +- **Helpers:** ``character_limiter()`` parameter names have been updated. If you use named arguments, you need to update the function calls. Removed Type Definitions ------------------------ @@ -350,6 +351,8 @@ Bugs Fixed - **Response:** - Headers set using the ``Response`` class are now prioritized and replace headers that can be set manually using the PHP ``header()`` function. +- **View:** Added support for multibyte strings for ``View::excerpt()``. +- **Helpers:** Added support for multibyte strings for ``excerpt()``. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/helpers/text_helper.rst b/user_guide_src/source/helpers/text_helper.rst index de836253b190..d5966347f2ef 100644 --- a/user_guide_src/source/helpers/text_helper.rst +++ b/user_guide_src/source/helpers/text_helper.rst @@ -166,11 +166,11 @@ The following functions are available: .. literalinclude:: text_helper/012.php -.. php:function:: word_limiter($str[, $limit = 100[, $end_char = '…']]) +.. php:function:: word_limiter($str[, $limit = 100[, $endChar = '…']]) :param string $str: Input string :param int $limit: Limit - :param string $end_char: End character (usually an ellipsis) + :param string $endChar: End character (usually an ellipsis) :returns: Word-limited string :rtype: string @@ -181,11 +181,11 @@ The following functions are available: The third parameter is an optional suffix added to the string. By default it adds an ellipsis. -.. php:function:: character_limiter($str[, $n = 500[, $end_char = '…']]) +.. php:function:: character_limiter($string[, $limit = 500[, $endChar = '…']]) - :param string $str: Input string - :param int $n: Number of characters - :param string $end_char: End character (usually an ellipsis) + :param string $string: Input string + :param int $limit: Number of characters + :param string $endChar: End character (usually an ellipsis) :returns: Character-limited string :rtype: string