diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 67088bfee303..b6198d0f5614 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -253,6 +253,42 @@ public static function endsWith($haystack, $needles) return false; } + /** + * Extracts an excerpt from text that matches the first instance of a phrase. + * + * @param string $text + * @param string $phrase + * @param array $options + * @return string|null + */ + public static function excerpt($text, $phrase = '', $options = []) + { + $radius = $options['radius'] ?? 100; + $omission = $options['omission'] ?? '...'; + + preg_match('/^(.*?)('.preg_quote((string) $phrase).')(.*)$/iu', (string) $text, $matches); + + if (empty($matches)) { + return null; + } + + $start = ltrim($matches[1]); + + $start = str(mb_substr($start, max(mb_strlen($start, 'UTF-8') - $radius, 0), $radius, 'UTF-8'))->ltrim()->unless( + fn ($startWithRadius) => $startWithRadius->exactly($start), + fn ($startWithRadius) => $startWithRadius->prepend($omission), + ); + + $end = rtrim($matches[3]); + + $end = str(mb_substr($end, 0, $radius, 'UTF-8'))->rtrim()->unless( + fn ($endWithRadius) => $endWithRadius->exactly($end), + fn ($endWithRadius) => $endWithRadius->append($omission), + ); + + return $start->append($matches[2], $end)->toString(); + } + /** * Cap a string with a single instance of a given value. * diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index 224c92468391..3ae7ed89f036 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -195,6 +195,18 @@ public function exactly($value) return $this->value === $value; } + /** + * Extracts an excerpt from text that matches the first instance of a phrase. + * + * @param string $phrase + * @param array $options + * @return string|null + */ + public function excerpt($phrase = '', $options = []) + { + return Str::excerpt($this->value, $phrase, $options); + } + /** * Explode the string into an array. * diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index a031ec4c015f..89a164f05ef1 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -149,6 +149,59 @@ public function testEndsWith() $this->assertFalse(Str::endsWith('你好', 'a')); } + public function testStrExcerpt() + { + $this->assertSame('...is a beautiful morn...', Str::excerpt('This is a beautiful morning', 'beautiful', ['radius' => 5])); + $this->assertSame('This is a...', Str::excerpt('This is a beautiful morning', 'this', ['radius' => 5])); + $this->assertSame('...iful morning', Str::excerpt('This is a beautiful morning', 'morning', ['radius' => 5])); + $this->assertNull(Str::excerpt('This is a beautiful morning', 'day')); + $this->assertSame('...is a beautiful! mor...', Str::excerpt('This is a beautiful! morning', 'Beautiful', ['radius' => 5])); + $this->assertSame('...is a beautiful? mor...', Str::excerpt('This is a beautiful? morning', 'beautiful', ['radius' => 5])); + $this->assertSame('', Str::excerpt('', '', ['radius' => 0])); + $this->assertSame('a', Str::excerpt('a', 'a', ['radius' => 0])); + $this->assertSame('...b...', Str::excerpt('abc', 'B', ['radius' => 0])); + $this->assertSame('abc', Str::excerpt('abc', 'b', ['radius' => 1])); + $this->assertSame('abc...', Str::excerpt('abcd', 'b', ['radius' => 1])); + $this->assertSame('...abc', Str::excerpt('zabc', 'b', ['radius' => 1])); + $this->assertSame('...abc...', Str::excerpt('zabcd', 'b', ['radius' => 1])); + $this->assertSame('zabcd', Str::excerpt('zabcd', 'b', ['radius' => 2])); + $this->assertSame('zabcd', Str::excerpt(' zabcd ', 'b', ['radius' => 4])); + $this->assertSame('...abc...', Str::excerpt('z abc d', 'b', ['radius' => 1])); + $this->assertSame('[...]is a beautiful morn[...]', Str::excerpt('This is a beautiful morning', 'beautiful', ['omission' => '[...]', 'radius' => 5])); + $this->assertSame( + 'This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]', + Str::excerpt('This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?', 'very', + ['omission' => '[...]'], + )); + + $this->assertSame('...y...', Str::excerpt('taylor', 'y', ['radius' => 0])); + $this->assertSame('...ayl...', Str::excerpt('taylor', 'Y', ['radius' => 1])); + $this->assertSame('
The article description
', Str::excerpt('
The article description
', 'article')); + $this->assertSame('...The article desc...', Str::excerpt('
The article description
', 'article', ['radius' => 5])); + $this->assertSame('The article description', Str::excerpt(strip_tags('
The article description
'), 'article')); + $this->assertSame('', Str::excerpt(null)); + $this->assertSame('', Str::excerpt('')); + $this->assertSame('', Str::excerpt(null, '')); + $this->assertSame('T...', Str::excerpt('The article description', null, ['radius' => 1])); + $this->assertSame('The arti...', Str::excerpt('The article description', '', ['radius' => 8])); + $this->assertSame('', Str::excerpt(' ')); + $this->assertSame('The arti...', Str::excerpt('The article description', ' ', ['radius' => 4])); + $this->assertSame('...cle description', Str::excerpt('The article description', 'description', ['radius' => 4])); + $this->assertSame('T...', Str::excerpt('The article description', 'T', ['radius' => 0])); + $this->assertSame('What i?', Str::excerpt('What is the article?', 'What', ['radius' => 2, 'omission' => '?'])); + + $this->assertSame('...ö - 二 sān 大åè...', Str::excerpt('åèö - 二 sān 大åèö', '二 sān', ['radius' => 4])); + $this->assertSame('åèö - 二...', Str::excerpt('åèö - 二 sān 大åèö', 'åèö', ['radius' => 4])); + $this->assertSame('åèö - 二 sān 大åèö', Str::excerpt('åèö - 二 sān 大åèö', 'åèö - 二 sān 大åèö', ['radius' => 4])); + $this->assertSame('åèö - 二 sān 大åèö', Str::excerpt('åèö - 二 sān 大åèö', 'åèö - 二 sān 大åèö', ['radius' => 4])); + $this->assertSame('...༼...', Str::excerpt('㏗༼㏗', '༼', ['radius' => 0])); + $this->assertSame('...༼...', Str::excerpt('㏗༼㏗', '༼', ['radius' => 0])); + $this->assertSame('...ocê e...', Str::excerpt('Como você está', 'ê', ['radius' => 2])); + $this->assertSame('...ocê e...', Str::excerpt('Como você está', 'Ê', ['radius' => 2])); + $this->assertSame('João...', Str::excerpt('João Antônio ', 'jo', ['radius' => 2])); + $this->assertSame('João Antô...', Str::excerpt('João Antônio', 'JOÃO', ['radius' => 5])); + } + public function testStrBefore() { $this->assertSame('han', Str::before('hannah', 'nah')); diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index c7cb5125794a..5fdeae14216a 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -474,6 +474,11 @@ public function testEndsWith() $this->assertFalse($this->stringable('Malmö')->endsWith('mo')); } + public function testExcerpt() + { + $this->assertSame('...is a beautiful morn...', (string) $this->stringable('This is a beautiful morning')->excerpt('beautiful', ['radius' => 5])); + } + public function testBefore() { $this->assertSame('han', (string) $this->stringable('hannah')->before('nah'));