Skip to content

Commit 8e59dc3

Browse files
authored
[5] Orphaned Ellipsis in Articles Module (joomla#45678)
1 parent 36516f6 commit 8e59dc3

File tree

2 files changed

+37
-44
lines changed

2 files changed

+37
-44
lines changed

libraries/src/HTML/Helpers/StringHelper.php

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ abstract class StringHelper
4040
*/
4141
public static function truncate($text, $length = 0, $noSplit = true, $allowHtml = true)
4242
{
43-
// Assume a lone open tag is invalid HTML.
43+
// Assume a lone open tag is invalid HTML
4444
if ($length === 1 && $text[0] === '<') {
4545
return '...';
4646
}
@@ -156,87 +156,80 @@ public static function truncate($text, $length = 0, $noSplit = true, $allowHtml
156156
*/
157157
public static function truncateComplex($html, $maxLength = 0, $noSplit = true)
158158
{
159-
// Start with some basic rules.
160159
$baseLength = \strlen($html);
161160

162-
// If the original HTML string is shorter than the $maxLength do nothing and return that.
163-
if ($baseLength <= $maxLength || $maxLength === 0) {
161+
// Early return for trivial cases
162+
if ($maxLength === 0 || $baseLength <= $maxLength) {
164163
return $html;
165164
}
166165

167-
// Take care of short simple cases.
168-
if ($maxLength <= 3 && $html[0] !== '<' && !str_contains(substr($html, 0, $maxLength - 1), '<') && $baseLength > $maxLength) {
166+
// Special case: very short cutoff, plain text.
167+
if ($maxLength <= 3 && $html[0] !== '<' && !str_contains(substr($html, 0, max(0, $maxLength - 1)), '<')) {
169168
return '...';
170169
}
171170

172-
// Deal with maximum length of 1 where the string starts with a tag.
171+
// Special case: string starts with a tag and maxLength is 1
173172
if ($maxLength === 1 && $html[0] === '<') {
174-
$endTagPos = \strlen(strstr($html, '>', true));
175-
$tag = substr($html, 1, $endTagPos);
176-
177-
$l = $endTagPos + 1;
178-
179-
if ($noSplit) {
180-
return substr($html, 0, $l) . '</' . $tag . '...';
173+
$endTagPos = strpos($html, '>');
174+
if ($endTagPos === false) {
175+
return '...';
181176
}
182-
183-
// @todo: $character doesn't seem to be used...
184-
$character = substr(strip_tags($html), 0, 1);
185-
186-
return substr($html, 0, $l) . '</' . $tag . '...';
177+
$tag = substr($html, 1, $endTagPos - 1);
178+
return substr($html, 0, $endTagPos + 1) . "</$tag>...";
187179
}
188180

189-
// First get the truncated plain text string. This is the rendered text we want to end up with.
190-
$ptString = HTMLHelper::_('string.truncate', $html, $maxLength, $noSplit, $allowHtml = false);
181+
// Get a plain text truncated string
182+
$ptString = HTMLHelper::_('string.truncate', $html, $maxLength, $noSplit, false);
191183

192-
// It's all HTML, just return it.
193184
if ($ptString === '') {
194185
return $html;
195186
}
196-
197-
// If the plain text is shorter than the max length the variable will not end in ...
198-
// In that case we use the whole string.
199187
if (!str_ends_with($ptString, '...')) {
200188
return $html;
201189
}
202-
203-
// Regular truncate gives us the ellipsis but we want to go back for text and tags.
204190
if ($ptString === '...') {
205191
$stripped = substr(strip_tags($html), 0, $maxLength);
206-
$ptString = HTMLHelper::_('string.truncate', $stripped, $maxLength, $noSplit, $allowHtml = false);
192+
$ptString = HTMLHelper::_('string.truncate', $stripped, $maxLength, $noSplit, false);
207193
}
208-
209-
// We need to trim the ellipsis that truncate adds.
210194
$ptString = rtrim($ptString, '.');
211195

212-
// Now deal with more complex truncation.
213196
while ($maxLength <= $baseLength) {
214-
// Get the truncated string assuming HTML is allowed.
215-
$htmlString = HTMLHelper::_('string.truncate', $html, $maxLength, $noSplit, $allowHtml = true);
197+
$htmlString = HTMLHelper::_('string.truncate', $html, $maxLength, $noSplit, true);
216198

217199
if ($htmlString === '...' && \strlen($ptString) + 3 > $maxLength) {
218-
return $htmlString;
200+
return '...';
219201
}
220202

221203
$htmlString = rtrim($htmlString, '.');
222204

223-
// Now get the plain text from the HTML string and trim it.
224-
$htmlStringToPtString = HTMLHelper::_('string.truncate', $htmlString, $maxLength, $noSplit, $allowHtml = false);
205+
// Get the plain text version of the truncated HTML string
206+
$htmlStringToPtString = HTMLHelper::_('string.truncate', $htmlString, $maxLength, $noSplit, false);
225207
$htmlStringToPtString = rtrim($htmlStringToPtString, '.');
226208

227-
// If the new plain text string matches the original plain text string we are done.
209+
// If plain text matches, we're done
228210
if ($ptString === $htmlStringToPtString) {
211+
// Remove whitespace, non-breaking spaces, and trailing tags before the ellipsis
212+
$htmlString = preg_replace('/(&nbsp;|\s)+(<\/[^>]+>)?$/u', '', $htmlString);
213+
214+
// If it ends with a closing tag, try to inject the ellipsis before the last closing tag
215+
if (preg_match('/(<\/[^>]+>)$/', $htmlString, $matches)) {
216+
return preg_replace('/(<\/[^>]+>)$/', '...$1', $htmlString);
217+
}
229218
return $htmlString . '...';
230219
}
231220

232-
// Get the number of HTML tag characters in the first $maxLength characters
221+
// Adjust length for HTML tags
233222
$diffLength = \strlen($ptString) - \strlen($htmlStringToPtString);
234-
235223
if ($diffLength <= 0) {
224+
// Remove whitespace, non-breaking spaces, and trailing tags before the ellipsis
225+
$htmlString = preg_replace('/(&nbsp;|\s)+(<\/[^>]+>)?$/u', '', $htmlString);
226+
227+
// If it ends with a closing tag, inject the ellipsis before the last closing tag
228+
if (preg_match('/(<\/[^>]+>)$/', $htmlString, $matches)) {
229+
return preg_replace('/(<\/[^>]+>)$/', '...$1', $htmlString);
230+
}
236231
return $htmlString . '...';
237232
}
238-
239-
// Set new $maxlength that adjusts for the HTML tags
240233
$maxLength += $diffLength;
241234
}
242235

tests/Unit/Libraries/Cms/Html/HtmlStringTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ public function getTestTruncateComplexData(): array
328328
'<span>Plain text</span>',
329329
8,
330330
true,
331-
'<span>Plain</span>...',
331+
'<span>Plain...</span>',
332332
],
333333
/*
334334
* @todo: Check these tests: 'Plain html over the limit splitting first word'
@@ -366,13 +366,13 @@ public function getTestTruncateComplexData(): array
366366
'<div><span><i>Plain</i> <b>text</b> foo</span></div>',
367367
8,
368368
false,
369-
'<div><span><i>Plain</i> <b>te</b></span></div>...',
369+
'<div><span><i>Plain</i> <b>te</b></span>...</div>',
370370
],
371371
'No split' => [
372372
'<div><span><i>Plain</i> <b>text</b> foo</span></div>',
373373
8,
374374
true,
375-
'<div><span><i>Plain</i></span></div>...',
375+
'<div><span><i>Plain</i></span>...</div>',
376376
],
377377
'First character is < with a maximum length of 1, no split' => [
378378
'<div><span><i>Plain</i> <b>text</b> foo</span></div>',

0 commit comments

Comments
 (0)