Skip to content

Commit

Permalink
fix: Add support for multibyte strings (#9372)
Browse files Browse the repository at this point in the history
* fix: Truncate multibyte namespaces in command

* fix: Add support multibyte to `View->excerpt()`

* fix: Add support multibyte to `excerpt()` helper

* docs: Update changelog

* fix: Improve readability `excerpt()`

* refactor: Rework `character_limiter()`

* refactor: Rework test `TextHelper`

* docs: Move to 4.6 branch
  • Loading branch information
neznaika0 authored Jan 7, 2025
1 parent 24a5ae0 commit c79d3a9
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 49 deletions.
4 changes: 2 additions & 2 deletions system/Commands/Utilities/Namespaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
73 changes: 42 additions & 31 deletions system/Helpers/text_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion system/View/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions tests/system/Commands/Utilities/NamespacesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,6 +25,7 @@
final class NamespacesTest extends CIUnitTestCase
{
use StreamFilterTrait;
use ReflectionHelper;

protected function setUp(): void
{
Expand Down Expand Up @@ -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));
}
}
37 changes: 28 additions & 9 deletions tests/system/Helpers/TextHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -165,19 +166,29 @@ public function testIncrementString(): void

public function testWordLimiter(): void
{
$this->assertSame('Once upon a time,&#8230;', word_limiter($this->_long_string, 4));
$this->assertSame('Once upon a time,&hellip;', word_limiter($this->_long_string, 4, '&hellip;'));
$this->assertSame('Once upon a time,&#8230;', word_limiter($this->longString, 4));
$this->assertSame('Once upon a time,&hellip;', word_limiter($this->longString, 4, '&hellip;'));
$this->assertSame('', word_limiter('', 4));
$this->assertSame('Once upon a&hellip;', word_limiter($this->_long_string, 3, '&hellip;'));
$this->assertSame('Once upon a&hellip;', word_limiter($this->longString, 3, '&hellip;'));
$this->assertSame('Once upon a time', word_limiter('Once upon a time', 4, '&hellip;'));

$this->assertSame('Давным-давно во фреймворке не было тестов.&#8230;', word_limiter($this->mbLongString, 6));
$this->assertSame('Давным-давно во фреймворке не было тестов.&hellip;', word_limiter($this->mbLongString, 6, '&hellip;'));
$this->assertSame('Давным-давно во фреймворке&hellip;', word_limiter($this->mbLongString, 3, '&hellip;'));
$this->assertSame('Давным-давно во фреймворке не было тестов.', word_limiter('Давным-давно во фреймворке не было тестов.', 6, '&hellip;'));
}

public function testCharacterLimiter(): void
{
$this->assertSame('Once upon a time, a&#8230;', character_limiter($this->_long_string, 20));
$this->assertSame('Once upon a time, a&hellip;', character_limiter($this->_long_string, 20, '&hellip;'));
$this->assertSame('Once upon a time, a&#8230;', character_limiter($this->longString, 20));
$this->assertSame('Once upon a time, a&hellip;', character_limiter($this->longString, 20, '&hellip;'));
$this->assertSame('Short', character_limiter('Short', 20));
$this->assertSame('Short', character_limiter('Short', 5));

$this->assertSame('Давным-давно во фреймворке не было тестов.&#8230;', character_limiter($this->mbLongString, 41));
$this->assertSame('Давным-давно во фреймворке не было тестов.&hellip;', character_limiter($this->mbLongString, 41, '&hellip;'));
$this->assertSame('Короткий', character_limiter('Короткий', 20));
$this->assertSame('Короткий', character_limiter('Короткий', 8));
}

public function testAsciiToEntities(): void
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tests/system/View/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
3 changes: 3 additions & 0 deletions user_guide_src/source/changelogs/v4.6.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------
Expand Down Expand Up @@ -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 <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
Expand Down
12 changes: 6 additions & 6 deletions user_guide_src/source/helpers/text_helper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ The following functions are available:

.. literalinclude:: text_helper/012.php

.. php:function:: word_limiter($str[, $limit = 100[, $end_char = '&#8230;']])
.. php:function:: word_limiter($str[, $limit = 100[, $endChar = '&#8230;']])
: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

Expand All @@ -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 = '&#8230;']])
.. php:function:: character_limiter($string[, $limit = 500[, $endChar = '&#8230;']])
: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

Expand Down

0 comments on commit c79d3a9

Please sign in to comment.