Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add support for multibyte strings #9372

Merged
merged 8 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
michalsn marked this conversation as resolved.
Show resolved Hide resolved
{
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));
paulbalandan marked this conversation as resolved.
Show resolved Hide resolved
$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
Loading