core-bundle/src/Resources/contao/library/Contao/StringUtil.php
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))
			{
				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);
	}
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))
			{
				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);
	}
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 ) ) { 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 ) ; }
Shorten a HTML string to a given number of characters

The function preserves words, so the result might be a bit shorter or
longer than the number of characters given. It preserves allowed tags.

@param string  $strString        The string to shorten
@param integer $intNumberOfChars The target number of characters

@return string The shortened HTML string
Shorten a HTML string to a given number of characters