/**
* Truncates text blocks over the specified character limit and closes
* all open HTML tags. The method will optionally not truncate an individual
* word, it will find the first space that is within the limit and
* truncate at that point. This method is UTF-8 safe.
*
* @param string $text The text to truncate.
* @param integer $length The maximum length of the text.
* @param boolean $noSplit Don't split a word if that is where the cutoff occurs (default: true).
* @param boolean $allowHtml Allow HTML tags in the output, and close any open tags (default: true).
*
* @return string The truncated text.
*
* @since 1.6
*/
public static function truncate($text, $length = 0, $noSplit = true, $allowHtml = true)
{
// Assume a lone open tag is invalid HTML.
if ($length === 1 && $text[0] === '<') {
return '...';
}
// Check if HTML tags are allowed.
if (!$allowHtml) {
// Deal with spacing issues in the input.
$text = str_replace('>', '> ', $text);
$text = str_replace(array(' ', ' '), ' ', $text);
$text = FrameworkStringHelper::trim(preg_replace('#\\s+#mui', ' ', $text));
// Strip the tags from the input and decode entities.
$text = strip_tags($text);
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
// Remove remaining extra spaces.
$text = str_replace(' ', ' ', $text);
$text = FrameworkStringHelper::trim(preg_replace('#\\s+#mui', ' ', $text));
}
// Whether or not allowing HTML, truncate the item text if it is too long.
if ($length > 0 && FrameworkStringHelper::strlen($text) > $length) {
$tmp = trim(FrameworkStringHelper::substr($text, 0, $length));
if ($tmp[0] === '<' && strpos($tmp, '>') === false) {
return '...';
}
// $noSplit true means that we do not allow splitting of words.
if ($noSplit) {
// Find the position of the last space within the allowed length.
$offset = FrameworkStringHelper::strrpos($tmp, ' ');
$tmp = FrameworkStringHelper::substr($tmp, 0, $offset + 1);
// If there are no spaces and the string is longer than the maximum
// we need to just use the ellipsis. In that case we are done.
if ($offset === false && strlen($text) > $length) {
return '...';
}
if (FrameworkStringHelper::strlen($tmp) > $length - 3) {
$tmp = trim(FrameworkStringHelper::substr($tmp, 0, FrameworkStringHelper::strrpos($tmp, ' ')));
}
}
if ($allowHtml) {
// Put all opened tags into an array
preg_match_all("#<([a-z][a-z0-9]*)\\b.*?(?!/)>#i", $tmp, $result);
$openedTags = $result[1];
// Some tags self close so they do not need a separate close tag.
$openedTags = array_diff($openedTags, array('img', 'hr', 'br'));
$openedTags = array_values($openedTags);
// Put all closed tags into an array
preg_match_all("#</([a-z][a-z0-9]*)\\b(?:[^>]*?)>#iU", $tmp, $result);
$closedTags = $result[1];
$numOpened = count($openedTags);
// Not all tags are closed so trim the text and finish.
if (count($closedTags) !== $numOpened) {
// Closing tags need to be in the reverse order of opening tags.
$openedTags = array_reverse($openedTags);
// Close tags
for ($i = 0; $i < $numOpened; $i++) {
if (!in_array($openedTags[$i], $closedTags)) {
$tmp .= '</' . $openedTags[$i] . '>';
} else {
unset($closedTags[array_search($openedTags[$i], $closedTags)]);
}
}
}
// Check if we are within a tag
if (FrameworkStringHelper::strrpos($tmp, '<') > FrameworkStringHelper::strrpos($tmp, '>')) {
$offset = FrameworkStringHelper::strrpos($tmp, '<');
$tmp = FrameworkStringHelper::trim(FrameworkStringHelper::substr($tmp, 0, $offset));
}
}
if ($tmp === false || strlen($text) > strlen($tmp)) {
$text = trim($tmp) . '...';
}
}
// Clean up any internal spaces created by the processing.
$text = str_replace(' </', '</', $text);
$text = str_replace(' ...', '...', $text);
return $text;
}