force_balance_tags

The timeline below displays how wordpress function force_balance_tags has changed across different WordPress versions. If a version is not listed, refer to the next available version below.

WordPress Version: 6.4

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 * @since 5.3.0 Improve accuracy and add support for custom element tags.
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags.
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source', 'track', 'wbr');
    // Tags that can be immediately nested within themselves.
    $nestable_tags = array('article', 'aside', 'blockquote', 'details', 'div', 'figure', 'object', 'q', 'section', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'.
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number).
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    /**
     * Matches supported tags.
     *
     * To get the pattern as a string without the comments paste into a PHP
     * REPL like `php -a`.
     *
     * @see https://html.spec.whatwg.org/#elements-2
     * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
     *
     * @example
     * ~# php -a
     * php > $s = [paste copied contents of expression below including parentheses];
     * php > echo $s;
     */
    $tag_pattern = '#<' . '(/?)' . '(' . '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' . '|' . '(?:[\w:]+)' . ')' . '(?:' . '\s*' . '(/?)' . '|' . '(\s+)' . '([^>]*)' . ')' . '>#';
    while (preg_match($tag_pattern, $text, $regex)) {
        $full_match = $regex[0];
        $has_leading_slash = !empty($regex[1]);
        $tag_name = $regex[2];
        $tag = strtolower($tag_name);
        $is_single_tag = in_array($tag, $single_tags, true);
        $pre_attribute_ws = isset($regex[4]) ? $regex[4] : '';
        $attributes = trim(isset($regex[5]) ? $regex[5] : $regex[3]);
        $has_self_closer = str_ends_with($attributes, '/');
        $newtext .= $tagqueue;
        $i = strpos($text, $full_match);
        $l = strlen($full_match);
        // Clear the shifter.
        $tagqueue = '';
        if ($has_leading_slash) {
            // End tag.
            // If too many closing tags.
            if ($stacksize <= 0) {
                $tag = '';
                // Or close to be safe $tag = '/' . $tag.
                // If stacktop value = tag close value, then pop.
            } elseif ($tagstack[$stacksize - 1] === $tag) {
                // Found closing tag.
                $tag = '</' . $tag . '>';
                // Close tag.
                array_pop($tagstack);
                --$stacksize;
            } else {
                // Closing tag not at top, search for it.
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] === $tag) {
                        // Add tag to tagqueue.
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            --$stacksize;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin tag.
            if ($has_self_closer) {
                /*
                 * If it presents itself as a self-closing tag, but it isn't a known single-entity self-closing tag,
                 * then don't let it be treated as such and immediately close it with a closing tag.
                 * The tag will encapsulate no text as a result.
                 */
                if (!$is_single_tag) {
                    $attributes = trim(substr($attributes, 0, -1)) . "></{$tag}";
                }
            } elseif ($is_single_tag) {
                // Else if it's a known single-entity tag but it doesn't close itself, do so.
                $pre_attribute_ws = ' ';
                $attributes .= '/';
            } else {
                /*
                 * It's not a single-entity tag.
                 * If the top of the stack is the same as the tag we want to push, close previous tag.
                 */
                if ($stacksize > 0 && !in_array($tag, $nestable_tags, true) && $tagstack[$stacksize - 1] === $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    --$stacksize;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes.
            if ($has_self_closer && $is_single_tag) {
                // We need some space - avoid <br/> and prefer <br />.
                $pre_attribute_ws = ' ';
            }
            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
            // If already queuing a close tag, then put this tag on too.
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear tag queue.
    $newtext .= $tagqueue;
    // Add remaining text.
    $newtext .= $text;
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close.
    }
    // WP fix for the bug with HTML comments.
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 6.3

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 * @since 5.3.0 Improve accuracy and add support for custom element tags.
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags.
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source', 'track', 'wbr');
    // Tags that can be immediately nested within themselves.
    $nestable_tags = array('article', 'aside', 'blockquote', 'details', 'div', 'figure', 'object', 'q', 'section', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'.
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number).
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    /**
     * Matches supported tags.
     *
     * To get the pattern as a string without the comments paste into a PHP
     * REPL like `php -a`.
     *
     * @see https://html.spec.whatwg.org/#elements-2
     * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
     *
     * @example
     * ~# php -a
     * php > $s = [paste copied contents of expression below including parentheses];
     * php > echo $s;
     */
    $tag_pattern = '#<' . '(/?)' . '(' . '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' . '|' . '(?:[\w:]+)' . ')' . '(?:' . '\s*' . '(/?)' . '|' . '(\s+)' . '([^>]*)' . ')' . '>#';
    while (preg_match($tag_pattern, $text, $regex)) {
        $full_match = $regex[0];
        $has_leading_slash = !empty($regex[1]);
        $tag_name = $regex[2];
        $tag = strtolower($tag_name);
        $is_single_tag = in_array($tag, $single_tags, true);
        $pre_attribute_ws = isset($regex[4]) ? $regex[4] : '';
        $attributes = trim(isset($regex[5]) ? $regex[5] : $regex[3]);
        $has_self_closer = str_ends_with($attributes, '/');
        $newtext .= $tagqueue;
        $i = strpos($text, $full_match);
        $l = strlen($full_match);
        // Clear the shifter.
        $tagqueue = '';
        if ($has_leading_slash) {
            // End tag.
            // If too many closing tags.
            if ($stacksize <= 0) {
                $tag = '';
                // Or close to be safe $tag = '/' . $tag.
                // If stacktop value = tag close value, then pop.
            } elseif ($tagstack[$stacksize - 1] === $tag) {
                // Found closing tag.
                $tag = '</' . $tag . '>';
                // Close tag.
                array_pop($tagstack);
                $stacksize--;
            } else {
                // Closing tag not at top, search for it.
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] === $tag) {
                        // Add tag to tagqueue.
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin tag.
            if ($has_self_closer) {
                /*
                 * If it presents itself as a self-closing tag, but it isn't a known single-entity self-closing tag,
                 * then don't let it be treated as such and immediately close it with a closing tag.
                 * The tag will encapsulate no text as a result.
                 */
                if (!$is_single_tag) {
                    $attributes = trim(substr($attributes, 0, -1)) . "></{$tag}";
                }
            } elseif ($is_single_tag) {
                // Else if it's a known single-entity tag but it doesn't close itself, do so.
                $pre_attribute_ws = ' ';
                $attributes .= '/';
            } else {
                /*
                 * It's not a single-entity tag.
                 * If the top of the stack is the same as the tag we want to push, close previous tag.
                 */
                if ($stacksize > 0 && !in_array($tag, $nestable_tags, true) && $tagstack[$stacksize - 1] === $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes.
            if ($has_self_closer && $is_single_tag) {
                // We need some space - avoid <br/> and prefer <br />.
                $pre_attribute_ws = ' ';
            }
            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
            // If already queuing a close tag, then put this tag on too.
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear tag queue.
    $newtext .= $tagqueue;
    // Add remaining text.
    $newtext .= $text;
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close.
    }
    // WP fix for the bug with HTML comments.
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 6.1

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 * @since 5.3.0 Improve accuracy and add support for custom element tags.
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags.
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source', 'track', 'wbr');
    // Tags that can be immediately nested within themselves.
    $nestable_tags = array('article', 'aside', 'blockquote', 'details', 'div', 'figure', 'object', 'q', 'section', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'.
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number).
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    /**
     * Matches supported tags.
     *
     * To get the pattern as a string without the comments paste into a PHP
     * REPL like `php -a`.
     *
     * @see https://html.spec.whatwg.org/#elements-2
     * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
     *
     * @example
     * ~# php -a
     * php > $s = [paste copied contents of expression below including parentheses];
     * php > echo $s;
     */
    $tag_pattern = '#<' . '(/?)' . '(' . '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' . '|' . '(?:[\w:]+)' . ')' . '(?:' . '\s*' . '(/?)' . '|' . '(\s+)' . '([^>]*)' . ')' . '>#';
    while (preg_match($tag_pattern, $text, $regex)) {
        $full_match = $regex[0];
        $has_leading_slash = !empty($regex[1]);
        $tag_name = $regex[2];
        $tag = strtolower($tag_name);
        $is_single_tag = in_array($tag, $single_tags, true);
        $pre_attribute_ws = isset($regex[4]) ? $regex[4] : '';
        $attributes = trim(isset($regex[5]) ? $regex[5] : $regex[3]);
        $has_self_closer = '/' === substr($attributes, -1);
        $newtext .= $tagqueue;
        $i = strpos($text, $full_match);
        $l = strlen($full_match);
        // Clear the shifter.
        $tagqueue = '';
        if ($has_leading_slash) {
            // End tag.
            // If too many closing tags.
            if ($stacksize <= 0) {
                $tag = '';
                // Or close to be safe $tag = '/' . $tag.
                // If stacktop value = tag close value, then pop.
            } elseif ($tagstack[$stacksize - 1] === $tag) {
                // Found closing tag.
                $tag = '</' . $tag . '>';
                // Close tag.
                array_pop($tagstack);
                $stacksize--;
            } else {
                // Closing tag not at top, search for it.
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] === $tag) {
                        // Add tag to tagqueue.
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin tag.
            if ($has_self_closer) {
                // If it presents itself as a self-closing tag...
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such
                // and immediately close it with a closing tag (the tag will encapsulate no text as a result).
                if (!$is_single_tag) {
                    $attributes = trim(substr($attributes, 0, -1)) . "></{$tag}";
                }
            } elseif ($is_single_tag) {
                // Else if it's a known single-entity tag but it doesn't close itself, do so.
                $pre_attribute_ws = ' ';
                $attributes .= '/';
            } else {
                // It's not a single-entity tag.
                // If the top of the stack is the same as the tag we want to push, close previous tag.
                if ($stacksize > 0 && !in_array($tag, $nestable_tags, true) && $tagstack[$stacksize - 1] === $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes.
            if ($has_self_closer && $is_single_tag) {
                // We need some space - avoid <br/> and prefer <br />.
                $pre_attribute_ws = ' ';
            }
            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
            // If already queuing a close tag, then put this tag on too.
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear tag queue.
    $newtext .= $tagqueue;
    // Add remaining text.
    $newtext .= $text;
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close.
    }
    // WP fix for the bug with HTML comments.
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 5.9

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 * @since 5.3.0 Improve accuracy and add support for custom element tags.
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags.
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source', 'track', 'wbr');
    // Tags that can be immediately nested within themselves.
    $nestable_tags = array('article', 'aside', 'blockquote', 'details', 'div', 'figure', 'object', 'q', 'section', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'.
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number).
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    /**
     * Matches supported tags.
     *
     * To get the pattern as a string without the comments paste into a PHP
     * REPL like `php -a`.
     *
     * @see https://html.spec.whatwg.org/#elements-2
     * @see https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name
     *
     * @example
     * ~# php -a
     * php > $s = [paste copied contents of expression below including parentheses];
     * php > echo $s;
     */
    $tag_pattern = '#<' . '(/?)' . '(' . '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' . '|' . '(?:[\w:]+)' . ')' . '(?:' . '\s*' . '(/?)' . '|' . '(\s+)' . '([^>]*)' . ')' . '>#';
    while (preg_match($tag_pattern, $text, $regex)) {
        $full_match = $regex[0];
        $has_leading_slash = !empty($regex[1]);
        $tag_name = $regex[2];
        $tag = strtolower($tag_name);
        $is_single_tag = in_array($tag, $single_tags, true);
        $pre_attribute_ws = isset($regex[4]) ? $regex[4] : '';
        $attributes = trim(isset($regex[5]) ? $regex[5] : $regex[3]);
        $has_self_closer = '/' === substr($attributes, -1);
        $newtext .= $tagqueue;
        $i = strpos($text, $full_match);
        $l = strlen($full_match);
        // Clear the shifter.
        $tagqueue = '';
        if ($has_leading_slash) {
            // End tag.
            // If too many closing tags.
            if ($stacksize <= 0) {
                $tag = '';
                // Or close to be safe $tag = '/' . $tag.
                // If stacktop value = tag close value, then pop.
            } elseif ($tagstack[$stacksize - 1] === $tag) {
                // Found closing tag.
                $tag = '</' . $tag . '>';
                // Close tag.
                array_pop($tagstack);
                $stacksize--;
            } else {
                // Closing tag not at top, search for it.
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] === $tag) {
                        // Add tag to tagqueue.
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin tag.
            if ($has_self_closer) {
                // If it presents itself as a self-closing tag...
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such
                // and immediately close it with a closing tag (the tag will encapsulate no text as a result).
                if (!$is_single_tag) {
                    $attributes = trim(substr($attributes, 0, -1)) . "></{$tag}";
                }
            } elseif ($is_single_tag) {
                // Else if it's a known single-entity tag but it doesn't close itself, do so.
                $pre_attribute_ws = ' ';
                $attributes .= '/';
            } else {
                // It's not a single-entity tag.
                // If the top of the stack is the same as the tag we want to push, close previous tag.
                if ($stacksize > 0 && !in_array($tag, $nestable_tags, true) && $tagstack[$stacksize - 1] === $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes.
            if ($has_self_closer && $is_single_tag) {
                // We need some space - avoid <br/> and prefer <br />.
                $pre_attribute_ws = ' ';
            }
            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
            // If already queuing a close tag, then put this tag on too.
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear tag queue.
    $newtext .= $tagqueue;
    // Add remaining text.
    $newtext .= $text;
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close.
    }
    // WP fix for the bug with HTML comments.
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 5.4

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 * @since 5.3.0 Improve accuracy and add support for custom element tags.
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags.
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source');
    // Tags that can be immediately nested within themselves.
    $nestable_tags = array('blockquote', 'div', 'object', 'q', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'.
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number).
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    /**
     * Matches supported tags.
     *
     * To get the pattern as a string without the comments paste into a PHP
     * REPL like `php -a`.
     *
     * @see https://html.spec.whatwg.org/#elements-2
     * @see https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name
     *
     * @example
     * ~# php -a
     * php > $s = [paste copied contents of expression below including parentheses];
     * php > echo $s;
     */
    $tag_pattern = '#<' . '(/?)' . '(' . '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' . '|' . '(?:[\w:]+)' . ')' . '(?:' . '\s*' . '(/?)' . '|' . '(\s+)' . '([^>]*)' . ')' . '>#';
    while (preg_match($tag_pattern, $text, $regex)) {
        $full_match = $regex[0];
        $has_leading_slash = !empty($regex[1]);
        $tag_name = $regex[2];
        $tag = strtolower($tag_name);
        $is_single_tag = in_array($tag, $single_tags, true);
        $pre_attribute_ws = isset($regex[4]) ? $regex[4] : '';
        $attributes = trim(isset($regex[5]) ? $regex[5] : $regex[3]);
        $has_self_closer = '/' === substr($attributes, -1);
        $newtext .= $tagqueue;
        $i = strpos($text, $full_match);
        $l = strlen($full_match);
        // Clear the shifter.
        $tagqueue = '';
        if ($has_leading_slash) {
            // End tag.
            // If too many closing tags.
            if ($stacksize <= 0) {
                $tag = '';
                // Or close to be safe $tag = '/' . $tag.
                // If stacktop value = tag close value, then pop.
            } elseif ($tagstack[$stacksize - 1] === $tag) {
                // Found closing tag.
                $tag = '</' . $tag . '>';
                // Close tag.
                array_pop($tagstack);
                $stacksize--;
            } else {
                // Closing tag not at top, search for it.
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] === $tag) {
                        // Add tag to tagqueue.
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin tag.
            if ($has_self_closer) {
                // If it presents itself as a self-closing tag...
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such
                // and immediately close it with a closing tag (the tag will encapsulate no text as a result).
                if (!$is_single_tag) {
                    $attributes = trim(substr($attributes, 0, -1)) . "></{$tag}";
                }
            } elseif ($is_single_tag) {
                // Else if it's a known single-entity tag but it doesn't close itself, do so.
                $pre_attribute_ws = ' ';
                $attributes .= '/';
            } else {
                // It's not a single-entity tag.
                // If the top of the stack is the same as the tag we want to push, close previous tag.
                if ($stacksize > 0 && !in_array($tag, $nestable_tags, true) && $tagstack[$stacksize - 1] === $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes.
            if ($has_self_closer && $is_single_tag) {
                // We need some space - avoid <br/> and prefer <br />.
                $pre_attribute_ws = ' ';
            }
            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
            // If already queuing a close tag, then put this tag on too.
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear tag queue.
    $newtext .= $tagqueue;
    // Add remaining text.
    $newtext .= $text;
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close.
    }
    // WP fix for the bug with HTML comments.
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 5.3

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 * @since 5.3.0 Improve accuracy and add support for custom element tags.
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source');
    // Tags that can be immediately nested within themselves
    $nestable_tags = array('blockquote', 'div', 'object', 'q', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number)
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    /**
     * Matches supported tags.
     *
     * To get the pattern as a string without the comments paste into a PHP
     * REPL like `php -a`.
     *
     * @see https://html.spec.whatwg.org/#elements-2
     * @see https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name
     *
     * @example
     * ~# php -a
     * php > $s = [paste copied contents of expression below including parentheses];
     * php > echo $s;
     */
    $tag_pattern = '#<' . '(/?)' . '(' . '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' . '|' . '(?:[\w:]+)' . ')' . '(?:' . '\s*' . '(/?)' . '|' . '(\s+)' . '([^>]*)' . ')' . '>#';
    while (preg_match($tag_pattern, $text, $regex)) {
        $full_match = $regex[0];
        $has_leading_slash = !empty($regex[1]);
        $tag_name = $regex[2];
        $tag = strtolower($tag_name);
        $is_single_tag = in_array($tag, $single_tags, true);
        $pre_attribute_ws = isset($regex[4]) ? $regex[4] : '';
        $attributes = trim(isset($regex[5]) ? $regex[5] : $regex[3]);
        $has_self_closer = '/' === substr($attributes, -1);
        $newtext .= $tagqueue;
        $i = strpos($text, $full_match);
        $l = strlen($full_match);
        // Clear the shifter.
        $tagqueue = '';
        if ($has_leading_slash) {
            // End Tag.
            // If too many closing tags.
            if ($stacksize <= 0) {
                $tag = '';
                // Or close to be safe $tag = '/' . $tag.
                // If stacktop value = tag close value, then pop.
            } elseif ($tagstack[$stacksize - 1] === $tag) {
                // Found closing tag.
                $tag = '</' . $tag . '>';
                // Close Tag.
                array_pop($tagstack);
                $stacksize--;
            } else {
                // Closing tag not at top, search for it.
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] === $tag) {
                        // Add tag to tagqueue.
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin Tag.
            if ($has_self_closer) {
                // If it presents itself as a self-closing tag...
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such and
                // immediately close it with a closing tag (the tag will encapsulate no text as a result)
                if (!$is_single_tag) {
                    $attributes = trim(substr($attributes, 0, -1)) . "></{$tag}";
                }
            } elseif ($is_single_tag) {
                // ElseIf it's a known single-entity tag but it doesn't close itself, do so
                $pre_attribute_ws = ' ';
                $attributes .= '/';
            } else {
                // It's not a single-entity tag.
                // If the top of the stack is the same as the tag we want to push, close previous tag.
                if ($stacksize > 0 && !in_array($tag, $nestable_tags, true) && $tagstack[$stacksize - 1] === $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes.
            if ($has_self_closer && $is_single_tag) {
                // We need some space - avoid <br/> and prefer <br />.
                $pre_attribute_ws = ' ';
            }
            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
            // If already queuing a close tag, then put this tag on too.
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear Tag Queue.
    $newtext .= $tagqueue;
    // Add remaining text.
    $newtext .= $text;
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close.
    }
    // WP fix for the bug with HTML comments.
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 5.1

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *      1.1  Fixed handling of append/stack pop order of end text
 *           Added Cleaning Hooks
 *      1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source');
    // Tags that can be immediately nested within themselves
    $nestable_tags = array('blockquote', 'div', 'object', 'q', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number)
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    while (preg_match('/<(\/?[\w:]*)\s*([^>]*)>/', $text, $regex)) {
        $newtext .= $tagqueue;
        $i = strpos($text, $regex[0]);
        $l = strlen($regex[0]);
        // clear the shifter
        $tagqueue = '';
        // Pop or Push
        if (isset($regex[1][0]) && '/' == $regex[1][0]) {
            // End Tag
            $tag = strtolower(substr($regex[1], 1));
            // if too many closing tags
            if ($stacksize <= 0) {
                $tag = '';
                // or close to be safe $tag = '/' . $tag;
                // if stacktop value = tag close value then pop
            } elseif ($tagstack[$stacksize - 1] == $tag) {
                // found closing tag
                $tag = '</' . $tag . '>';
                // Close Tag
                // Pop
                array_pop($tagstack);
                $stacksize--;
            } else {
                // closing tag not at top, search for it
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] == $tag) {
                        // add tag to tagqueue
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin Tag
            $tag = strtolower($regex[1]);
            // Tag Cleaning
            // If it's an empty tag "< >", do nothing
            if ('' == $tag) {
                // do nothing
            } elseif (substr($regex[2], -1) == '/') {
                // ElseIf it presents itself as a self-closing tag...
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such and
                // immediately close it with a closing tag (the tag will encapsulate no text as a result)
                if (!in_array($tag, $single_tags)) {
                    $regex[2] = trim(substr($regex[2], 0, -1)) . "></{$tag}";
                }
            } elseif (in_array($tag, $single_tags)) {
                // ElseIf it's a known single-entity tag but it doesn't close itself, do so
                $regex[2] .= '/';
            } else {
                // Else it's not a single-entity tag
                // If the top of the stack is the same as the tag we want to push, close previous tag
                if ($stacksize > 0 && !in_array($tag, $nestable_tags) && $tagstack[$stacksize - 1] == $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes
            $attributes = $regex[2];
            if (!empty($attributes) && $attributes[0] != '>') {
                $attributes = ' ' . $attributes;
            }
            $tag = '<' . $tag . $attributes . '>';
            //If already queuing a close tag, then put this tag on, too
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear Tag Queue
    $newtext .= $tagqueue;
    // Add Remaining text
    $newtext .= $text;
    // Empty Stack
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
        // Add remaining tags to close
    }
    // WP fix for the bug with HTML comments
    $newtext = str_replace('< !--', '<!--', $newtext);
    $newtext = str_replace('<    !--', '< !--', $newtext);
    return $newtext;
}

WordPress Version: 4.2

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *		1.1  Fixed handling of append/stack pop order of end text
 *			 Added Cleaning Hooks
 *		1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source');
    // Tags that can be immediately nested within themselves
    $nestable_tags = array('blockquote', 'div', 'object', 'q', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number)
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    while (preg_match("/<(\\/?[\\w:]*)\\s*([^>]*)>/", $text, $regex)) {
        $newtext .= $tagqueue;
        $i = strpos($text, $regex[0]);
        $l = strlen($regex[0]);
        // clear the shifter
        $tagqueue = '';
        // Pop or Push
        if (isset($regex[1][0]) && '/' == $regex[1][0]) {
            // End Tag
            $tag = strtolower(substr($regex[1], 1));
            // if too many closing tags
            if ($stacksize <= 0) {
                $tag = '';
                // or close to be safe $tag = '/' . $tag;
            } elseif ($tagstack[$stacksize - 1] == $tag) {
                // found closing tag
                $tag = '</' . $tag . '>';
                // Close Tag
                // Pop
                array_pop($tagstack);
                $stacksize--;
            } else {
                // closing tag not at top, search for it
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] == $tag) {
                        // add tag to tagqueue
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin Tag
            $tag = strtolower($regex[1]);
            // Tag Cleaning
            // If it's an empty tag "< >", do nothing
            if ('' == $tag) {
                // do nothing
            } elseif (substr($regex[2], -1) == '/') {
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such and
                // immediately close it with a closing tag (the tag will encapsulate no text as a result)
                if (!in_array($tag, $single_tags)) {
                    $regex[2] = trim(substr($regex[2], 0, -1)) . "></{$tag}";
                }
            } elseif (in_array($tag, $single_tags)) {
                $regex[2] .= '/';
            } else {
                // If the top of the stack is the same as the tag we want to push, close previous tag
                if ($stacksize > 0 && !in_array($tag, $nestable_tags) && $tagstack[$stacksize - 1] == $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes
            $attributes = $regex[2];
            if (!empty($attributes) && $attributes[0] != '>') {
                $attributes = ' ' . $attributes;
            }
            $tag = '<' . $tag . $attributes . '>';
            //If already queuing a close tag, then put this tag on, too
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear Tag Queue
    $newtext .= $tagqueue;
    // Add Remaining text
    $newtext .= $text;
    // Empty Stack
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
    }
    // Add remaining tags to close
    // WP fix for the bug with HTML comments
    $newtext = str_replace("< !--", "<!--", $newtext);
    $newtext = str_replace("<    !--", "< !--", $newtext);
    return $newtext;
}

WordPress Version: 3.7

/**
 * Balances tags of string using a modified stack.
 *
 * @since 2.0.4
 *
 * @author Leonard Lin <leonard@acm.org>
 * @license GPL
 * @copyright November 4, 2001
 * @version 1.1
 * @todo Make better - change loop condition to $text in 1.2
 * @internal Modified by Scott Reilly (coffee2code) 02 Aug 2004
 *		1.1  Fixed handling of append/stack pop order of end text
 *			 Added Cleaning Hooks
 *		1.0  First Version
 *
 * @param string $text Text to be balanced.
 * @return string Balanced text.
 */
function force_balance_tags($text)
{
    $tagstack = array();
    $stacksize = 0;
    $tagqueue = '';
    $newtext = '';
    // Known single-entity/self-closing tags
    $single_tags = array('area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source');
    // Tags that can be immediately nested within themselves
    $nestable_tags = array('blockquote', 'div', 'object', 'q', 'span');
    // WP bug fix for comments - in case you REALLY meant to type '< !--'
    $text = str_replace('< !--', '<    !--', $text);
    // WP bug fix for LOVE <3 (and other situations with '<' before a number)
    $text = preg_replace('#<([0-9]{1})#', '&lt;$1', $text);
    while (preg_match("/<(\\/?[\\w:]*)\\s*([^>]*)>/", $text, $regex)) {
        $newtext .= $tagqueue;
        $i = strpos($text, $regex[0]);
        $l = strlen($regex[0]);
        // clear the shifter
        $tagqueue = '';
        // Pop or Push
        if (isset($regex[1][0]) && '/' == $regex[1][0]) {
            // End Tag
            $tag = strtolower(substr($regex[1], 1));
            // if too many closing tags
            if ($stacksize <= 0) {
                $tag = '';
                // or close to be safe $tag = '/' . $tag;
            } else if ($tagstack[$stacksize - 1] == $tag) {
                // found closing tag
                $tag = '</' . $tag . '>';
                // Close Tag
                // Pop
                array_pop($tagstack);
                $stacksize--;
            } else {
                // closing tag not at top, search for it
                for ($j = $stacksize - 1; $j >= 0; $j--) {
                    if ($tagstack[$j] == $tag) {
                        // add tag to tagqueue
                        for ($k = $stacksize - 1; $k >= $j; $k--) {
                            $tagqueue .= '</' . array_pop($tagstack) . '>';
                            $stacksize--;
                        }
                        break;
                    }
                }
                $tag = '';
            }
        } else {
            // Begin Tag
            $tag = strtolower($regex[1]);
            // Tag Cleaning
            // If it's an empty tag "< >", do nothing
            if ('' == $tag) {
                // do nothing
            } elseif (substr($regex[2], -1) == '/') {
                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such and
                // immediately close it with a closing tag (the tag will encapsulate no text as a result)
                if (!in_array($tag, $single_tags)) {
                    $regex[2] = trim(substr($regex[2], 0, -1)) . "></{$tag}";
                }
            } elseif (in_array($tag, $single_tags)) {
                $regex[2] .= '/';
            } else {
                // If the top of the stack is the same as the tag we want to push, close previous tag
                if ($stacksize > 0 && !in_array($tag, $nestable_tags) && $tagstack[$stacksize - 1] == $tag) {
                    $tagqueue = '</' . array_pop($tagstack) . '>';
                    $stacksize--;
                }
                $stacksize = array_push($tagstack, $tag);
            }
            // Attributes
            $attributes = $regex[2];
            if (!empty($attributes) && $attributes[0] != '>') {
                $attributes = ' ' . $attributes;
            }
            $tag = '<' . $tag . $attributes . '>';
            //If already queuing a close tag, then put this tag on, too
            if (!empty($tagqueue)) {
                $tagqueue .= $tag;
                $tag = '';
            }
        }
        $newtext .= substr($text, 0, $i) . $tag;
        $text = substr($text, $i + $l);
    }
    // Clear Tag Queue
    $newtext .= $tagqueue;
    // Add Remaining text
    $newtext .= $text;
    // Empty Stack
    while ($x = array_pop($tagstack)) {
        $newtext .= '</' . $x . '>';
    }
    // Add remaining tags to close
    // WP fix for the bug with HTML comments
    $newtext = str_replace("< !--", "<!--", $newtext);
    $newtext = str_replace("<    !--", "< !--", $newtext);
    return $newtext;
}