public static function substrHtml($strString, $intNumberOfChars)
{
$strReturn = '';
$intCharCount = 0;
$arrOpenTags = array();
$arrTagBuffer = array();
$arrEmptyTags = array('area', 'base', 'br', 'col', 'hr', 'img', 'input', 'frame', 'link', 'meta', 'param');
$strString = preg_replace('/[\\t\\n\\r]+/', ' ', $strString);
$strString = strip_tags($strString, \Config::get('allowedTags'));
$strString = preg_replace('/ +/', ' ', $strString);
// Seperate tags and text
$arrChunks = preg_split('/(<[^>]+>)/', $strString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
for ($i = 0, $c = count($arrChunks); $i < $c; $i++) {
// Buffer tags to include them later
if (preg_match('/<([^>]+)>/', $arrChunks[$i])) {
$arrTagBuffer[] = $arrChunks[$i];
continue;
}
$buffer = $arrChunks[$i];
// Get the substring of the current text
if (($arrChunks[$i] = static::substr($arrChunks[$i], $intNumberOfChars - $intCharCount, false)) == false) {
break;
}
$blnModified = $buffer !== $arrChunks[$i];
$intCharCount += Utf8::strlen(static::decodeEntities($arrChunks[$i]));
if ($intCharCount <= $intNumberOfChars) {
foreach ($arrTagBuffer as $strTag) {
$strTagName = strtolower(trim($strTag));
// Extract the tag name (see #5669)
if (($pos = strpos($strTagName, ' ')) !== false) {
$strTagName = substr($strTagName, 1, $pos - 1);
} else {
$strTagName = substr($strTagName, 1, -1);
}
// Skip empty tags
if (in_array($strTagName, $arrEmptyTags)) {
continue;
}
// Store opening tags in the open_tags array
if (strncmp($strTagName, '/', 1) !== 0) {
if (!empty($arrChunks[$i]) || $i < $c) {
$arrOpenTags[] = $strTagName;
}
continue;
}
// Closing tags will be removed from the "open tags" array
if (!empty($arrChunks[$i]) || $i < $c) {
$arrOpenTags = array_values($arrOpenTags);
for ($j = count($arrOpenTags) - 1; $j >= 0; $j--) {
if ($strTagName == '/' . $arrOpenTags[$j]) {
unset($arrOpenTags[$j]);
break;
}
}
}
}
// If the current chunk contains text, add tags and text to the return string
if (strlen($arrChunks[$i]) || $i < $c) {
$strReturn .= implode('', $arrTagBuffer) . $arrChunks[$i];
}
// Stop after the first shortened chunk (see #7311)
if ($blnModified) {
break;
}
$arrTagBuffer = array();
continue;
}
break;
}
// Close all remaining open tags
krsort($arrOpenTags);
foreach ($arrOpenTags as $strTag) {
$strReturn .= '</' . $strTag . '>';
}
return trim($strReturn);
}