From 853fda336524184df8e6975d0673af665bd10a90 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 13:14:31 +0300 Subject: [PATCH 1/8] fix: Truncate multibyte namespaces in command --- system/Commands/Utilities/Namespaces.php | 4 ++-- tests/system/Commands/Utilities/NamespacesTest.php | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) 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/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)); + } } From 24542dad0a1df3140728a73f5bbdc319bcf49a2e Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 15:54:09 +0300 Subject: [PATCH 2/8] fix: Add support multibyte to `View->excerpt()` --- system/View/View.php | 2 +- tests/system/View/ViewTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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/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)); + } } From 23586b7c3d325e0cfbee3c7d2443731f7cd9edbb Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 15:54:19 +0300 Subject: [PATCH 3/8] fix: Add support multibyte to `excerpt()` helper --- system/Helpers/text_helper.php | 16 ++++++++-------- tests/system/Helpers/TextHelperTest.php | 10 ++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 170964f532cc..2f70a852edcb 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -712,34 +712,34 @@ 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); + $phrasePos = mb_stripos($text, $phrase); + $phraseLen = mb_strlen($phrase); } else { $phrasePos = $radius / 2; $phraseLen = 1; } - $pre = explode(' ', substr($text, 0, $phrasePos)); - $pos = explode(' ', substr($text, $phrasePos + $phraseLen)); + $pre = explode(' ', mb_substr($text, 0, $phrasePos)); + $pos = explode(' ', mb_substr($text, $phrasePos + $phraseLen)); $prev = ' '; $post = ' '; $count = 0; foreach (array_reverse($pre) as $e) { - if ((strlen($e) + $count + 1) < $radius) { + if ((mb_strlen($e) + $count + 1) < $radius) { $prev = ' ' . $e . $prev; } - $count = ++$count + strlen($e); + $count = ++$count + mb_strlen($e); } $count = 0; foreach ($pos as $s) { - if ((strlen($s) + $count + 1) < $radius) { + if ((mb_strlen($s) + $count + 1) < $radius) { $post .= $s . ' '; } - $count = ++$count + strlen($s); + $count = ++$count + mb_strlen($s); } $ellPre = $phrase !== null ? $ellipsis : ''; diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index ff0ecacf8746..8f807f0431c9 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -394,6 +394,11 @@ public function testExcerpt(): void $string = $this->_long_string; $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); + + $multibyteString = 'Давным-давно во фреймворке не было тестов. Это печально. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем счастливее становилось. Все были счастливы.'; + $multibyteResult = ' Давным-давно во фреймворке не было тестов. Это печ льно. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем ...'; + + $this->assertSame(excerpt($multibyteString), $multibyteResult); } public function testExcerptRadius(): void @@ -402,6 +407,11 @@ public function testExcerptRadius(): void $phrase = 'began'; $result = '... people began to ...'; $this->assertSame(excerpt($string, $phrase, 10), $result); + + $multibyteString = 'Давным-давно во фреймворке не было тестов. Это печально. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем счастливее становилось. Все были счастливы.'; + $multibyteResult = '... Это печально . И вот ...'; + + $this->assertSame(excerpt($multibyteString, 'печально', 10), $multibyteResult); } public function testAlternator(): void From 08a1d85c221dca0ca9214750f7287c7e0fa0e07a Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 15:59:58 +0300 Subject: [PATCH 4/8] docs: Update changelog --- user_guide_src/source/changelogs/v4.5.8.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.5.8.rst b/user_guide_src/source/changelogs/v4.5.8.rst index ae29b59fb165..2ea9768394b7 100644 --- a/user_guide_src/source/changelogs/v4.5.8.rst +++ b/user_guide_src/source/changelogs/v4.5.8.rst @@ -31,6 +31,8 @@ Bugs Fixed ********** - **Database:** Fixed a bug where ``Builder::affectedRows()`` threw an error when the previous query call failed in ``Postgre`` and ``SQLSRV`` drivers. +- **View:** Added support for multibyte strings for ``View::excerpt()``. +- **Helpers:** Added support for multibyte strings for ``excerpt()``. See the repo's `CHANGELOG.md `_ From c4d26091ec63b7a8be7f746e35d2b3e57471ab95 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 17:26:51 +0300 Subject: [PATCH 5/8] fix: Improve readability `excerpt()` --- system/Helpers/text_helper.php | 40 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 2f70a852edcb..c44edeacd0e3 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -712,38 +712,44 @@ function alternator(...$args): string function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string { if (isset($phrase)) { - $phrasePos = mb_stripos($text, $phrase); - $phraseLen = mb_strlen($phrase); + $phrasePosition = mb_stripos($text, $phrase); + $phraseLength = mb_strlen($phrase); } else { - $phrasePos = $radius / 2; - $phraseLen = 1; + $phrasePosition = $radius / 2; + $phraseLength = 1; } - $pre = explode(' ', mb_substr($text, 0, $phrasePos)); - $pos = explode(' ', mb_substr($text, $phrasePos + $phraseLen)); + $beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition)); + $afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength)); - $prev = ' '; - $post = ' '; + $firstPartOutput = ' '; + $endPartOutput = ' '; $count = 0; - foreach (array_reverse($pre) as $e) { - if ((mb_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 + mb_strlen($e); + + $count = ++$count + $beforeWordLength; } $count = 0; - foreach ($pos as $s) { - if ((mb_strlen($s) + $count + 1) < $radius) { - $post .= $s . ' '; + foreach ($afterWords as $afterWord) { + $afterWordLength = mb_strlen($afterWord); + + if (($afterWordLength + $count + 1) < $radius) { + $endPartOutput .= $afterWord . ' '; } - $count = ++$count + mb_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); } } From d18026d60f810c0d639de945513eac29a2034f75 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 18:13:23 +0300 Subject: [PATCH 6/8] refactor: Rework `character_limiter()` --- system/Helpers/text_helper.php | 37 +++++++++++-------- user_guide_src/source/helpers/text_helper.rst | 12 +++--- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index c44edeacd0e3..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; } } @@ -722,9 +727,9 @@ function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition)); $afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength)); - $firstPartOutput = ' '; - $endPartOutput = ' '; - $count = 0; + $firstPartOutput = ' '; + $endPartOutput = ' '; + $count = 0; foreach (array_reverse($beforeWords) as $beforeWord) { $beforeWordLength = mb_strlen($beforeWord); 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 From 5b0892225120ac5d5d811ec1e68b78efa51c3077 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 5 Jan 2025 18:14:32 +0300 Subject: [PATCH 7/8] refactor: Rework test `TextHelper` --- tests/system/Helpers/TextHelperTest.php | 35 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index 8f807f0431c9..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,27 +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)); - $multibyteString = 'Давным-давно во фреймворке не было тестов. Это печально. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем счастливее становилось. Все были счастливы.'; $multibyteResult = ' Давным-давно во фреймворке не было тестов. Это печ льно. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем ...'; - $this->assertSame(excerpt($multibyteString), $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); - $multibyteString = 'Давным-давно во фреймворке не было тестов. Это печально. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем счастливее становилось. Все были счастливы.'; $multibyteResult = '... Это печально . И вот ...'; - $this->assertSame(excerpt($multibyteString, 'печально', 10), $multibyteResult); + $this->assertSame($multibyteResult, excerpt($this->mbLongString, 'печально', 10)); } public function testAlternator(): void From 3bdc25c98411c359bf93794b8742f7235261d612 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Tue, 7 Jan 2025 21:16:58 +0300 Subject: [PATCH 8/8] docs: Move to 4.6 branch --- user_guide_src/source/changelogs/v4.5.8.rst | 2 -- user_guide_src/source/changelogs/v4.6.0.rst | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.5.8.rst b/user_guide_src/source/changelogs/v4.5.8.rst index 2ea9768394b7..ae29b59fb165 100644 --- a/user_guide_src/source/changelogs/v4.5.8.rst +++ b/user_guide_src/source/changelogs/v4.5.8.rst @@ -31,8 +31,6 @@ Bugs Fixed ********** - **Database:** Fixed a bug where ``Builder::affectedRows()`` threw an error when the previous query call failed in ``Postgre`` and ``SQLSRV`` drivers. -- **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/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 `_