<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    core_webstandards
 */

/*
The webstandards checking is designed for a special blend between uber-modern-standards and cross-browser stability - to only allow XHTML5 and CSS3 that runs (or gracefully degrades) on IE8.

We favour the W3C standard over the WHATWG living document.

We continue to prohibit much of what was deprecated in XHTML but brought back into HTML5 (e.g. 'b' tag).
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__webstandards()
{
    if (!defined('DOCTYPE_HTML')) {
        // These are old doctypes we'll recognise for gracefulness, but we don't accept them as valid
        define('DOCTYPE_HTML', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">');
        define('DOCTYPE_HTML_STRICT', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">');
        define('DOCTYPE_XHTML', '<!DOCTYPE html>');
        define('DOCTYPE_XHTML_STRICT', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">');
        define('DOCTYPE_XHTML_11', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">');

        // (X)HTML5, the future
        define('DOCTYPE_XHTML5', '<!DOCTYPE html>');
    }

    global $WEBSTANDARDS_CHECKER_OFF, $WELL_FORMED_ONLY, $WEBSTANDARDS_JAVASCRIPT, $WEBSTANDARDS_CSS, $WEBSTANDARDS_WCAG, $WEBSTANDARDS_COMPAT, $WEBSTANDARDS_EXT_FILES, $WEBSTANDARDS_MANUAL;
    $WEBSTANDARDS_JAVASCRIPT = true;
    $WEBSTANDARDS_CSS = true;
    $WEBSTANDARDS_WCAG = true;
    $WEBSTANDARDS_COMPAT = true;
    $WEBSTANDARDS_EXT_FILES = true;
    $WEBSTANDARDS_MANUAL = false;

    global $EXTRA_CHECK;
    $EXTRA_CHECK = array();

    global $VALIDATED_ALREADY;
    $VALIDATED_ALREADY = array();

    global $NO_XHTML_LINK_FOLLOW;
    $NO_XHTML_LINK_FOLLOW = false;

    global $CSS_TAG_RANGES, $CSS_VALUE_RANGES;
    $CSS_TAG_RANGES = array();
    $CSS_VALUE_RANGES = array();

    global $ENTITIES;
    $ENTITIES = array(
        'quot' => true, 'amp' => true, 'lt' => true, 'gt' => true, 'nbsp' => true, 'iexcl' => true, 'cent' => true,
        'pound' => true, 'curren' => true, 'yen' => true, 'brvbar' => true, 'sect' => true, 'uml' => true,
        'copy' => true, 'ordf' => true, 'laquo' => true, 'not' => true, 'shy' => true, 'reg' => true, 'macr' => true,
        'deg' => true, 'plusmn' => true, 'sup2' => true, 'sup3' => true, 'acute' => true, 'micro' => true,
        'para' => true, 'middot' => true, 'cedil' => true, 'sup1' => true, 'ordm' => true, 'raquo' => true,
        'frac14' => true, 'frac12' => true, 'frac34' => true, 'iquest' => true,
        'Agrave' => true, 'Aacute' => true, 'Acirc' => true, 'Atilde' => true, 'Auml' => true, 'Aring' => true,
        'AElig' => true, 'Ccedil' => true, 'Egrave' => true, 'Eacute' => true,
        'Ecirc' => true, 'Euml' => true, 'Igrave' => true, 'Iacute' => true, 'Icirc' => true, 'Iuml' => true,
        'ETH' => true, 'Ntilde' => true, 'Ograve' => true, 'Oacute' => true, 'Ocirc' => true,
        'Otilde' => true, 'Ouml' => true, 'times' => true, 'Oslash' => true, 'Ugrave' => true, 'Uacute' => true,
        'Ucirc' => true, 'Uuml' => true, 'Yacute' => true, 'THORN' => true,
        'szlig' => true, 'agrave' => true, 'aacute' => true, 'acirc' => true, 'atilde' => true, 'auml' => true,
        'aring' => true, 'aelig' => true, 'ccedil' => true, 'egrave' => true,
        'eacute' => true, 'ecirc' => true, 'euml' => true, 'igrave' => true, 'iacute' => true, 'icirc' => true,
        'iuml' => true, 'eth' => true, 'ntilde' => true, 'ograve' => true, 'oacute' => true,
        'ocirc' => true, 'otilde' => true, 'ouml' => true, 'divide' => true, 'oslash' => true, 'ugrave' => true,
        'uacute' => true, 'ucirc' => true, 'uuml' => true, 'yacute' => true,
        'thorn' => true, 'yuml' => true, 'fnof' => true, 'Alpha' => true, 'Beta' => true, 'Gamma' => true,
        'Delta' => true, 'Epsilon' => true, 'Zeta' => true, 'Eta' => true, 'Theta' => true, 'Iota' => true,
        'Kappa' => true, 'Lambda' => true, 'Mu' => true, 'Nu' => true, 'Xi' => true, 'Omicron' => true, 'Pi' => true,
        'Rho' => true, 'Sigma' => true, 'Tau' => true, 'Upsilon' => true, 'Phi' => true, 'Chi' => true,
        'Psi' => true, 'Omega' => true, 'alpha' => true, 'beta' => true, 'gamma' => true, 'delta' => true,
        'epsilon' => true, 'zeta' => true, 'eta' => true, 'theta' => true, 'iota' => true, 'kappa' => true,
        'lambda' => true, 'mu' => true, 'nu' => true, 'xi' => true, 'omicron' => true, 'pi' => true, 'rho' => true,
        'sigmaf' => true, 'sigma' => true, 'tau' => true, 'upsilon' => true, 'phi' => true, 'chi' => true,
        'psi' => true, 'omega' => true, 'thetasym' => true, 'upsih' => true, 'piv' => true, 'bull' => true,
        'hellip' => true, 'prime' => true, 'Prime' => true, 'oline' => true, 'frasl' => true,
        'weierp' => true, 'image' => true, 'real' => true, 'trade' => true, 'alefsym' => true, 'larr' => true,
        'uarr' => true, 'rarr' => true, 'darr' => true, 'harr' => true, 'crarr' => true,
        'lArr' => true, 'uArr' => true, 'rArr' => true, 'dArr' => true, 'hArr' => true, 'forall' => true,
        'part' => true, 'exist' => true, 'empty' => true, 'nabla' => true, 'isin' => true, 'notin' => true,
        'ni' => true, 'prod' => true, 'sum' => true, 'minus' => true, 'lowast' => true, 'radic' => true, 'prop' => true,
        'infin' => true, 'ang' => true, 'and' => true, 'or' => true, 'cap' => true, 'cup' => true, 'int' => true,
        'there4' => true, 'sim' => true, 'cong' => true, 'asymp' => true, 'ne' => true, 'equiv' => true, 'le' => true,
        'ge' => true, 'sub' => true, 'sup' => true, 'nsub' => true, 'sube' => true, 'supe' => true,
        'oplus' => true, 'otimes' => true, 'perp' => true, 'sdot' => true, 'lceil' => true, 'rceil' => true,
        'lfloor' => true, 'rfloor' => true, 'lang' => true, 'rang' => true, 'loz' => true,
        'spades' => true, 'clubs' => true, 'hearts' => true, 'diams' => true, 'OElig' => true, 'oelig' => true,
        'Scaron' => true, 'scaron' => true, 'Yuml' => true, 'circ' => true, 'tidle' => true,
        'ensp' => true, 'emsp' => true, 'thinsp' => true, 'zwnj' => true, 'zwj' => true, 'lrm' => true, 'rlm' => true,
        'ndash' => true, 'mdash' => true, 'lsquo' => true, 'rsquo' => true, 'sbquo' => true,
        'ldquo' => true, 'rdquo' => true, 'bdquo' => true, 'dagger' => true, 'Dagger' => true, 'permil' => true,
        'lsaquo' => true, 'rsaquo' => true, 'euro' => true);

    $strict_form_accessibility = false; // Form fields may not be empty with this strict rule

    global $NEVER_SELFCLOSE_TAGS;
    $NEVER_SELFCLOSE_TAGS = array(
        'div' => true,
        'h1' => true,
        'h2' => true,
        'h3' => true,
        'h4' => true,
        'h5' => true,
        'h6' => true,
        'p' => true,
        'blockquote' => true,
        'pre' => true,
        'fieldset' => true,
        'figure' => true,
        'address' => true,
        'iframe' => true,
        'noscript' => true,
        'table' => true,
        'tbody' => true,
        'td' => true,
        'tfoot' => true,
        'th' => true,
        'thead' => true,
        'tr' => true,
        'dd' => true,
        'dt' => true,
        'dl' => true,
        'li' => true,
        'ol' => true,
        'ul' => true,
        'rbc' => true,
        'rtc' => true,
        'rb' => true,
        'rt' => true,
        'rp' => true,
        'video' => true,
        'details' => true,
        'summary' => true,
        'section' => true,
        'nav' => true,
        'header' => true,
        'footer' => true,
        'canvas' => true,
        'audio' => true,
        'aside' => true,
        'article' => true,
        'span' => true,
        'abbr' => true,
        'cite' => true,
        'code' => true,
        'dfn' => true,
        'em' => true,
        'strong' => true,
        'kbd' => true,
        'q' => true,
        'samp' => true,
        'var' => true,
        'sub' => true,
        'sup' => true,
        'del' => true,
        'ruby' => true,
        'a' => true,
        'bdo' => true,
        'ins' => true,
        'textarea' => true,
        'select' => true,
        'object' => true,
        'caption' => true,
        'label' => true,
        'time' => true,
        'progress' => true,
        'output' => true,
        'meter' => true,
        'mark' => true,
        'datalist' => true,
        'body' => true,
        'colgroup' => true,
        'head' => true,
        'html' => true,
        'map' => true,
        'optgroup' => true,
        'option' => true,
        'style' => true,
        'title' => true,
        'legend' => true,
        'figcaption' => true,
        'script' => true,
        'form' => true,
        'dir' => true,
        'menu' => true,
        'center' => true,
        'applet' => true,
        'font' => true,
        's' => true,
        'strike' => true,
        'u' => true,
    );

    global $POSSIBLY_EMPTY_TAGS;
    $POSSIBLY_EMPTY_TAGS = array(
        'a' => true, // When it's an anchor only - we will detect this with custom code
        'div' => true,
        'span' => true,
        'td' => true,
        'th' => true, // Only use for 'corner' ones
        'textarea' => true,
        'button' => true,
        'script' => true, // If we have one of these as self-closing in IE... it kills it!
        'noscript' => true,
        'li' => true,
        'embed' => true,
    );
    if ($strict_form_accessibility) {
        unset($POSSIBLY_EMPTY_TAGS['textarea']);
    }

    global $MUST_SELFCLOSE_TAGS;
    $MUST_SELFCLOSE_TAGS = array(
        'img' => true,
        'hr' => true,
        'br' => true,
        'param' => true,
        'input' => true,
        'base' => true,
        'link' => true,
        'meta' => true,
        'area' => true,
        'col' => true,
        'source' => true,
        'nobr' => true,
    );

    // B's may not appear under A
    global $PROHIBITIONS;
    $PROHIBITIONS = array(
        'a' => array('a'),
        'button' => array('input', 'select', 'textarea', 'label', 'button', 'form', 'fieldset', 'iframe'),
        'p' => array('p', 'table', 'div', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'hr'),
        'form' => array('form'),
        'th' => array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'),
        'em' => array('em'),
        'abbr' => array('abbr'),
        'strong' => array('strong'),
        'label' => array('label', 'div'));

    // Only B's can be under A
    global $ONLY_CHILDREN;
    $ONLY_CHILDREN = array(
        'ruby' => array('rbc', 'rtc', 'rp'),
        'tr' => array('td', 'th'),
        'thead' => array('tr'),
        'tbody' => array('tr'),
        'tfoot' => array('tr'),
        'table' => array('tbody', 'thead', 'tfoot', 'colgroup', 'col', 'caption'),
        'colgroup' => array('col'),
        'select' => array('option', 'optgroup'),
        'legend' => array('ins', 'del'),
        //'map' => array('area'), Apparently no such rule (see w3.org)
        'html' => array('head', 'body'),
        'embed' => array('noembed'),
        'applet' => array('param'),
        'head' => array('meta', 'base', 'basefont', 'script', 'link', 'noscript', 'map', 'title', 'style'),
        'ul' => array('li'),
        'ol' => array('li'),
        'menu' => array('li'),
        'dl' => array('li', 'dt', 'dd'),
        'dir' => array('li'),
        'hr' => array(),
        'img' => array(),
        'input' => array(),
        'br' => array(),
        'meta' => array(),
        'base' => array(),
        'title' => array(),
        'textarea' => array(),
        'style' => array(),
        'pre' => array(),
        'script' => array(),
        'param' => array(),
        /*'option' => array(),*/
        'area' => array(),
        'link' => array('link'),
        'basefont' => array(),
        'col' => array()
    );
    $ONLY_CHILDREN += array(
        'details' => array('summary'),
        'datalist' => array('option'),
    );

    // A can only occur underneath B's
    global $ONLY_PARENT;
    $ONLY_PARENT = array(
        'rb' => array('rbc'),
        'rt' => array('rtc'),
        'rbc' => array('ruby'),
        'rtc' => array('ruby'),
        'rp' => array('ruby'),
        'area' => array('map'),
        'base' => array('head'),
        'body' => array('html'),
        'head' => array('html'),
        'param' => array('script', 'object'),
        //'link' => array('head', 'link'),  Composr will dynamically optimise things to tend towards correctness, so can't enable this rule
        //'style' => array('head'), "
        'li' => array('ul', 'ol', 'dd', 'menu', 'dt', 'dl', 'dir'),
        'tbody' => array('table'),
        'tfoot' => array('table'),
        'thead' => array('table'),
        'th' => array('tr'),
        'td' => array('tr'),
        'tr' => array('table', 'thead', 'tbody', 'tfoot'),
        'title' => array('head'),
        'caption' => array('table'),
        'col' => array('colgroup', 'table'),
        'colgroup' => array('table'),
        'option' => array('select', 'optgroup', 'datalist'),
        'noembed' => array('embed'),
    );
    $ONLY_PARENT += array(
        'figcaption' => array('figure'),
        'summary' => array('details'),
        'track' => array('audio', 'video'),
    );

    global $REQUIRE_ANCESTOR;
    $REQUIRE_ANCESTOR = array(
        //'textarea' => 'form',
        //'input' => 'form',
        //'button' => 'form',
        'option' => 'form',
        'optgroup' => 'form',
        'select' => 'form',
    );

    global $TEXT_NO_BLOCK;
    $TEXT_NO_BLOCK = array(
        'table' => true,
        'tr' => true,
        'tfoot' => true,
        'thead' => true,
        'ul' => true,
        'ol' => true,
        'dl' => true,
        'optgroup' => true,
        'select' => true,
        'colgroup' => true,
        'map' => true,
        'body' => true,
        'form' => true,
    );
    $TEXT_NO_BLOCK += array(
        'menu' => true,
    );

    if (!defined('IN_XML_TAG')) {
        define('IN_XML_TAG', -3);
        define('IN_DTD_TAG', -2);
        define('NO_MANS_LAND', -1);
        define('IN_COMMENT', 0);
        define('IN_TAG_NAME', 1);
        define('STARTING_TAG', 2);
        define('IN_TAG_BETWEEN_ATTRIBUTES', 3);
        define('IN_TAG_ATTRIBUTE_NAME', 4);
        define('IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_LEFT', 5);
        define('IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_RIGHT', 7);
        define('IN_TAG_ATTRIBUTE_VALUE_BIG_QUOTES', 10);
        define('IN_TAG_ATTRIBUTE_VALUE_NO_QUOTES', 12);
        define('IN_TAG_EMBEDDED_COMMENT', 9);
        define('IN_TAG_ATTRIBUTE_VALUE_LITTLE_QUOTES', 8);
        define('IN_CDATA', 11);
    }
}

/**
 * Check the specified XHTML, and return the results.
 *
 * @param  string $out The XHTML to check
 * @param  boolean $well_formed_only Whether to avoid checking for relational errors (false implies just a quick structural check, aka a 'well formed' check)
 * @param  boolean $is_fragment Whether what is being checked is an HTML fragment, rather than a whole document
 * @param  boolean $webstandards_javascript Validate javascript
 * @param  boolean $webstandards_css Validate CSS
 * @param  boolean $webstandards_wcag Validate WCAG
 * @param  boolean $webstandards_compat Validate for compatibility
 * @param  boolean $webstandards_ext_files Validate external files
 * @param  boolean $webstandards_manual Bring up messages about manual checks
 * @return array Parse information
 */
function check_xhtml($out, $well_formed_only = false, $is_fragment = false, $webstandards_javascript = true, $webstandards_css = true, $webstandards_wcag = true, $webstandards_compat = true, $webstandards_ext_files = true, $webstandards_manual = false)
{
    if (php_function_allowed('set_time_limit')) {
        @set_time_limit(100);
    }

    if (function_exists('disable_php_memory_limit')) {
        disable_php_memory_limit();
    }

    global $WEBSTANDARDS_CHECKER_OFF, $WELL_FORMED_ONLY, $WEBSTANDARDS_JAVASCRIPT, $WEBSTANDARDS_CSS, $WEBSTANDARDS_WCAG, $WEBSTANDARDS_COMPAT, $WEBSTANDARDS_EXT_FILES, $WEBSTANDARDS_MANUAL, $UNDER_XMLNS;
    if (function_exists('mixed')) {
        $WEBSTANDARDS_CHECKER_OFF = mixed();
    }
    $WEBSTANDARDS_CHECKER_OFF = null;
    $WELL_FORMED_ONLY = $well_formed_only;
    if (!$WELL_FORMED_ONLY) {
        if (function_exists('require_code')) {
            require_code('webstandards2');
        }
    }
    $WEBSTANDARDS_JAVASCRIPT = $webstandards_javascript;
    $WEBSTANDARDS_CSS = $webstandards_css;
    $WEBSTANDARDS_WCAG = $webstandards_wcag;
    $WEBSTANDARDS_COMPAT = $webstandards_compat;
    $WEBSTANDARDS_EXT_FILES = $webstandards_ext_files;
    $WEBSTANDARDS_MANUAL = $webstandards_manual;

    global $IDS_SO_FAR;
    $IDS_SO_FAR = array();

    $content_start_stack = array();

    global $BLOCK_CONSTRAIN, $XML_CONSTRAIN, $LAST_TAG_ATTRIBUTES, $FOUND_DOCTYPE, $FOUND_DESCRIPTION, $FOUND_KEYWORDS, $FOUND_CONTENTTYPE, $THE_DOCTYPE, $TAGS_DEPRECATE_ALLOW, $URL_BASE, $PARENT_TAG, $TABS_SEEN, $KEYS_SEEN, $ANCHORS_SEEN, $ATT_STACK, $TAG_STACK, $POS, $LINENO, $LINESTART, $OUT, $T_POS, $PROHIBITIONS, $ONLY_PARENT, $ONLY_CHILDREN, $REQUIRE_ANCESTOR, $LEN, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $POSSIBLY_EMPTY_TAGS, $MUST_SELFCLOSE_TAGS, $FOR_LABEL_IDS, $FOR_LABEL_IDS_2, $INPUT_TAG_IDS;
    global $TAG_RANGES, $VALUE_RANGES, $LAST_A_TAG, $A_LINKS, $XHTML_FORM_ENCODING;
    global $AREA_LINKS, $LAST_HEADING, $CRAWLED_URLS, $HYPERLINK_URLS, $EMBED_URLS, $THE_LANGUAGE, $PSPELL_LINK;
    global $TAGS_BLOCK, $TAGS_INLINE, $TAGS_NORMAL, $TAGS_BLOCK_DEPRECATED, $TAGS_INLINE_DEPRECATED, $TAGS_NORMAL_DEPRECATED, $NEVER_SELFCLOSE_TAGS;
    $PSPELL_LINK = null;
    $THE_LANGUAGE = 'en';
    $THE_DOCTYPE = $is_fragment ? DOCTYPE_XHTML : DOCTYPE_HTML;
    $TAGS_DEPRECATE_ALLOW = true;
    $XML_CONSTRAIN = $is_fragment;
    $BLOCK_CONSTRAIN = false;
    $LINENO = 0;
    $LINESTART = 0;
    $HYPERLINK_URLS = array();
    $EMBED_URLS = array();
    $AREA_LINKS = array();
    $LAST_HEADING = 0;
    $FOUND_DOCTYPE = false;
    $FOUND_CONTENTTYPE = false;
    $FOUND_KEYWORDS = false;
    $FOUND_DESCRIPTION = false;
    $CRAWLED_URLS = array();
    $PARENT_TAG = '';
    $XHTML_FORM_ENCODING = '';
    $UNDER_XMLNS = false;
    $KEYS_SEEN = array();
    $TABS_SEEN = array();
    $TAG_RANGES = array();
    $VALUE_RANGES = array();
    $LAST_A_TAG = null;
    $ANCHORS_SEEN = array();
    $FOR_LABEL_IDS = array();
    $FOR_LABEL_IDS_2 = array();
    $INPUT_TAG_IDS = array();
    $TAG_STACK = array();
    $ATT_STACK = array();
    $ANCESTOR_BLOCK = 0;
    $ANCESTOR_INLINE = 0;
    $POS = 0;
    $OUT = $out;
    unset($out);
    $LEN = strlen($OUT);
    $level_ranges = array();
    $stack_size = 0;
    $to_find = array('html' => true, 'head' => true, 'title' => true/*, 'meta' => true*/);
    $only_one_of_stack = array();
    $only_one_of_template = array('title' => 1, 'head' => 1, '1' => 1, 'base' => 1, 'thead' => 1, 'tfoot' => 1);
    $only_one_of = $only_one_of_template;
    $A_LINKS = array();
    $previous = '';
    if (!isset($GLOBALS['MAIL_MODE'])) {
        $GLOBALS['MAIL_MODE'] = false;
    }

    $errors = array();

    $bad_root = false;

    $token = _get_next_tag();
    while ($token !== null) {
        //echo $T_POS . '-' . $POS . ' (' . $stack_size . ')<br />';

        while ((is_array($token)) && (count($token) != 0)) { // Some kind of error in our token
            if ($WEBSTANDARDS_CHECKER_OFF === null) {
                foreach ($token[1] as $error) {
                    $errors[] = _xhtml_error($error[0], array_key_exists(1, $error) ? $error[1] : '', array_key_exists(2, $error) ? $error[2] : '', array_key_exists(3, $error) ? $error[3] : '', array_key_exists('raw', $error) ? $error['raw'] : false, array_key_exists('pos', $error) ? $error['pos'] : 0);
                }
                if ($token[0] === null) {
                    return array('level_ranges' => $level_ranges, 'tag_ranges' => $TAG_RANGES, 'value_ranges' => $VALUE_RANGES, 'errors' => $errors);
                }
            }
            $token = $token[0];
        }

        $basis_token = _get_tag_basis($token);

        // Open, close, or monitonic?
        $term = strpos($token, '/');
        if (!($WEBSTANDARDS_CHECKER_OFF === null)) {
            if ($term === false) {
                $WEBSTANDARDS_CHECKER_OFF++;
            } elseif ($term == 1) {
                if ($WEBSTANDARDS_CHECKER_OFF == 0) {
                    $WEBSTANDARDS_CHECKER_OFF = null;
                } else {
                    $WEBSTANDARDS_CHECKER_OFF--;
                }
            }
        }

        if ($term !== 1) {
            if (isset($only_one_of[$basis_token])) {
                if ($only_one_of[$basis_token] == 0) {
                    $errors[] = _xhtml_error('XHTML_ONLY_ONE_ALLOWED', $basis_token);
                }
                $only_one_of[$basis_token]--;
            }

            //echo 'Push $basis_token<br />';
            $level_ranges[] = array($stack_size, $T_POS, $POS);
            if (isset($to_find[$basis_token])) {
                unset($to_find[$basis_token]);
            }
            if ((!$WELL_FORMED_ONLY) && (($WEBSTANDARDS_CHECKER_OFF === null))) {
                if (((!$is_fragment) && ($stack_size == 0)) && ($basis_token != 'html')) {
                    $errors[] = _xhtml_error('XHTML_BAD_ROOT');
                    $bad_root = true;
                }
                if ($stack_size != 0) {
                    if (isset($ONLY_CHILDREN[$PARENT_TAG])) {
                        if (!in_array($basis_token, $ONLY_CHILDREN[$PARENT_TAG])) {
                            $errors[] = _xhtml_error('XHTML_BAD_CHILD', $basis_token, $PARENT_TAG);
                        }
                    }

                    foreach ($TAG_STACK as $parent_tag) {
                        if (isset($PROHIBITIONS[$parent_tag])) {
                            $prohibitions = $PROHIBITIONS[$parent_tag];
                            if (in_array($basis_token, $prohibitions)) {
                                $errors[] = _xhtml_error('XHTML_PROHIBITION', $basis_token, $parent_tag);
                            }
                        }
                    }
                }

                if ((isset($REQUIRE_ANCESTOR[$basis_token])) && (!$is_fragment)) {
                    if (!in_array($REQUIRE_ANCESTOR[$basis_token], $TAG_STACK)) {
                        $errors[] = _xhtml_error('XHTML_MISSING_ANCESTOR', $basis_token, $REQUIRE_ANCESTOR[$basis_token]);
                    }
                }
                if (isset($ONLY_PARENT[$basis_token])) {
                    if ($stack_size == 0) {
                        if (!$is_fragment) {
                            $errors[] = _xhtml_error('XHTML_BAD_PARENT', $basis_token, '/');
                        }
                    } else {
                        if (!in_array($PARENT_TAG, $ONLY_PARENT[$basis_token])) {
                            $errors[] = _xhtml_error('XHTML_BAD_PARENT', $basis_token, $PARENT_TAG);
                        }
                    }
                }
            }

            // In order to ease webstandards checking, we tolerate these in the parser (but of course, mark as errors)
            if ((($WEBSTANDARDS_CHECKER_OFF === null)) && (!$WELL_FORMED_ONLY) && ($term === false) && (isset($MUST_SELFCLOSE_TAGS[$basis_token]))) {
                if ($XML_CONSTRAIN) {
                    $errors[] = _xhtml_error('XHTML_NONEMPTY_TAG', $basis_token);
                }
            } else {
                if ($term === false) {
                    $PARENT_TAG = $basis_token;
                    array_push($TAG_STACK, $basis_token);
                    array_push($ATT_STACK, $LAST_TAG_ATTRIBUTES);
                    array_push($content_start_stack, $POS);
                    array_push($only_one_of_stack, $only_one_of);
                    $only_one_of = $only_one_of_template;
                    ++$stack_size;
                } else {
                    if ((($WEBSTANDARDS_CHECKER_OFF === null)) && (!$WELL_FORMED_ONLY) && ((!$XML_CONSTRAIN) || (isset($NEVER_SELFCLOSE_TAGS[$basis_token]))) && (($WEBSTANDARDS_CHECKER_OFF === null))) { // A tags must not self close even when only an anchor. Makes a weird underlined line effect in firefox
                        if (!$bad_root) {
                            $errors[] = _xhtml_error('XHTML_CEMPTY_TAG', $basis_token);
                        }
                    }
                }
            }
        } elseif ($term == 1) { // Check its the closing to the stacks highest
            // HTML allows implicit closing. We will flag errors when we have to do it. See 1-2-3 note
            do {
                // For case 3 (see note below)
                if (!in_array($basis_token, $TAG_STACK)) {
                    if ((($WEBSTANDARDS_CHECKER_OFF === null)) && ($XML_CONSTRAIN)) {
                        $errors[] = _xhtml_error('XML_NO_CLOSE_MATCH', $basis_token, $previous);
                    }
                    break;
                }

                $previous = array_pop($TAG_STACK);
                $PARENT_TAG = ($TAG_STACK == array()) ? '' : $TAG_STACK[count($TAG_STACK) - 1];
                $start_pos = array_pop($content_start_stack);
                array_pop($ATT_STACK);
                $only_one_of = array_pop($only_one_of_stack);
                if ($previous === null) {
                    if ((($WEBSTANDARDS_CHECKER_OFF === null)) && ($XML_CONSTRAIN)) {
                        $errors[] = _xhtml_error('XML_MORE_CLOSE_THAN_OPEN', $basis_token);
                    }
                    break;
                }

                if ($basis_token != $previous) {
                    // This is really tricky, and totally XHTML-incompliant. There are three situations:
                    // 1) Overlapping tags. We really can't survive this, and it's very invalid. We could only detect it if we broke support for cases (1) and (2). e.g. <i><b></i></b>
                    // 2) Implicit closing. We close everything implicitly until we find the matching tag. E.g. <i><b></i>
                    // 3) Closing something that was never open. This is tricky - we can't survive it if it was opened somewhere as a parent, as we'd end up closing a whole load of tags by rule (2) - but if it's a lone closing, we can skip it. Good e.g. <b></i></b>. Bad e.g. <div><p></div></p></div>
                    if ((($WEBSTANDARDS_CHECKER_OFF === null)) && ($XML_CONSTRAIN)) {
                        $errors[] = _xhtml_error('XML_NO_CLOSE_MATCH', $basis_token, $previous);
                    }
                }

                if ((!$WELL_FORMED_ONLY) && (($WEBSTANDARDS_CHECKER_OFF === null))) {
                    if ((isset($MUST_SELFCLOSE_TAGS[$previous])) && ($XML_CONSTRAIN)) {
                        $errors[] = _xhtml_error('XHTML_NONEMPTY_TAG', $previous);
                    }

                    if ((!isset($MUST_SELFCLOSE_TAGS[$previous])) && (!isset($POSSIBLY_EMPTY_TAGS[$previous])) && (trim(substr($OUT, $start_pos, $T_POS - $start_pos)) == '')) {
                        if ((isset($TAGS_BLOCK[$previous])) || (isset($TAGS_INLINE[$previous])) || (isset($TAGS_NORMAL[$previous])) || (isset($TAGS_BLOCK_DEPRECATED[$previous])) || (isset($TAGS_INLINE_DEPRECATED[$previous])) || (isset($TAGS_NORMAL_DEPRECATED[$previous]))) {
                            $errors[] = _xhtml_error('XHTML_EMPTY_TAG', $previous);
                        }
                    }
                }
                $stack_size--;
                $level_ranges[] = array($stack_size, $T_POS, $POS);
                //echo 'Popped $previous<br />';

                if ((($WEBSTANDARDS_CHECKER_OFF === null)) && (!$WELL_FORMED_ONLY) && (($WEBSTANDARDS_CHECKER_OFF === null))) {
                    if ($previous == 'script') {
                        $tag_contents = substr($OUT, $start_pos, $T_POS - $start_pos);
                        $c_section = strpos($tag_contents, ']]>');
                        if ((trim($tag_contents) != '') && (strpos($tag_contents, '//-->') === false) && (strpos($tag_contents, '// -->') === false) && ($c_section === false)) {
                            $errors[] = _xhtml_error('XHTML_SCRIPT_COMMENTING', $previous);
                        } elseif (($c_section === false) && ((strpos($tag_contents, '<!--') !== false))) {
                            if ($XML_CONSTRAIN) {
                                $errors[] = _xhtml_error('XHTML_CDATA');
                            }
                        }
                        if (strpos($tag_contents, '</') !== false) {
                            $errors[] = _xhtml_error('XML_JS_TAG_ESCAPE');
                        }
                    }
                }
            } while ($basis_token != $previous);
        }

        $token = _get_next_tag();
    }

    // Check we have everything closed
    if ($stack_size != 0) {
        if ($XML_CONSTRAIN) {
            $errors[] = _xhtml_error('XML_NO_CLOSE', array_pop($TAG_STACK));
        }
        return array('level_ranges' => $level_ranges, 'tag_ranges' => $TAG_RANGES, 'value_ranges' => $VALUE_RANGES, 'errors' => $errors);
    }

    if (!$well_formed_only) { // if ((($WEBSTANDARDS_CHECKER_OFF === null)) || (!$well_formed_only)) // checker-off check needed because it's possible a non-checkable portion foobars up possibility of interpreting the rest of the document such that checking ends early
        if (!$is_fragment) {
            foreach (array_keys($to_find) as $tag) {
                $errors[] = _xhtml_error('XHTML_MISSING_TAG', $tag);
            }

            if ((!$FOUND_DOCTYPE) && (!$GLOBALS['MAIL_MODE'])) {
                $errors[] = _xhtml_error('XHTML_DOCTYPE');
            }
            if (($FOUND_DOCTYPE) && ($GLOBALS['MAIL_MODE'])) {
                $errors[] = _xhtml_error('MAIL_DOCTYPE');
            }
            if (!$FOUND_CONTENTTYPE) {
                $errors[] = _xhtml_error('XHTML_CONTENTTYPE');
            }
            //if (!$FOUND_KEYWORDS) $errors[]=_xhtml_error('XHTML_KEYWORDS');
            //if (!$FOUND_DESCRIPTION) $errors[]=_xhtml_error('XHTML_DESCRIPTION');
        }

        if (!$is_fragment) {
            // Check that all area-links have a corresponding hyperlink
            foreach (array_keys($AREA_LINKS) as $id) {
                if (!in_array($id, $HYPERLINK_URLS)) {
                    $errors[] = _xhtml_error('WCAG_AREA_EQUIV', $id);
                }
            }

            // Check that all labels apply to real input tags
            foreach (array_keys($FOR_LABEL_IDS_2) as $id) {
                if (!isset($INPUT_TAG_IDS[$id])) {
                    $errors[] = _xhtml_error('XHTML_ID_UNBOUND', $id);
                }
            }
        }
    }

    // Main spelling
    if (isset($GLOBALS['SPELLING'])) {
        $stripped = $OUT;
        $matches = array();
        if (stripos($stripped, '<style') !== false) {
            $num_matches = preg_match_all('#\<style.*\</style\>#Umis', $stripped, $matches);
            for ($i = 0; $i < $num_matches; $i++) {
                $stripped = str_replace($matches[0][$i], str_repeat(' ', strlen($matches[0][$i])), $stripped);
            }
        }
        if (stripos($stripped, '<script') !== false) {
            $num_matches = preg_match_all('#\<script.*\</script\>#Umis', $stripped, $matches);
            for ($i = 0; $i < $num_matches; $i++) {
                $stripped = str_replace($matches[0][$i], str_repeat(' ', strlen($matches[0][$i])), $stripped);
            }
        }
        $stripped = @html_entity_decode(strip_tags($stripped), ENT_QUOTES, get_charset());
        require_code('webstandards2');
        $new_errors = check_spelling($stripped);
        $misspellings = array();
        global $POS, $LINENO, $LINESTART;
        foreach ($new_errors as $error) {
            if (array_key_exists($error[1], $misspellings)) {
                continue;
            }
            $misspellings[$error[1]] = 1;
            $POS = strpos($OUT, $error[1]);
            $LINESTART = strrpos(substr($OUT, 0, $POS), "\n");
            $LINENO = substr_count(substr($OUT, 0, $LINESTART), "\n") + 1;
            $errors[] = _xhtml_error($error[0], $error[1]);
        }
    }

    unset($OUT);

    return array('level_ranges' => $level_ranges, 'tag_ranges' => $TAG_RANGES, 'value_ranges' => $VALUE_RANGES, 'errors' => $errors);
}

/**
 * Get some general debugging information for an identified XHTML error.
 *
 * @param  string $error The error that occurred
 * @param  string $param_a The first parameter of the error
 * @param  string $param_b The second parameter of the error
 * @param  string $param_c The third parameter of the error
 * @param  boolean $raw Whether to not do a lang lookup
 * @param  integer $rel_pos Offset position
 * @return map A map of the error information
 *
 * @ignore
 */
function _xhtml_error($error, $param_a = '', $param_b = '', $param_c = '', $raw = false, $rel_pos = 0)
{
    global $POS, $OUT, $LINENO, $LINESTART;
    $lineno = ($rel_pos == 0) ? 0 : substr_count(substr($OUT, $POS, $rel_pos), "\n");
    $out = array();
    $out['line'] = $LINENO + 1 + $lineno;
    if ($rel_pos == 0) {
        $out['pos'] = $POS - $LINESTART;
    } else {
        $out['pos'] = $POS + $rel_pos - strrpos(substr($OUT, 0, $POS + $rel_pos), "\n");
    }
    $out['global_pos'] = $POS + $rel_pos;
    if (function_exists('do_lang')) {
        $out['error'] = $raw ? $error : do_lang($error, htmlentities($param_a), htmlentities($param_b), htmlentities($param_c));
    } else {
        $out['error'] = $raw ? $error : ($error . ': ' . htmlentities($param_a) . ', ' . htmlentities($param_b) . ', ' . htmlentities($param_c));
    }

    return $out;
}

/**
 * Checks to see if a string holds a hexadecimal number.
 *
 * @param  string $string The string to check
 * @return boolean Whether the string holds a hexadecimal number
 */
function is_hex($string)
{
    if (function_exists('ctype_xdigit')) {
        return ctype_xdigit($string);
    }
    return preg_match('#^[\da-f]+$#i', $string) != 0;
}

// Be prepared for some hideous code. I've had to optimise this relatively heavily to keep performance up!

/**
 * Test the next entity in the output stream.
 *
 * @param  integer $offset Checking offset
 * @return ?mixed An array of error details (null: no errors)
 */
function test_entity($offset = 0)
{
    global $OUT, $POS, $ENTITIES;

    $lump = substr($OUT, $POS + $offset, 8);

    $errors = array();

    $pos = strpos($lump, ';');
    //if ($pos!==0) { // "&; sequence" is possible. It's in IPB's posts and to do with emoticon meta tagging
    if ($pos === false) {
        $errors[] = array('XHTML_BAD_ENTITY');
    } else {
        $lump = substr($lump, 0, $pos);
        if (!(($lump[0] == '#') && ((is_numeric(substr($lump, 1))) || (($lump[1] == 'x') && (is_hex(substr($lump, 2))))))) { // It's ok if this is a numeric code, so no need to check further
            // Check against list
            if (!isset($ENTITIES[$lump])) {
                $errors[] = array('XHTML_BAD_ENTITY');
            }
        }
    }
    //}

    if (!isset($errors[0])) {
        return null;
    }
    return $errors;
}

/**
 * Fix any invalid entities in the text.
 *
 * @param  string $in Text to fix in
 * @return string Fixed result
 */
function fix_entities($in)
{
    global $ENTITIES;

    $out = '';

    if ((strpos($in, '&') === false) && (strpos($in, '<') === false)) {
        return $in;
    }

    $len = strlen($in);
    $cdata = false;
    for ($i = 0; $i < $len; $i++) {
        $char = $in[$i];

        $out .= $char;

        if (($char == '<') && (substr($in, $i, 9) == '<![CDATA[')) {
            $cdata = true;
        }

        if ($cdata) {
            if (($char == '/') && (substr($in, $i, 5) == '//]]>')) {
                $cdata = false;
            }
        } else {
            if ($char == '&') {
                $lump = substr($in, $i + 1, 8);
                $pos = strpos($lump, ';');

                if ($pos === false) {
                    $out .= 'amp;';
                } else {
                    $lump = substr($lump, 0, $pos);
                    if (!((isset($lump[0])) && ($lump[0] == '#') && ((is_numeric(substr($lump, 1))) || ((isset($lump[1])) && ($lump[1] == 'x') && (is_hex(substr($lump, 2))))))) {
                        if (!isset($ENTITIES[$lump])) {
                            $out .= 'amp;';
                        }
                    }
                }
            }
        }
    }

    return $out;
}

/**
 * Get the next tag in the current XHTML document.
 *
 * @return ?mixed Either an array of error details, a string of the tag, or null for finished (null: no next tag)
 * @ignore
 */
function _get_next_tag()
{
    global $PARENT_TAG, $POS, $LINENO, $LINESTART, $OUT, $T_POS, $ENTITIES, $LEN, $ANCESTOR_BLOCK, $TAG_STACK, $WEBSTANDARDS_CHECKER_OFF, $TEXT_NO_BLOCK, $INBETWEEN_TEXT;
    global $TAG_RANGES, $VALUE_RANGES;

    $status = NO_MANS_LAND;

    $current_tag = '';
    $current_attribute_name = '';
    $current_attribute_value = '';
    $close = false;
    $doc_type = '';
    $INBETWEEN_TEXT = '';

    $attribute_map = array();

    $errors = array();

    $special_chars = null;
    if ($special_chars === null) {
        $special_chars = array('=' => true, '"' => true, '&' => true, '/' => true, '<' => true, '>' => true, ' ' => true, "\n" => true, "\r" => true);
    }

    while ($POS < $LEN) {
        $next = $OUT[$POS];
        $POS++;

        if ($next == "\n") {
            $LINENO++;
            $LINESTART = $POS;
        }
        //echo $status . ' for ' . $next . '<br />';

        // Entity checking
        if (($next == '&') && ($status != IN_CDATA) && ($status != IN_COMMENT) && ($WEBSTANDARDS_CHECKER_OFF === null)) {
            $test = test_entity();
            if ($test !== null) {
                $errors = array_merge($errors, $test);
            }
        }

        // State machine
        switch ($status) {
            case NO_MANS_LAND:
                $in_no_mans_land = '';
                $continue = ($next != '<') && ($next != '&') && ($POS < $LEN - 1);
                if ($next != '<') {
                    $INBETWEEN_TEXT .= $next;
                }
                while ($continue) {
                    $next = $OUT[$POS];
                    $POS++;
                    $continue = ($next != '<') && ($next != '&') && ($POS < $LEN - 1);
                    if ($continue) {
                        $in_no_mans_land .= $next;
                    }
                    if ($next != '<') {
                        $INBETWEEN_TEXT .= $next;
                    }
                    if ($next == "\n") {
                        $LINENO++;
                        $LINESTART = $POS;
                    }
                }
                if (($next == '&') && ($WEBSTANDARDS_CHECKER_OFF === null)) {
                    $test = test_entity();
                    if ($test !== null) {
                        $errors = array_merge($errors, $test);
                    }
                }

                // Can't have loose text in form/body/etc
                // 'x' is there for when called externally, checking on an x that has replaced, for example, a directive tag (which isn't actual text - so can't trip the error)
                if (($in_no_mans_land != 'x') && (trim($in_no_mans_land) != '') && (isset($TEXT_NO_BLOCK[$PARENT_TAG])) && ($GLOBALS['BLOCK_CONSTRAIN'])) {
                    $errors[] = array('XHTML_TEXT_NO_BLOCK', $PARENT_TAG);
                }

                if (($next == '<') && (isset($OUT[$POS + 2])) && ($OUT[$POS] == '!')) {
                    if (($OUT[$POS + 1] == '-') && ($OUT[$POS + 2] == '-')) {
                        $status = IN_COMMENT;
                        $INBETWEEN_TEXT .= '<!--';
                        $POS += 3;
                    } elseif (substr($OUT, $POS - 1, 9) == '<![CDATA[') {
                        $status = IN_CDATA;
                        $POS += 8;
                        $INBETWEEN_TEXT .= '<![CDATA[';
                    } else {
                        $status = IN_DTD_TAG;
                    }
                } elseif (($next == '<') && (isset($OUT[$POS])) && ($OUT[$POS] == '?') && ($POS < 10)) {
                    if (!isset($GLOBALS['MAIL_MODE'])) {
                        $GLOBALS['MAIL_MODE'] = false;
                    }
                    if ($GLOBALS['MAIL_MODE']) {
                        $errors[] = array('MAIL_PROLOG');
                    }
                    $status = IN_XML_TAG;
                } elseif ($next == '<') {
                    $T_POS = $POS - 1;
                    $status = STARTING_TAG;
                } else {
                    if ($next == '>') {
                        $errors[] = array('XML_TAG_CLOSE_ANOMALY');
                        return array(null, $errors);
                    }
                }
                break;
            case IN_TAG_NAME:
                $more_to_come = (!isset($special_chars[$next])) && ($POS < $LEN);
                while ($more_to_come) {
                    $current_tag .= $next;
                    $next = $OUT[$POS];
                    $POS++;
                    if ($next == "\n") {
                        $LINENO++;
                        $LINESTART = $POS;
                    }
                    $more_to_come = (!isset($special_chars[$next])) && ($POS < $LEN);
                }
                if (($next == ' ') || ($next == "\n") || ($next == "\r")) {
                    $TAG_RANGES[] = array($T_POS + 1, $POS - 1, $current_tag);
                    $status = IN_TAG_BETWEEN_ATTRIBUTES;
                } elseif ($next == '<') {
                    $errors[] = array('XML_TAG_OPEN_ANOMALY', '1');
                    return array(null, $errors);
                } elseif ($next == '>') {
                    if ($OUT[$POS - 2] == '/') {
                        $TAG_RANGES[] = array($T_POS + 1, $POS - 1, $current_tag);
                        return _check_tag($current_tag, array(), true, $close, $errors);
                    } else {
                        $TAG_RANGES[] = array($T_POS + 1, $POS - 1, $current_tag);
                        return _check_tag($current_tag, array(), false, $close, $errors);
                    }
                } elseif ($next != '/') {
                    $current_tag .= $next;
                }
                break;
            case STARTING_TAG:
                if ($next == '/') {
                    $close = true;
                } elseif ($next == '<') {
                    $errors[] = array('XML_TAG_OPEN_ANOMALY', '2');
                    $POS--;
                    $status = NO_MANS_LAND;
                } elseif ($next == '>') {
                    $errors[] = array('XML_TAG_CLOSE_ANOMALY', '3');
                    $status = NO_MANS_LAND;
                } else {
                    $current_tag .= $next;
                    $status = IN_TAG_NAME;
                }
                break;
            case IN_TAG_BETWEEN_ATTRIBUTES:
                if (($next == '/') && (isset($OUT[$POS])) && ($OUT[$POS] == '>')) {
                    ++$POS;
                    return _check_tag($current_tag, $attribute_map, true, $close, $errors);
                } elseif ($next == '>') {
                    return _check_tag($current_tag, $attribute_map, false, $close, $errors);
                } elseif (($next == '<') && (isset($OUT[$POS + 3])) && ($OUT[$POS] == '!') && ($OUT[$POS + 1] == '-') && ($OUT[$POS + 2] == '-')) {
                    $status = IN_TAG_EMBEDDED_COMMENT;
                    if ($OUT[$POS + 3] == '-') {
                        $errors[] = array('XHTML_WRONG_COMMENTING');
                    }
                } elseif ($next == '<') {
                    $errors[] = array('XML_TAG_OPEN_ANOMALY', '4');
                    return array(null, $errors);
                } elseif (($next != ' ') && ($next != "\t") && ($next != '/') && ($next != "\n") && ($next != "\r")) {
                    $status = IN_TAG_ATTRIBUTE_NAME;
                    $current_attribute_name .= $next;
                }
                break;
            case IN_TAG_ATTRIBUTE_NAME:
                $more_to_come = (!isset($special_chars[$next])) && ($POS < $LEN);
                while ($more_to_come) {
                    $current_attribute_name .= $next;
                    $next = $OUT[$POS];
                    $POS++;
                    if ($next == "\n") {
                        $LINENO++;
                        $LINESTART = $POS;
                    }
                    $more_to_come = (!isset($special_chars[$next])) && ($POS < $LEN);
                }

                if ($next == '=') {
                    if (function_exists('require_code')) {
                        require_code('type_sanitisation');
                    }
                    if ((function_exists('ctype_alnum')) && (ctype_alnum($current_attribute_name))) {
                    } else {
                        if ((preg_match('#^\w+$#', $current_attribute_name) == 0/*optimisation*/) && (!is_alphanumeric(preg_replace('#^([^:]+):#', '${1}', $current_attribute_name)))) {
                            $errors[] = array('XML_TAG_BAD_ATTRIBUTE', $current_attribute_name);
                            $current_attribute_name = 'wrong' . strval($POS);
                        }
                    }
                    $status = IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_RIGHT;
                } elseif ($next == '<') {
                    $errors[] = array('XML_TAG_OPEN_ANOMALY', '5');
                    //return array(null, $errors);
                    // We have to assume we shouldn't REALLY have found a tag
                    $POS--;
                    $current_tag = '';
                    $status = NO_MANS_LAND;
                } elseif ($next == '>') {
                    if (function_exists('require_code')) {
                        require_code('type_sanitisation');
                    }
                    if (!is_alphanumeric(preg_replace('#^([^:]+):#', '${1}', $current_attribute_name))) {
                        $errors[] = array('XML_TAG_BAD_ATTRIBUTE', $current_attribute_name);
                        $current_attribute_name = 'wrong' . strval($POS);
                    }

                    if ($GLOBALS['XML_CONSTRAIN']) {
                        $errors[] = array('XML_TAG_CLOSE_ANOMALY');
                    }
                    // Things like nowrap, checked, etc
                    //return array(null, $errors);

                    if (isset($attribute_map[$current_attribute_name])) {
                        $errors[] = array('XML_TAG_DUPLICATED_ATTRIBUTES', $current_tag);
                    }
                    $attribute_map[$current_attribute_name] = $current_attribute_name;
                    $current_attribute_name = '';
                    $VALUE_RANGES[] = array($POS - 1, $POS - 1);
                    return _check_tag($current_tag, $attribute_map, false, $close, $errors);
                } elseif (($next != ' ') && ($next != "\t") && ($next != "\n") && ($next != "\r")) {
                    $current_attribute_name .= $next;
                } else {
                    if (function_exists('require_code')) {
                        require_code('type_sanitisation');
                    }
                    if (!is_alphanumeric(preg_replace('#^([^:]+):#', '${1}', $current_attribute_name))) {
                        $errors[] = array('XML_TAG_BAD_ATTRIBUTE', $current_attribute_name);
                        $current_attribute_name = 'wrong' . strval($POS);
                    }
                    $status = IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_LEFT;
                }
                break;
            case IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_LEFT:
                if ($next == '=') {
                    $status = IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_RIGHT;
                } elseif (($next != ' ') && ($next != "\t") && ($next != "\n") && ($next != "\r")) {
                    if ($GLOBALS['XML_CONSTRAIN']) {
                        $errors[] = array('XML_ATTRIBUTE_ERROR');
                    }
                    //return array(null, $errors);  Actually  <blah nowrap ... /> could cause this

                    $status = IN_TAG_BETWEEN_ATTRIBUTES;
                    if (isset($attribute_map[$current_attribute_name])) {
                        $errors[] = array('XML_TAG_DUPLICATED_ATTRIBUTES', $current_tag);
                    }
                    $attribute_map[$current_attribute_name] = $current_attribute_name;
                    $current_attribute_name = $next;
                    $VALUE_RANGES[] = array($POS - 1, $POS - 1);
                }
                break;
            case IN_TAG_BETWEEN_ATTRIBUTE_NAME_VALUE_RIGHT:
                if ($next == '"') {
                    $v_pos = $POS;
                    $status = IN_TAG_ATTRIBUTE_VALUE_BIG_QUOTES;
                } elseif (($next == '\'') && (true)) { // Change to false if we want to turn off these quotes (preferred - but we can't control all input :( )
                    $v_pos = $POS;
                    $status = IN_TAG_ATTRIBUTE_VALUE_LITTLE_QUOTES;
                } elseif (($next != ' ') && ($next != "\t") && ($next != "\n") && ($next != "\r")) {
                    if ($next == '<') {
                        $errors[] = array('XML_TAG_OPEN_ANOMALY', '6');
                    } elseif ($next == '>') {
                        $errors[] = array('XML_TAG_CLOSE_ANOMALY');
                    }

                    if ($GLOBALS['XML_CONSTRAIN']) {
                        $errors[] = array('XML_ATTRIBUTE_ERROR');
                    }
                    $POS--;
                    $v_pos = $POS;
                    $status = IN_TAG_ATTRIBUTE_VALUE_NO_QUOTES;
                }
                break;
            case IN_TAG_ATTRIBUTE_VALUE_NO_QUOTES:
                if ($next == '>') {
                    if (isset($attribute_map[$current_attribute_name])) {
                        $errors[] = array('XML_TAG_DUPLICATED_ATTRIBUTES', $current_tag);
                    }
                    $attribute_map[$current_attribute_name] = $current_attribute_value;
                    $current_attribute_value = '';
                    $current_attribute_name = '';
                    $VALUE_RANGES[] = array($v_pos, $POS - 1);
                    return _check_tag($current_tag, $attribute_map, false, $close, $errors);
                } elseif (($next == ' ') || ($next == "\t") || ($next == "\n") || ($next == "\r")) {
                    $status = IN_TAG_BETWEEN_ATTRIBUTES;
                    if (isset($attribute_map[$current_attribute_name])) {
                        $errors[] = array('XML_TAG_DUPLICATED_ATTRIBUTES', $current_tag);
                    }
                    $attribute_map[$current_attribute_name] = $current_attribute_value;
                    $current_attribute_value = '';
                    $current_attribute_name = '';
                    $VALUE_RANGES[] = array($v_pos, $POS - 1);
                } else {
                    if ($next == '<') {
                        $errors[] = array('XML_TAG_OPEN_ANOMALY', '7');
                    }

                    $current_attribute_value .= $next;
                }
                break;
            case IN_TAG_ATTRIBUTE_VALUE_BIG_QUOTES:
                $more_to_come = (!isset($special_chars[$next])) && ($POS < $LEN);
                while ($more_to_come) {
                    $current_attribute_value .= $next;
                    $next = $OUT[$POS];
                    $POS++;
                    if ($next == "\n") {
                        $LINENO++;
                        $LINESTART = $POS;
                    }
                    $more_to_come = (!isset($special_chars[$next])) && ($POS < $LEN);
                }
                if (($next == '&') && ($WEBSTANDARDS_CHECKER_OFF === null)) {
                    $test = test_entity();
                    if ($test !== null) {
                        $errors = array_merge($errors, $test);
                    }
                }

                if ($next == '"') {
                    $status = IN_TAG_BETWEEN_ATTRIBUTES;
                    if (isset($attribute_map[$current_attribute_name])) {
                        $errors[] = array('XML_TAG_DUPLICATED_ATTRIBUTES', $current_tag);
                    }
                    $attribute_map[$current_attribute_name] = $current_attribute_value;
                    $current_attribute_value = '';
                    $current_attribute_name = '';
                    $VALUE_RANGES[] = array($v_pos, $POS - 1);
                } else {
                    if ($next == '<') {
                        $errors[] = array('XML_TAG_OPEN_ANOMALY', '7');
                    } elseif ($next == '>') {
                        $errors[] = array('XML_TAG_CLOSE_ANOMALY');
                    }

                    $current_attribute_value .= $next;
                }
                break;
            case IN_TAG_ATTRIBUTE_VALUE_LITTLE_QUOTES:
                if ($next == '\'') {
                    $status = IN_TAG_BETWEEN_ATTRIBUTES;
                    $attribute_map[$current_attribute_name] = $current_attribute_value;
                    $current_attribute_value = '';
                    $current_attribute_name = '';
                    $VALUE_RANGES[] = array($v_pos, $POS - 1);
                } else {
                    if ($next == '<') {
                        $errors[] = array('XML_TAG_OPEN_ANOMALY', '7');
                    } elseif ($next == '>') {
                        $errors[] = array('XML_TAG_CLOSE_ANOMALY');
                    }

                    $current_attribute_value .= $next;
                }
                break;
            case IN_XML_TAG:
                if (($OUT[$POS - 2] == '?') && ($next == '>')) {
                    $status = NO_MANS_LAND;
                }
                break;
            case IN_DTD_TAG: // This is a parser-directive, but we only use them for doctypes
                $doc_type .= $next;
                if ($next == '>') {
                    if (substr($doc_type, 0, 8) == '!DOCTYPE') {
                        global $THE_DOCTYPE, $TAGS_DEPRECATE_ALLOW, $FOUND_DOCTYPE, $XML_CONSTRAIN, $BLOCK_CONSTRAIN;

                        $FOUND_DOCTYPE = true;
                        $valid_doctypes = array(DOCTYPE_XHTML5);
                        $doc_type = preg_replace('#//EN"\s+"#', '//EN" "', $doc_type);
                        if (!in_array('<' . $doc_type, $valid_doctypes)) {
                            $errors[] = array('XHTML_DOCTYPE');
                        } else {
                            $THE_DOCTYPE = '<' . $doc_type;
                            $TAGS_DEPRECATE_ALLOW = false;
                            $BLOCK_CONSTRAIN = true;
                            $XML_CONSTRAIN = true;
                        }
                    }
                    $status = NO_MANS_LAND;
                }
                break;
            case IN_CDATA:
                $INBETWEEN_TEXT .= $next;
                if (($next == '>') && ($OUT[$POS - 2] == ']') && ($OUT[$POS - 3] == ']')) {
                    $status = NO_MANS_LAND;
                }
                break;
            case IN_COMMENT:
                $INBETWEEN_TEXT .= $next;
                if (($next == '>') && ($OUT[$POS - 2] == '-') && ($OUT[$POS - 3] == '-')) {
                    if ($OUT[$POS - 4] == '-') {
                        $errors[] = array('XHTML_WRONG_COMMENTING');
                    }
                    $status = NO_MANS_LAND;
                }
                break;
            case IN_TAG_EMBEDDED_COMMENT:
                if (($next == '>') && ($OUT[$POS - 2] == '-') && ($OUT[$POS - 3] == '-')) {
                    $status = IN_TAG_BETWEEN_ATTRIBUTES;
                }
                break;
        }
    }
    if ($status != NO_MANS_LAND) {
        $errors[] = array('XML_BROKEN_END');
        return array(null, $errors);
    }
    return null;
}

/**
 * Checks an XHTML tag for conformance, including attributes. Return the results.
 *
 * @param  string $tag The name of the tag to check
 * @param  map $attributes A map of attributes (name=>value) the tag has
 * @param  boolean $self_close Whether this is a self-closing tag
 * @param  boolean $close Whether this is a closing tag
 * @param  array $errors Errors detected so far. We will add to these and return
 * @return mixed String for tag basis form, or array of error information
 *
 * @ignore
 */
function _check_tag($tag, $attributes, $self_close, $close, $errors)
{
    global $XML_CONSTRAIN, $LAST_TAG_ATTRIBUTES, $WELL_FORMED_ONLY, $WEBSTANDARDS_CHECKER_OFF, $MUST_SELFCLOSE_TAGS;

    $ltag = strtolower($tag);
    if ($ltag != $tag) {
        if ($XML_CONSTRAIN) {
            $errors[] = array('XHTML_CASE_TAG', $tag);
        }
        $tag = $ltag;
    }

    $LAST_TAG_ATTRIBUTES = $attributes;

    $actual_self_close = $self_close;
    if ((!$WELL_FORMED_ONLY) && (!$self_close) && (isset($MUST_SELFCLOSE_TAGS[$tag]))) {
        $self_close = true; // Will be flagged later
    }

    if (((isset($attributes['class'])) && (strpos($attributes['class'], 'webstandards_checker_off') !== false)) || ((isset($attributes['xmlns'])) && (strpos($attributes['xmlns'], 'xhtml') === false))) {
        $WEBSTANDARDS_CHECKER_OFF = 0;
    }

    if (!$WELL_FORMED_ONLY) {
        $errors = __check_tag($tag, $attributes, $self_close, $close, $errors);
    }

    if ($WEBSTANDARDS_CHECKER_OFF > 0) {
        $errors = array();
    }
    return array('<' . ($close ? '/' : '') . $tag . ($actual_self_close ? '/' : '') . '>', $errors);
}

/**
 * Get the tag basis for the specified tag. e.g. '<br />' would become 'br'. Note: tags with parameters given are not supported.
 *
 * @param  string $full The full tag
 * @return string The basis of the tag
 *
 * @ignore
 */
function _get_tag_basis($full)
{
    return trim($full, '/ <>');
}
