public function truncatePreservingTags($str, $textMaxLength, $postString = '...', $encoding = 'UTF-8')
    {
        // complete string length with tags
        $htmlLength = mb_strlen($str, $encoding);
        // cursor position
        $htmlPos = 0;
        // extracted text length without tags
        $textLength = 0;
        // tags that need to closed
        $closeTag = array();

        if ($htmlLength <= $textMaxLength) {
            return $str;
        }

        $inEntity = false;

        // loop through str, start at cursor position
        for ($i = $htmlPos; $i < $htmlLength; ++$i) {
            // extract multibyte encoded char on cursor position
            $char = mb_substr($str, $i, 1, $encoding);

            if ($char === '&') {
                if (!$inEntity) {
                    $inEntity = true;
                }
            } elseif ($char === ';') {
                if ($inEntity) {
                    $inEntity = false;
                }
            } // check on an angle bracket assuming text following is a tag
            elseif ($char === '<') {
                // sub cursor position inside the tag (after the first angle bracket)
                $tagPos = $i + 1;
                // remember first position inside the tag
                $tagFirst = $tagPos;
                // size of tag (including angle brackets <1234>)
                $tagSize = 1;
                // tag name
                $tagName = '';

                // tag is valid ( <tag> OR </tag> )
                $isTag = true;
                // tag is a closing tag </tag>
                $isClosingTag = false;

                // loop through text inside the tag until it is closed
                for ($j = $tagPos; $j < $htmlLength; ++$j) {
                    // extract multibyte encoded char on sub cursor position
                    $charTag = mb_substr($str, $j, 1, $encoding);

                    // another opening angle bracket = first angle bracket serves as "smaller as"
                    // not a valid tag, set mark and break
                    if ($charTag === '<') {
                        $isTag = false;
                        break;
                    }
                    // seems to be an '<>' entity
                    // not a valid tag, set mark and break
                    elseif ($tagFirst === $j
                        && $charTag === '>'
                    ) {
                        $isTag = false;
                        break;
                    } // closing tag, set mark
                    elseif ($tagFirst === $j
                        && $charTag === '/'
                    ) {
                        $isClosingTag = true;
                    } // tag has ended, break
                    elseif ($charTag === '>') {
                        break;
                    } // remember char as tag name
                    else {
                        $tagName .= $charTag;
                    }

                    ++$tagSize;
                }

                // valid tag
                if (!empty($isTag)) {
                    // closing tag </tag>
                    if (!empty($isClosingTag)) {
                        // remove it from closing array
                        array_pop($closeTag);
                    } // not a closing tag
                    else {
                        // remember to close it
                        $closeTag[] = $tagName;
                    }

                    // set cursor position, continue with next char after the tag
                    $i = $i + $tagSize;
                } // not a valid tag
                else {
                    // set assumed tag size as additional textLength, because it is text
                    $textLength = $textLength + $tagSize;
                    // set cursor position and reset the last < or > char to start a new check
                    $i = $i + $tagSize - 1;
                }
            } // normal char, increase textLength
            else {
                ++$textLength;
            }

            // if maximum length reached break
            if (!$inEntity && $textLength >= $textMaxLength) {
                break;
            }
        }

        // set cursor to new position
        $htmlPos = $i + 1;

        // extract preview text from 0 to current cursor position
        $ret = mb_substr($str, 0, $htmlPos, $encoding);

        // if necessary set placeholder string (before tags get closed)
        if ($textLength >= $textMaxLength) {
            $ret .= $postString;
        }

        // if we are between tags, close them correctly
        if (count($closeTag)) {
            foreach (array_reverse($closeTag) as $tag) {
                $tokens = explode(' ', $tag);
                $ret .= '</'.$tokens[0].'>';
            }
        }

        return $ret;
    }
public function truncatePreservingTags($str, $textMaxLength, $postString = '...', $encoding = 'UTF-8')
    {
        // complete string length with tags
        $htmlLength = mb_strlen($str, $encoding);
        // cursor position
        $htmlPos = 0;
        // extracted text length without tags
        $textLength = 0;
        // tags that need to closed
        $closeTag = array();

        if ($htmlLength <= $textMaxLength) {
            return $str;
        }

        $inEntity = false;

        // loop through str, start at cursor position
        for ($i = $htmlPos; $i < $htmlLength; ++$i) {
            // extract multibyte encoded char on cursor position
            $char = mb_substr($str, $i, 1, $encoding);

            if ($char === '&') {
                if (!$inEntity) {
                    $inEntity = true;
                }
            } elseif ($char === ';') {
                if ($inEntity) {
                    $inEntity = false;
                }
            } // check on an angle bracket assuming text following is a tag
            elseif ($char === '<') {
                // sub cursor position inside the tag (after the first angle bracket)
                $tagPos = $i + 1;
                // remember first position inside the tag
                $tagFirst = $tagPos;
                // size of tag (including angle brackets <1234>)
                $tagSize = 1;
                // tag name
                $tagName = '';

                // tag is valid ( <tag> OR </tag> )
                $isTag = true;
                // tag is a closing tag </tag>
                $isClosingTag = false;

                // loop through text inside the tag until it is closed
                for ($j = $tagPos; $j < $htmlLength; ++$j) {
                    // extract multibyte encoded char on sub cursor position
                    $charTag = mb_substr($str, $j, 1, $encoding);

                    // another opening angle bracket = first angle bracket serves as "smaller as"
                    // not a valid tag, set mark and break
                    if ($charTag === '<') {
                        $isTag = false;
                        break;
                    }
                    // seems to be an '<>' entity
                    // not a valid tag, set mark and break
                    elseif ($tagFirst === $j
                        && $charTag === '>'
                    ) {
                        $isTag = false;
                        break;
                    } // closing tag, set mark
                    elseif ($tagFirst === $j
                        && $charTag === '/'
                    ) {
                        $isClosingTag = true;
                    } // tag has ended, break
                    elseif ($charTag === '>') {
                        break;
                    } // remember char as tag name
                    else {
                        $tagName .= $charTag;
                    }

                    ++$tagSize;
                }

                // valid tag
                if (!empty($isTag)) {
                    // closing tag </tag>
                    if (!empty($isClosingTag)) {
                        // remove it from closing array
                        array_pop($closeTag);
                    } // not a closing tag
                    else {
                        // remember to close it
                        $closeTag[] = $tagName;
                    }

                    // set cursor position, continue with next char after the tag
                    $i = $i + $tagSize;
                } // not a valid tag
                else {
                    // set assumed tag size as additional textLength, because it is text
                    $textLength = $textLength + $tagSize;
                    // set cursor position and reset the last < or > char to start a new check
                    $i = $i + $tagSize - 1;
                }
            } // normal char, increase textLength
            else {
                ++$textLength;
            }

            // if maximum length reached break
            if (!$inEntity && $textLength >= $textMaxLength) {
                break;
            }
        }

        // set cursor to new position
        $htmlPos = $i + 1;

        // extract preview text from 0 to current cursor position
        $ret = mb_substr($str, 0, $htmlPos, $encoding);

        // if necessary set placeholder string (before tags get closed)
        if ($textLength >= $textMaxLength) {
            $ret .= $postString;
        }

        // if we are between tags, close them correctly
        if (count($closeTag)) {
            foreach (array_reverse($closeTag) as $tag) {
                $tokens = explode(' ', $tag);
                $ret .= '</'.$tokens[0].'>';
            }
        }

        return $ret;
    }
public function truncatePreservingTags ( $ str , $ textMaxLength , $ postString = '...' , $ encoding = 'UTF-8' ) { // complete string length with tags $ htmlLength = mb_strlen ( $ str , $ encoding ) ; // cursor position $ htmlPos = 0 ; // extracted text length without tags $ textLength = 0 ; // tags that need to closed $ closeTag = array ( ) ; if ( $ htmlLength <= $ textMaxLength ) { return $ str ; } $ inEntity = false ; // loop through str, start at cursor position for ( $ i = $ htmlPos ; $ i < $ htmlLength ; ++ $ i ) { // extract multibyte encoded char on cursor position $ char = mb_substr ( $ str , $ i , 1 , $ encoding ) ; if ( $ char === '&' ) { if ( ! $ inEntity ) { $ inEntity = true ; } } elseif ( $ char === ';' ) { if ( $ inEntity ) { $ inEntity = false ; } } // check on an angle bracket assuming text following is a tag elseif ( $ char === '<' ) { // sub cursor position inside the tag (after the first angle bracket) $ tagPos = $ i + 1 ; // remember first position inside the tag $ tagFirst = $ tagPos ; // size of tag (including angle brackets <1234>) $ tagSize = 1 ; // tag name $ tagName = '' ; // tag is valid ( <tag> OR </tag> ) $ isTag = true ; // tag is a closing tag </tag> $ isClosingTag = false ; // loop through text inside the tag until it is closed for ( $ j = $ tagPos ; $ j < $ htmlLength ; ++ $ j ) { // extract multibyte encoded char on sub cursor position $ charTag = mb_substr ( $ str , $ j , 1 , $ encoding ) ; // another opening angle bracket = first angle bracket serves as "smaller as" // not a valid tag, set mark and break if ( $ charTag === '<' ) { $ isTag = false ; break ; } // seems to be an '<>' entity // not a valid tag, set mark and break elseif ( $ tagFirst === $ j && $ charTag === '>' ) { $ isTag = false ; break ; } // closing tag, set mark elseif ( $ tagFirst === $ j && $ charTag === '/' ) { $ isClosingTag = true ; } // tag has ended, break elseif ( $ charTag === '>' ) { break ; } // remember char as tag name else { $ tagName .= $ charTag ; } ++ $ tagSize ; } // valid tag if ( ! empty ( $ isTag ) ) { // closing tag </tag> if ( ! empty ( $ isClosingTag ) ) { // remove it from closing array array_pop ( $ closeTag ) ; } // not a closing tag else { // remember to close it $ closeTag [ ] = $ tagName ; } // set cursor position, continue with next char after the tag $ i = $ i + $ tagSize ; } // not a valid tag else { // set assumed tag size as additional textLength, because it is text $ textLength = $ textLength + $ tagSize ; // set cursor position and reset the last < or > char to start a new check $ i = $ i + $ tagSize - 1 ; } } // normal char, increase textLength else { ++ $ textLength ; } // if maximum length reached break if ( ! $ inEntity && $ textLength >= $ textMaxLength ) { break ; } } // set cursor to new position $ htmlPos = $ i + 1 ; // extract preview text from 0 to current cursor position $ ret = mb_substr ( $ str , 0 , $ htmlPos , $ encoding ) ; // if necessary set placeholder string (before tags get closed) if ( $ textLength >= $ textMaxLength ) { $ ret .= $ postString ; } // if we are between tags, close them correctly if ( count ( $ closeTag ) ) { foreach ( array_reverse ( $ closeTag ) as $ tag ) { $ tokens = explode ( ' ' , $ tag ) ; $ ret .= '</' . $ tokens [ 0 ] . '>' ; } } return $ ret ; }
Function truncates a HTML string, preserving and closing all tags.

@param string $str
@param int    $textMaxLength
@param string $postString
@param string $encoding

@return string
Function truncates a HTML string preserving and closing all tags .