Skip to content

Commit c79d3a9

Browse files
authored
fix: Add support for multibyte strings (#9372)
* 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
1 parent 24a5ae0 commit c79d3a9

File tree

8 files changed

+102
-49
lines changed

8 files changed

+102
-49
lines changed

system/Commands/Utilities/Namespaces.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,10 @@ private function outputAllNamespaces(array $params): array
120120

121121
private function truncate(string $string, int $max): string
122122
{
123-
$length = strlen($string);
123+
$length = mb_strlen($string);
124124

125125
if ($length > $max) {
126-
return substr($string, 0, $max - 3) . '...';
126+
return mb_substr($string, 0, $max - 3) . '...';
127127
}
128128

129129
return $string;

system/Helpers/text_helper.php

+42-31
Original file line numberDiff line numberDiff line change
@@ -44,35 +44,40 @@ function word_limiter(string $str, int $limit = 100, string $endChar = '…'
4444
/**
4545
* Character Limiter
4646
*
47-
* Limits the string based on the character count. Preserves complete words
47+
* Limits the string based on the character count. Preserves complete words
4848
* so the character count may not be exactly as specified.
4949
*
5050
* @param string $endChar the end character. Usually an ellipsis
5151
*/
52-
function character_limiter(string $str, int $n = 500, string $endChar = '…'): string
52+
function character_limiter(string $string, int $limit = 500, string $endChar = '…'): string
5353
{
54-
if (mb_strlen($str) < $n) {
55-
return $str;
54+
if (mb_strlen($string) < $limit) {
55+
return $string;
5656
}
5757

5858
// a bit complicated, but faster than preg_replace with \s+
59-
$str = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $str));
59+
$string = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $string));
60+
$stringLength = mb_strlen($string);
6061

61-
if (mb_strlen($str) <= $n) {
62-
return $str;
62+
if ($stringLength <= $limit) {
63+
return $string;
6364
}
6465

65-
$out = '';
66+
$output = '';
67+
$outputLength = 0;
68+
$words = explode(' ', trim($string));
6669

67-
foreach (explode(' ', trim($str)) as $val) {
68-
$out .= $val . ' ';
69-
if (mb_strlen($out) >= $n) {
70-
$out = trim($out);
70+
foreach ($words as $word) {
71+
$output .= $word . ' ';
72+
$outputLength = mb_strlen($output);
73+
74+
if ($outputLength >= $limit) {
75+
$output = trim($output);
7176
break;
7277
}
7378
}
7479

75-
return (mb_strlen($out) === mb_strlen($str)) ? $out : $out . $endChar;
80+
return ($outputLength === $stringLength) ? $output : $output . $endChar;
7681
}
7782
}
7883

@@ -712,38 +717,44 @@ function alternator(...$args): string
712717
function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string
713718
{
714719
if (isset($phrase)) {
715-
$phrasePos = stripos($text, $phrase);
716-
$phraseLen = strlen($phrase);
720+
$phrasePosition = mb_stripos($text, $phrase);
721+
$phraseLength = mb_strlen($phrase);
717722
} else {
718-
$phrasePos = $radius / 2;
719-
$phraseLen = 1;
723+
$phrasePosition = $radius / 2;
724+
$phraseLength = 1;
720725
}
721726

722-
$pre = explode(' ', substr($text, 0, $phrasePos));
723-
$pos = explode(' ', substr($text, $phrasePos + $phraseLen));
727+
$beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition));
728+
$afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength));
724729

725-
$prev = ' ';
726-
$post = ' ';
727-
$count = 0;
730+
$firstPartOutput = ' ';
731+
$endPartOutput = ' ';
732+
$count = 0;
728733

729-
foreach (array_reverse($pre) as $e) {
730-
if ((strlen($e) + $count + 1) < $radius) {
731-
$prev = ' ' . $e . $prev;
734+
foreach (array_reverse($beforeWords) as $beforeWord) {
735+
$beforeWordLength = mb_strlen($beforeWord);
736+
737+
if (($beforeWordLength + $count + 1) < $radius) {
738+
$firstPartOutput = ' ' . $beforeWord . $firstPartOutput;
732739
}
733-
$count = ++$count + strlen($e);
740+
741+
$count = ++$count + $beforeWordLength;
734742
}
735743

736744
$count = 0;
737745

738-
foreach ($pos as $s) {
739-
if ((strlen($s) + $count + 1) < $radius) {
740-
$post .= $s . ' ';
746+
foreach ($afterWords as $afterWord) {
747+
$afterWordLength = mb_strlen($afterWord);
748+
749+
if (($afterWordLength + $count + 1) < $radius) {
750+
$endPartOutput .= $afterWord . ' ';
741751
}
742-
$count = ++$count + strlen($s);
752+
753+
$count = ++$count + $afterWordLength;
743754
}
744755

745756
$ellPre = $phrase !== null ? $ellipsis : '';
746757

747-
return str_replace(' ', ' ', $ellPre . $prev . $phrase . $post . $ellipsis);
758+
return str_replace(' ', ' ', $ellPre . $firstPartOutput . $phrase . $endPartOutput . $ellipsis);
748759
}
749760
}

system/View/View.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ public function renderString(string $view, ?array $options = null, ?bool $saveDa
339339
*/
340340
public function excerpt(string $string, int $length = 20): string
341341
{
342-
return (strlen($string) > $length) ? substr($string, 0, $length - 3) . '...' : $string;
342+
return (mb_strlen($string) > $length) ? mb_substr($string, 0, $length - 3) . '...' : $string;
343343
}
344344

345345
/**

tests/system/Commands/Utilities/NamespacesTest.php

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace CodeIgniter\Commands\Utilities;
1515

1616
use CodeIgniter\Test\CIUnitTestCase;
17+
use CodeIgniter\Test\ReflectionHelper;
1718
use CodeIgniter\Test\StreamFilterTrait;
1819
use PHPUnit\Framework\Attributes\Group;
1920

@@ -24,6 +25,7 @@
2425
final class NamespacesTest extends CIUnitTestCase
2526
{
2627
use StreamFilterTrait;
28+
use ReflectionHelper;
2729

2830
protected function setUp(): void
2931
{
@@ -84,4 +86,14 @@ public function testNamespacesCommandAllNamespaces(): void
8486
str_replace(' ', '', $this->getBuffer())
8587
);
8688
}
89+
90+
public function testTruncateNamespaces(): void
91+
{
92+
$commandObject = new Namespaces(service('logger'), service('commands'));
93+
$truncateRunner = $this->getPrivateMethodInvoker($commandObject, 'truncate');
94+
95+
$this->assertSame('App\Controllers\...', $truncateRunner('App\Controllers\Admin', 19));
96+
// multibyte namespace
97+
$this->assertSame('App\Контроллеры\...', $truncateRunner('App\Контроллеры\Админ', 19));
98+
}
8799
}

tests/system/Helpers/TextHelperTest.php

+28-9
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
#[Group('Others')]
2525
final class TextHelperTest extends CIUnitTestCase
2626
{
27-
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.';
27+
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.';
28+
private string $mbLongString = 'Давным-давно во фреймворке не было тестов. Это печально. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем счастливее становилось. Все были счастливы.';
2829

2930
protected function setUp(): void
3031
{
@@ -165,19 +166,29 @@ public function testIncrementString(): void
165166

166167
public function testWordLimiter(): void
167168
{
168-
$this->assertSame('Once upon a time,&#8230;', word_limiter($this->_long_string, 4));
169-
$this->assertSame('Once upon a time,&hellip;', word_limiter($this->_long_string, 4, '&hellip;'));
169+
$this->assertSame('Once upon a time,&#8230;', word_limiter($this->longString, 4));
170+
$this->assertSame('Once upon a time,&hellip;', word_limiter($this->longString, 4, '&hellip;'));
170171
$this->assertSame('', word_limiter('', 4));
171-
$this->assertSame('Once upon a&hellip;', word_limiter($this->_long_string, 3, '&hellip;'));
172+
$this->assertSame('Once upon a&hellip;', word_limiter($this->longString, 3, '&hellip;'));
172173
$this->assertSame('Once upon a time', word_limiter('Once upon a time', 4, '&hellip;'));
174+
175+
$this->assertSame('Давным-давно во фреймворке не было тестов.&#8230;', word_limiter($this->mbLongString, 6));
176+
$this->assertSame('Давным-давно во фреймворке не было тестов.&hellip;', word_limiter($this->mbLongString, 6, '&hellip;'));
177+
$this->assertSame('Давным-давно во фреймворке&hellip;', word_limiter($this->mbLongString, 3, '&hellip;'));
178+
$this->assertSame('Давным-давно во фреймворке не было тестов.', word_limiter('Давным-давно во фреймворке не было тестов.', 6, '&hellip;'));
173179
}
174180

175181
public function testCharacterLimiter(): void
176182
{
177-
$this->assertSame('Once upon a time, a&#8230;', character_limiter($this->_long_string, 20));
178-
$this->assertSame('Once upon a time, a&hellip;', character_limiter($this->_long_string, 20, '&hellip;'));
183+
$this->assertSame('Once upon a time, a&#8230;', character_limiter($this->longString, 20));
184+
$this->assertSame('Once upon a time, a&hellip;', character_limiter($this->longString, 20, '&hellip;'));
179185
$this->assertSame('Short', character_limiter('Short', 20));
180186
$this->assertSame('Short', character_limiter('Short', 5));
187+
188+
$this->assertSame('Давным-давно во фреймворке не было тестов.&#8230;', character_limiter($this->mbLongString, 41));
189+
$this->assertSame('Давным-давно во фреймворке не было тестов.&hellip;', character_limiter($this->mbLongString, 41, '&hellip;'));
190+
$this->assertSame('Короткий', character_limiter('Короткий', 20));
191+
$this->assertSame('Короткий', character_limiter('Короткий', 8));
181192
}
182193

183194
public function testAsciiToEntities(): void
@@ -391,17 +402,25 @@ public function testDefaultWordWrapCharlim(): void
391402

392403
public function testExcerpt(): void
393404
{
394-
$string = $this->_long_string;
405+
$string = $this->longString;
395406
$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. ...';
396-
$this->assertSame(excerpt($string), $result);
407+
$this->assertSame($result, excerpt($string));
408+
409+
$multibyteResult = ' Давным-давно во фреймворке не было тестов. Это печ льно. И вот несколько хороших людей начали писать тесты. Чем больше времени проходило, тем ...';
410+
411+
$this->assertSame($multibyteResult, excerpt($this->mbLongString));
397412
}
398413

399414
public function testExcerptRadius(): void
400415
{
401-
$string = $this->_long_string;
416+
$string = $this->longString;
402417
$phrase = 'began';
403418
$result = '... people began to ...';
404419
$this->assertSame(excerpt($string, $phrase, 10), $result);
420+
421+
$multibyteResult = '... Это печально . И вот ...';
422+
423+
$this->assertSame($multibyteResult, excerpt($this->mbLongString, 'печально', 10));
405424
}
406425

407426
public function testAlternator(): void

tests/system/View/ViewTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -405,4 +405,12 @@ public function testRenderSectionSavingData(): void
405405
$view->setVar('testString', 'Hello World');
406406
$this->assertStringContainsString($expected, $view->render('extend_reuse_section'));
407407
}
408+
409+
public function testViewExcerpt(): void
410+
{
411+
$view = new View($this->config, $this->viewsDir, $this->loader);
412+
413+
$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));
414+
$this->assertSame('CodeIgniter - это полнофункциональный веб-фреймворк...', $view->excerpt('CodeIgniter - это полнофункциональный веб-фреймворк на PHP, который является легким, быстрым, гибким и безопасным.', 54));
415+
}
408416
}

user_guide_src/source/changelogs/v4.6.0.rst

+3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ Method Signature Changes
168168
- **Time:** The first parameter type of the ``createFromTimestamp()`` has been
169169
changed from ``int`` to ``int|float``, and the return type ``static`` has been
170170
added.
171+
- **Helpers:** ``character_limiter()`` parameter names have been updated. If you use named arguments, you need to update the function calls.
171172

172173
Removed Type Definitions
173174
------------------------
@@ -350,6 +351,8 @@ Bugs Fixed
350351
- **Response:**
351352
- Headers set using the ``Response`` class are now prioritized and replace headers
352353
that can be set manually using the PHP ``header()`` function.
354+
- **View:** Added support for multibyte strings for ``View::excerpt()``.
355+
- **Helpers:** Added support for multibyte strings for ``excerpt()``.
353356

354357
See the repo's
355358
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_

user_guide_src/source/helpers/text_helper.rst

+6-6
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ The following functions are available:
166166

167167
.. literalinclude:: text_helper/012.php
168168

169-
.. php:function:: word_limiter($str[, $limit = 100[, $end_char = '&#8230;']])
169+
.. php:function:: word_limiter($str[, $limit = 100[, $endChar = '&#8230;']])
170170
171171
:param string $str: Input string
172172
:param int $limit: Limit
173-
:param string $end_char: End character (usually an ellipsis)
173+
:param string $endChar: End character (usually an ellipsis)
174174
:returns: Word-limited string
175175
:rtype: string
176176

@@ -181,11 +181,11 @@ The following functions are available:
181181
The third parameter is an optional suffix added to the string. By
182182
default it adds an ellipsis.
183183

184-
.. php:function:: character_limiter($str[, $n = 500[, $end_char = '&#8230;']])
184+
.. php:function:: character_limiter($string[, $limit = 500[, $endChar = '&#8230;']])
185185
186-
:param string $str: Input string
187-
:param int $n: Number of characters
188-
:param string $end_char: End character (usually an ellipsis)
186+
:param string $string: Input string
187+
:param int $limit: Number of characters
188+
:param string $endChar: End character (usually an ellipsis)
189189
:returns: Character-limited string
190190
:rtype: string
191191

0 commit comments

Comments
 (0)