<?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
 */

/*EXTRA FUNCTIONS: pspell\_.+*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__webstandards2()
{
    global $TAGS_BLOCK;
    $TAGS_BLOCK = array(
        'div' => 1,
        'h1' => 1,
        'h2' => 1,
        'h3' => 1,
        'h4' => 1,
        'h5' => 1,
        'h6' => 1,
        'p' => 1,
        'blockquote' => 1,
        'pre' => 1,
        'hr' => 1,
        'fieldset' => 1,
        'figure' => 1,

        // Best classified as block
        'address' => 1,
        'iframe' => 1,
        'noscript' => 1,
        'table' => 1,
        'tbody' => 1,
        'td' => 1,
        'tfoot' => 1,
        'th' => 1,
        'thead' => 1,
        'tr' => 1,
        'dd' => 1,
        'dt' => 1,
        'dl' => 1,
        'li' => 1,
        'ol' => 1,
        'ul' => 1,

        // XHTML5
        'rbc' => 1,
        'rtc' => 1,
        'rb' => 1,
        'rt' => 1,
        'rp' => 1,
    );
    $TAGS_BLOCK += array(
        'video' => 1,
        'details' => 1,
        'summary' => 1,
        'section' => 1,
        'nav' => 1,
        'header' => 1,
        'footer' => 1,
        'figure' => 1,
        'canvas' => 1,
        'audio' => 1,
        'aside' => 1,
        'article' => 1,
    );

    global $TAGS_INLINE;
    $TAGS_INLINE = array(
        'span' => 1,
        'br' => 1,
        'abbr' => 1,
        'cite' => 1,
        'code' => 1,
        'dfn' => 1,
        'em' => 1,
        'strong' => 1,
        'kbd' => 1,
        'q' => 1,
        'samp' => 1,
        'var' => 1,
        'sub' => 1,
        'sup' => 1,
        'del' => 1,

        // XHTML5
        'ruby' => 1,

        // Best classified as inline
        'a' => 1,
        'bdo' => 1,
        'img' => 1,
        'ins' => 1,
        'param' => 1,
        'textarea' => 1,
        'button' => 1,
        'input' => 1,
        'select' => 1,
        'embed' => 1,
        'object' => 1,
        'caption' => 1,
        'label' => 1,
    );

    $TAGS_INLINE += array(
        'wbr' => 1,
        'time' => 1,
        'progress' => 1,
        'output' => 1,
        'meter' => 1,
        'mark' => 1,
        'keygen' => 1,
        'datalist' => 1,
        'command' => 1,
        'track' => 1,
    );

    global $TAGS_NORMAL;
    $TAGS_NORMAL = array(
        'base' => 1,
        'body' => 1,
        'col' => 1,
        'colgroup' => 1,
        'head' => 1,
        'html' => 1,
        'link' => 1,
        'map' => 1,
        'meta' => 1,
        'optgroup' => 1,
        'option' => 1,
        'style' => 1,
        'title' => 1,
        'legend' => 1,
        'figcaption' => 1,
        'script' => 1,
        'area' => 1,

        // I'd call this 'block', but XHTML-strict other checkers would disagree - probably xhtml-strict doesn't consider 'programmatic' elements to be inline/block
        'form' => 1,
    );

    $TAGS_NORMAL += array(
        'source' => 1,
    );

    global $TAGS_BLOCK_DEPRECATED;
    $TAGS_BLOCK_DEPRECATED = array(
        'dir' => 1,
        'menu' => 1,
    );

    global $TAGS_INLINE_DEPRECATED;
    $TAGS_INLINE_DEPRECATED = array(
        // Would be removed in XHTML strict and deprecated in transitional
        'center' => 1,
        'applet' => 1,
        'font' => 1,
        's' => 1,
        'strike' => 1,
        'u' => 1,
    );

    global $TAGS_NORMAL_DEPRECATED;
    $TAGS_NORMAL_DEPRECATED = array(
        'basefont' => 1,
    );

    if (function_exists('cms_srv')) {
        $browser = strtolower(cms_srv('HTTP_USER_AGENT'));
        $is_ie = (strpos($browser, 'msie') !== false);
    } else {
        $is_ie = false;
    }

    $enforce_javascript = '([^\n]+)';
    $enforce_lang = '[a-zA-Z][a-zA-Z](-[a-zA-Z]+)?';
    $enforce_direction = '(ltr|rtl)';
    $enforce_align = '(left|center|right|justify|char)';
    $enforce_align2 = '(top|middle|bottom|left|right)';
    $enforce_align3 = '(left|center|right|justify)';
    $enforce_align4 = '(top|bottom|left|right)';
    $enforce_valign = '(top|middle|bottom|baseline)';
    $enforce_number = '(-?[0-9]+)';
    $enforce_fractional = '(-?[0-9]*(\.)[0-9]+?)';
    $enforce_inumber = '[0-9]+';
    $enforce_character = '.';
    $enforce_color = '(black|silver|gray|white|maroon|purple|fuchsia|green|darkgrey|slategrey|lime|olive|yellow|navy|blue|teal|aqua|orange|red|(\#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])|(\#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]))'; // orange and red aren't 'official' -- but kind of handy ;). In reality, the colour codes were never properly defined, and these two are obvious names for obviously needed ones-- they'll be supported
    $enforce_length = '((0)|(' . $enforce_number . '(|in|cm|mm|ex|pt|pc|px|em|%|vw))|((' . $enforce_fractional . ')(in|cm|mm|ex|px|em|%|vw)))'; // |ex|pt|in|cm|mm|pc  We don't want these in our XHTML... preferably we only want em when it comes to font size!
    $enforce_ilength = '((0)|(' . $enforce_inumber . '(|in|cm|mm|ex|pt|pc|px|em|%|vw))|((' . $enforce_inumber . ')?\.' . $enforce_inumber . '(in|cm|mm|ex|em|%|vw)))'; // |ex|pt|in|cm|mm|pc We don't want these in our XHTML... preferably we only want em when it comes to font size!
    $enforce_pixels = '[0-9]+';
    $enforce_auto_or_length = '(auto|' . $enforce_length . ')';
    $enforce_auto_or_ilength = '(auto|' . $enforce_ilength . ')';
    $enforce_normal_or_length = '(normal|' . $enforce_length . ')';
    $enforce_border_width = '(thin|medium|thick|' . $enforce_length . ')';
    $enforce_potential_4d_border_width = $enforce_border_width . '( ' . $enforce_border_width . '( ' . $enforce_border_width . '( ' . $enforce_border_width . '|)|)|)';
    $color_types = array(
        '(rgb\(' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?\))',
        '(rgba\(' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?,\s*[01]?(\.\d+)?\))',
        '(hsl\(' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?\))',
        '(hsla\(' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?,\s*' . $enforce_inumber . '%?,\s*[01]?(\.\d+)?\))',
        '(\#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])',
        $enforce_color,
        'ActiveBorder|ActiveCaption|AppWorkspace|Background|Buttonface|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText',
    );
    $enforce_css_color = '(' . implode('|', $color_types) . ')';
    $enforce_transparent_or_color = '(transparent|' . $enforce_css_color . ')';
    $enforce_fraction = '(\d%|\d\d%|100%|0?\.\d+|1\.0|0|1)';
    $_enforce_font_list = "((?i)cursive|fantasy|monospace|serif|sans-serif|Georgia|Times|Trebuchet|Tahoma|Geneva|Verdana|Arial|Helvetica|Courier|Courier New|Impact|'Georgia'|'Times'|'Trebuchet'|'Tahoma'|'Geneva'|'Verdana'|'Arial'|'Helvetica'|'Courier'|'Courier New'|'Impact'(?-i))";
    $enforce_font_list = '((([\w\-]+)|("[\w\- ]+")|(\'[\w\- ]+\')),\s*)*' . $_enforce_font_list;
    $enforce_functional_url = '(url\(\'.+\'\)|url\(".+"\)|url\([^\(\);]+\))';
    $enforce_functional_url_or_none = '(' . $enforce_functional_url . '|none)';
    $enforce_border_style = '(none|dotted|dashed|solid|double|groove|ridge|inset|outset|transparent)'; // 'transparent' not supported in IE6
    $enforce_background_repeat = '(repeat|repeat-x|repeat-y|no-repeat)';
    $enforce_attachment = '(scroll|fixed)';
    $_enforce_background_position = '((' . $enforce_length . '|top( ' . $enforce_length . ')?|center|bottom( ' . $enforce_length . ')?)|(' . $enforce_length . '|left( ' . $enforce_length . ')?|center|right( ' . $enforce_length . ')?))';
    $enforce_background_position = '((' . $_enforce_background_position . ')|(' . $_enforce_background_position . ' ' . $_enforce_background_position . '))';
    $enforce_border = '((' . $enforce_border_width . '|' . $enforce_border_style . '|' . $enforce_css_color . ')( |$))+';
    $enforce_potential_4d_length = $enforce_length . '( ' . $enforce_length . '( ' . $enforce_length . '( ' . $enforce_length . '|)|)|)';
    $enforce_potential_4d_length_auto = $enforce_auto_or_length . '( ' . $enforce_auto_or_length . '( ' . $enforce_auto_or_length . '( ' . $enforce_auto_or_length . '|)|)|)';
    $enforce_potential_4d_ilength = $enforce_ilength . '( ' . $enforce_ilength . '( ' . $enforce_ilength . '( ' . $enforce_ilength . '|)|)|)';
    $enforce_potential_4d_ilength_auto = $enforce_auto_or_ilength . '( ' . $enforce_auto_or_ilength . '( ' . $enforce_auto_or_ilength . '( ' . $enforce_auto_or_ilength . '|)|)|)';
    $enforce_font_size = '(larger|smaller|xx-small|x-small|small|medium|large|x-large|xx-large|' . $enforce_length . '(/' . $enforce_length . ')?)';
    $enforce_font_style = '(normal|italic|oblique)';
    $enforce_font_variant = '(normal|small-caps)';
    $enforce_font_weight = '(lighter|normal|bold|bolder|((\d)+))';
    $enforce_list_style_position = '(inside|outside)';
    $enforce_list_style_type = '(none|disc|circle|square|decimal|lower-roman|upper-roman|lower-alpha|upper-alpha' . ((!$is_ie) ? '|decimal-leading-zero|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha' : '') . ')';
    $enforce_list_style_image = '(none|' . $enforce_functional_url . ')';
    $enforce_id = '[a-zA-Z][\w\-\:\.]*';
    $enforce_name = '[\w\-\:\.]+(\[\])?';
    if (function_exists('get_forum_type')) {
        require_code('obfuscate');
        $enforce_link = ((get_forum_type() == 'none') ? '(mailto:.*)?' : '') . '(mailto:.*|' . preg_quote(mailto_obfuscated(), '#') . '.*)?[^\s\#]*(\#[^\s\#]*)?';
    } else {
        $enforce_link = '.*';
    }
    $enforce_class = '[ \w\-]*';
    $_counter_increment = '((\w+( \d+)?)+)';
    $enforce_counter_increment = $_counter_increment . '( ' . $_counter_increment . ')*';
    $enforce_transition_timing_function = '(linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier\(' . $enforce_fraction . ' ' . $enforce_fraction . ' ' . $enforce_fraction . ' ' . $enforce_fraction . '\))';
    $enforce_time = '\d[\d\.]*s';
    $enforce_box_shadow = '((inset )?' . $enforce_length . '( ' . $enforce_length . '( ' . $enforce_length . '( ' . $enforce_length . ')?)?)? ' . $enforce_css_color . ')';
    $enforce_transition_property = '[\w,\-]+';
    $enforce_transform_origin = '(left|center|right|' . $enforce_length . ')';
    $enforce_transform_style = '(flat|preserve-3d)';
    $enforce_transition = $enforce_transition_property . '( ' . $enforce_time . '( ' . $enforce_transition_timing_function . '( ' . $enforce_time . ')?)?)?';

    global $CSS_PROPERTIES;
    $CSS_PROPERTIES = array(
        'background' => '((' . $enforce_transparent_or_color . '|' . $enforce_functional_url_or_none . '|' . $enforce_background_repeat . '|' . $enforce_attachment . '|' . $enforce_background_position . ')( |$))+',
        'background-attachment' => $enforce_attachment,
        'background-color' => $enforce_transparent_or_color,
        'background-image' => /*$enforce_functional_url_or_none*/'.*', // Changed to .* to allow gradients
        'background-repeat' => $enforce_background_repeat,
        'background-position' => $enforce_background_position,
        'border' => $enforce_border,
        'border-collapse' => '(collapse|separate)',
        'border-color' => $enforce_transparent_or_color . '( ' . $enforce_transparent_or_color . '( ' . $enforce_transparent_or_color . '( ' . $enforce_transparent_or_color . '|)|)|)',
        'border-spacing' => $enforce_length . '( ' . $enforce_length . ')?',
        'border-style' => $enforce_border_style,
        'border-width' => $enforce_potential_4d_border_width,
        'border-bottom' => $enforce_border,
        'border-bottom-color' => $enforce_transparent_or_color,
        'border-bottom-style' => $enforce_border_style,
        'border-bottom-width' => $enforce_border_width,
        'border-left' => $enforce_border,
        'border-left-color' => $enforce_transparent_or_color,
        'border-left-style' => $enforce_border_style,
        'border-left-width' => $enforce_border_width,
        'border-right' => $enforce_border,
        'border-right-color' => $enforce_transparent_or_color,
        'border-right-style' => $enforce_border_style,
        'border-right-width' => $enforce_border_width,
        'border-top' => $enforce_border,
        'border-top-color' => $enforce_transparent_or_color,
        'border-top-style' => $enforce_border_style,
        'border-top-width' => $enforce_border_width,
        'bottom' => $enforce_auto_or_length,
        'clear' => '(both|left|right|none)',
        'clip' => 'auto|(rect\(' . $enforce_potential_4d_length . '\))',
        'color' => $enforce_css_color,
        'cursor' => '(' . $enforce_functional_url . '|(?i)auto|crosshair|default|move|text|wait|help|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|pointer|not-allowed|no-drop|vertical-text|all-scroll|col-resize|row-resize|none' . ((!$is_ie) ? '|progress' : '') . '(?-i))', // hand is actually IE specific version of pointer; we'll use Tempcode so as to only show that when really needed
        'direction' => '(ltr|rtl)',
        'display' => '(none|inline|block|list-item|table|table-header-group|table-footer-group|inline-block|run-in' . ((!$is_ie) ? '|inline-table|table-row|table-row-group|table-column-group|table-column|table-cell|table-caption' : '') . ')',
        'float' => '(left|right|none)',
        'font' => '((caption|icon|menu|message-box|small-caption|status-bar|' . $enforce_font_style . '|' . $enforce_font_variant . '|' . $enforce_font_weight . '|' . $enforce_font_size . '|' . $enforce_normal_or_length . '|' . $enforce_font_list . ')( |$))+',
        'font-family' => $enforce_font_list,
        'font-size' => $enforce_font_size,
        'font-style' => $enforce_font_style,
        'font-variant' => $enforce_font_variant,
        'font-weight' => $enforce_font_weight,
        'height' => $enforce_auto_or_length,
        'left' => $enforce_auto_or_length,
        'right' => $enforce_auto_or_length,
        'letter-spacing' => $enforce_normal_or_length,
        'line-height' => '(\d*(\.\d+)?|' . $enforce_normal_or_length . ')',
        'list-style' => '((' . $enforce_list_style_type . '|' . $enforce_list_style_position . '|' . $enforce_list_style_image . ')( |$))+',
        'list-style-image' => '(' . $enforce_functional_url . '|none)',
        'list-style-position' => $enforce_list_style_position,
        'list-style-type' => $enforce_list_style_type,
        'margin' => $enforce_potential_4d_length_auto,
        'margin-bottom' => $enforce_auto_or_length,
        'margin-left' => $enforce_auto_or_length,
        'margin-right' => $enforce_auto_or_length,
        'margin-top' => $enforce_auto_or_length,
        'overflow' => '(visible|hidden|scroll|auto)',
        'padding' => $enforce_potential_4d_ilength,
        'padding-bottom' => $enforce_auto_or_ilength,
        'padding-left' => $enforce_auto_or_ilength,
        'padding-right' => $enforce_auto_or_ilength,
        'padding-top' => $enforce_auto_or_ilength,
        'page-break-after' => '(auto|left|right|always)',
        'page-break-before' => '(auto|left|right|always)',
        'position' => '(static|relative|absolute' . ((!$is_ie) ? '|fixed' : '') . ')',
        'table-layout' => '(auto|fixed)',
        'text-align' => '(left|right|center|justify)',
        'text-decoration' => '(underline|line-through|none' . ((!$is_ie) ? '|blink' : '') . ')',
        'text-indent' => $enforce_length,
        'text-transform' => '(capitalize|uppercase|lowercase|none)',
        'top' => $enforce_auto_or_length,
        'unicode-bidi' => '(bidi-override|normal|embed)',
        'vertical-align' => '(baseline|sub|super|top|text-top|middle|bottom|text-bottom|' . $enforce_length . ')',
        'visibility' => '(hidden|visible|collapse)',
        'white-space' => '(normal|pre|nowrap' . ((!$is_ie) ? '|pre-wrap|pre-line' : '') . ')',
        'width' => $enforce_auto_or_length,
        'word-spacing' => $enforce_normal_or_length,
        'z-index' => '(auto|(\d+))',
        'content' => '.+',
        'quotes' => '.+ .+',
        'max-width' => '(none|' . $enforce_auto_or_length . ')',
        'min-width' => $enforce_auto_or_length,
        'max-height' => '(none|' . $enforce_auto_or_length . ')',
        'min-height' => $enforce_auto_or_length,
        'marker-offset' => $enforce_auto_or_length,
        'caption-side' => 'top|bottom|left|right',
        'empty-cells' => 'show|hide',
        'counter-increment' => $enforce_counter_increment,
        'counter-reset' => $enforce_counter_increment,
        'outline' => $enforce_border,
        'outline-color' => $enforce_transparent_or_color,
        'outline-style' => $enforce_border_style,
        'outline-width' => $enforce_border_width,
        'opacity' => $enforce_fraction,
        'overflow-x' => '(visible|hidden|scroll|auto)',
        'overflow-y' => '(visible|hidden|scroll|auto)',

        // CSS3, widely supported
        'box-sizing' => '(border-box|content-box|padding-box)', // should be vendor prefixed (for Firefox)

        // CSS3, not supported on IE8 but irrelevant as these just add flashiness; should be vendor prefixed
        'background-size' => '(cover|contain|((' . $enforce_length . ' )?' . $enforce_length . '))',
        'box-shadow' => '(none|(' . $enforce_box_shadow . '(,\s*' . $enforce_box_shadow . '(,\s*' . $enforce_box_shadow . '(,\s*' . $enforce_box_shadow . ')?)?)?))',
        'text-shadow' => '(none|(' . $enforce_length . ' ' . $enforce_length . '( ' . $enforce_length . ')?( ' . $enforce_css_color . ')?))',
        'border-radius' => $enforce_length . '( ' . $enforce_length . '( ' . $enforce_length . '( ' . $enforce_length . ')?)?)?',
        'border-top-left-radius' => $enforce_length,
        'border-top-right-radius' => $enforce_length,
        'border-bottom-left-radius' => $enforce_length,
        'border-bottom-right-radius' => $enforce_length,
        'transition-property' => $enforce_transition_property,
        'transition-duration' => $enforce_time,
        'transition-timing-function' => $enforce_transition_timing_function,
        'transition-delay' => $enforce_time,
        'transition' => $enforce_transition . '(\s*,\s*' . $enforce_transition . ')*',
        'background-origin' => '(border-box|content-box)', // padding-box not widely supported yet; may be dropped from spec
        'transform' => '(none|\w+\([^\(\)]+\))',
        'transform-origin' => $enforce_transform_origin . '( ' . $enforce_transform_origin . '( ' . $enforce_transform_origin . ')?)?',
        'transform-style' => $enforce_transform_style,
        'user-select' => '(none|auto|text|all|contain)',
        'text-overflow' => '(clip|ellipsis|\'[^\']\')',
        'touch-action' => '(auto|none|pan-x|pan-y|manipulation|pan-left|pan-right|pan-up|pan-down|pinch-zoom)',
        'pointer-events' => '(auto|none)',
        'animation-duration' => '\d+s',
        'animation-delay' => '\d+s',
        'animation-name' => '.+',
        'animation-iteration-count' => '(\d+|infinite)',
        'animation-direction' => '(normal|reverse|alternate|alternate-reverse)',
        'animation-play-state' => '(paused|running)',
        'animation-fill-mode' => '(none|forwards|backwards|both)',
        'animation-timing-func' => '(linear|ease|ease-in|ease-out|ease-in-out|step-start|step-end|steps\(\d+,(start|end)\)|cubic-bezier\(\d+,\d+,\d+,\d+\))',

        /* Purposely left out these CSS2 features due to very poor browser support (not just IE not having it) */
        /*
        (print module)
        (aural module)
        */

        /* These are non standard but we want them */
        'writing-mode' => '(tb-rl|lr-tb)', // A more complex W3C standard is underway. Only IE supports this one.
        'word-wrap' => '(normal|break-word)', // Was renamed to overflow-wrap, but that name is not supported widely
        'overflow-scrolling' => '(touch|auto)',
        'text-size-adjust' => '(none|auto|\d%|\d\d%|100%)',
    );

    global $CSS_NON_IE_PROPERTIES;
    $CSS_NON_IE_PROPERTIES = array(
        // Empty for now, as CSS is evolving fast, and the point where we're at now we don't need to use things IE does not support, or if we would like to, they're not fully standardised yet anyway
        //  In other words, we are just supporting CSS2.1 and parts of CSS3 specs in this checker. That is our baseline.
        //  There is no real 'CSS3' anyway, just a rapidly evolving series of specs with varying levels of industry and standards acceptance.
    );

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

    global $TAG_ATTRIBUTES;
    $TAG_ATTRIBUTES = array(); // Split up to workaround bug in HHVM
    $TAG_ATTRIBUTES += array(
        'a.accesskey' => $enforce_character,
        'a.charset' => '.+',
        'a.download' => '(download)',
        'a.coords' => '.+',
        'a.href' => $enforce_link,
        'a.hreflang' => $enforce_lang,
        'a.onblur' => $enforce_javascript,
        'a.onfocus' => $enforce_javascript,
        'a.rel' => '.*',
        'a.rev' => '.+',
        'a.shape' => '(rect|circle|poly|default)',
        'a.tabindex' => $enforce_inumber,
        'a.type' => '.+',
        'area.accesskey' => $enforce_character,
        'area.alt' => '.*',
        'area.coords' => '.+',
        'area.href' => $enforce_link,
        'area.nohref' => 'nohref',
        'area.onblur' => '.+',
        'area.onfocus' => $enforce_javascript,
        'area.shape' => '(rect|circle|poly|default)',
        'area.tabindex' => $enforce_inumber,
    );
    $TAG_ATTRIBUTES += array(
        'base.href' => $enforce_link,
        'blockquote.cite' => '.+',
        'body.onload' => $enforce_javascript,
        'body.onunload' => $enforce_javascript,
        'body.onafterprint' => $enforce_javascript,
        'body.onbeforeprint' => $enforce_javascript,
        'body.onbeforeunload' => $enforce_javascript,
        'body.onclose' => $enforce_javascript,
        'body.onerror' => $enforce_javascript,
        'body.onhashchange' => $enforce_javascript,
        'body.onlanguagechange' => $enforce_javascript,
        'body.onmessage' => $enforce_javascript,
        'body.onoffline' => $enforce_javascript,
        'body.ononline' => $enforce_javascript,
        'body.onpagehide' => $enforce_javascript,
        'body.onpageshow' => $enforce_javascript,
        'body.onpopstate' => $enforce_javascript,
        'body.onrejectionhandled' => $enforce_javascript,
        'body.onresize' => $enforce_javascript,
        'body.onstorage' => $enforce_javascript,
        'body.onunhandledrejection' => $enforce_javascript,
        'button.accesskey' => $enforce_character,
        'button.disabled' => 'disabled',
        'button.name' => $enforce_name,
        'button.onblur' => $enforce_javascript,
        'button.onfocus' => $enforce_javascript,
        'button.tabindex' => $enforce_inumber,
        'button.type' => '(button|submit|reset)',
        'button.value' => '.+',
    );
    $TAG_ATTRIBUTES += array(
        'col.char' => $enforce_character,
        'col.charoff' => $enforce_length,
        'col.span' => $enforce_inumber,
        'colgroup.char' => $enforce_character,
        'colgroup.charoff' => $enforce_length,
        'colgroup.span' => $enforce_inumber,
        'del.cite' => '.+',
        'del.datetime' => '.+',
        'div.xml:lang' => $enforce_lang,
    );
    $TAG_ATTRIBUTES += array(
        'form.accept-charset' => '.+',
        'form.action' => $enforce_link,
        'form.enctype' => 'multipart/form-data|application/x-www-form-urlencoded',
        'form.method' => '(get|post)',
        'form.onreset' => '.+',
        'form.onsubmit' => '.+',
    );
    $TAG_ATTRIBUTES += array(
        'html.xml:lang' => $enforce_lang,
        'html.version' => '.+',
        'html.xmlns' => '.+',
        'img.alt' => '.*', // Have to allow this really, for non-semantic images
        'img.height' => $enforce_inumber,
        'img.ismap' => 'ismap',
        'img.longdesc' => '.+',
        'img.src' => $enforce_link,
        'img.sizes' => $enforce_length,
        'img.srcset' => '(' . $enforce_link . ' \d+x( |$))*',
        'img.usemap' => '.+',
        'img.width' => $enforce_inumber,
        'img.onabort' => $enforce_javascript,
        'img.onerror' => $enforce_javascript,
        'img.onload' => $enforce_javascript,
        'embed.type' => '.*',
        'embed.height' => $enforce_inumber,
        'embed.src' => $enforce_link,
        'embed.width' => $enforce_inumber,
        'embed.onerror' => $enforce_javascript,
        'embed.onload' => $enforce_javascript,
        'input.accept' => '.+',
        'input.accesskey' => $enforce_character,
        'input.alt' => '.*',
        'input.checked' => 'checked',
        'input.disabled' => 'disabled',
        'input.maxlength' => $enforce_inumber,
        'input.name' => $enforce_name,
        'input.onblur' => '.+',
        'input.onchange' => '.+',
        'input.onfocus' => $enforce_javascript,
        'input.onselect' => '.+',
        'input.readonly' => 'readonly',
        'input.size' => '.+',
        'input.src' => $enforce_link,
        'input.tabindex' => $enforce_inumber,
        'input.type' => '(text|password|checkbox|radio|submit|reset|file|hidden|image|button|color|date|datetime|datetime-local|email|month|number|range|search|tel|time|url|week)',
        'input.usemap' => '.+',
        'input.value' => '.' . ($strict_form_accessibility ? '+' : '*'),
        'input.oninvalid' => $enforce_javascript,
        'ins.cite' => '.+',
        'ins.datetime' => '.+',
    );
    $TAG_ATTRIBUTES += array(
        'label.accesskey' => $enforce_character,
        'label.for' => $enforce_id,
        'label.onblur' => '.+',
        'label.onfocus' => $enforce_javascript,
        'legend.accesskey' => $enforce_character,
        'link.charset' => '.+',
        'link.href' => $enforce_link,
        'link.hreflang' => $enforce_lang,
        'link.media' => '.+',
        'link.rel' => '.+',
        'link.rev' => '.+',
        'link.type' => '.+',
        'link.sizes' => '.+',
    );
    $TAG_ATTRIBUTES += array(
        'meta.content' => '.*',
        'meta.http-equiv' => '[a-zA-Z].+',
        'meta.name' => '[a-zA-Z].+',
        'meta.scheme' => '.+',
        'object.archive' => '.+',
        'object.classid' => '.+',
        'object.codebase' => $enforce_link,
        'object.codetype' => '.+',
        'object.data' => $enforce_link,
        'object.declare' => 'declare',
        'object.height' => $enforce_length,
        'object.name' => $enforce_name,
        'object.standby' => '.+',
        'object.tabindex' => $enforce_inumber,
        'object.type' => '.+',
        'object.usemap' => '.+',
        'object.width' => $enforce_length,
        'object.onerror' => $enforce_javascript,
        'object.onload' => $enforce_javascript,
        'optgroup.disabled' => 'disabled',
        'optgroup.label' => '.+',
        'option.disabled' => 'disabled',
        'option.label' => '.+',
        'option.selected' => 'selected',
        'option.value' => '.*',
    );
    $TAG_ATTRIBUTES += array(
        'param.name' => $enforce_name,
        'param.type' => '.+',
        'param.value' => '.+',
        'param.valuetype' => '(data|ref|object)',
        'q.cite' => '.+',
        'script.charset' => '.+',
        'script.defer' => 'defer',
        'script.event' => '.+',
        'script.for' => '.+',
        'script.src' => $enforce_link,
        'script.type' => 'text/javascript',
        'script.onerror' => $enforce_javascript,
        'script.onload' => $enforce_javascript,
        'select.disabled' => 'disabled',
        'select.multiple' => 'multiple',
        'select.name' => $enforce_name,
        'select.onblur' => '.+',
        'select.onchange' => '.+',
        'select.onfocus' => $enforce_javascript,
        'select.size' => $enforce_inumber,
        'select.tabindex' => $enforce_inumber,
        'span.xml:lang' => $enforce_lang,
        'style.media' => '.+',
        'style.type' => 'text/css',
        'style.onerror' => $enforce_javascript,
        'style.onload' => $enforce_javascript,
    );
    $TAG_ATTRIBUTES += array(
        'table.frame' => '(void|above|below|hsides|lhs|rhs|vsides|box|border)',
        'table.rules' => '(none|groups|rows|cols|all)',
        //'table.summary' => '.*', Not in HTML5
        'tbody.char' => $enforce_character,
        'tbody.charoff' => $enforce_length,
        'td.axis' => '.+',
        'td.char' => $enforce_character,
        'td.charoff' => $enforce_length,
        'td.colspan' => $enforce_inumber,
        'td.headers' => '.+',
        'td.rowspan' => $enforce_inumber,
        'td.scope' => '(row|col|rowgroup|colgroup)',
        'textarea.accesskey' => $enforce_character,
        'textarea.cols' => $enforce_inumber,
        'textarea.disabled' => 'disabled',
        'textarea.name' => $enforce_name,
        'textarea.onblur' => '.+',
        'textarea.onchange' => '.+',
        'textarea.onfocus' => $enforce_javascript,
        'textarea.onselect' => '.+',
        'textarea.readonly' => 'readonly',
        'textarea.rows' => $enforce_inumber,
        'textarea.tabindex' => $enforce_inumber,
        'textarea.oninvalid' => $enforce_javascript,
        'tfoot.char' => $enforce_character,
        'tfoot.charoff' => $enforce_length,
        'th.axis' => '.+',
        'th.char' => $enforce_character,
        'th.charoff' => $enforce_length,
        'th.colspan' => $enforce_inumber,
        'th.headers' => '.+',
        'th.rowspan' => $enforce_inumber,
        'th.scope' => '(row|col|rowgroup|colgroup)',
        'thead.char' => $enforce_character,
        'thead.charoff' => $enforce_length,
        'tr.char' => $enforce_character,
        'tr.charoff' => $enforce_length,
    );
    $TAG_ATTRIBUTES += array(
        'map.name' => $enforce_name,

        // Below brought back in in modules (target, iframe) / fully in HTML5
        'a.target' => '.+',
        //'area.target' => '.+',
        'base.target' => '.+',
        'form.target' => '.+',
        'iframe.longdesc' => '.+',
        'iframe.name' => $enforce_name,
        'iframe.src' => $enforce_link,

        // These are needed in IE, so we will have to browser sniff and output if IE being used, but not check them as okay
        //'iframe.scrolling' => '(yes|no|auto)',
        //'iframe.frameborder' => '(1|0)',
        //'iframe.marginheight' => $enforce_pixels,
        //'iframe.marginwidth' => $enforce_pixels,
    );
    $TAG_ATTRIBUTES += array(
        '*.hidden' => '(hidden)',
        '*.class' => $enforce_class,
        '*.dir' => $enforce_direction,
        '*.id' => $enforce_id,
        '*.translate' => '(yes|no)',
        '*.lang' => $enforce_lang,
        '*.onclick' => $enforce_javascript,
        '*.ondblclick' => $enforce_javascript,
        '*.onkeydown' => $enforce_javascript,
        '*.onkeypress' => $enforce_javascript,
        '*.onkeyup' => $enforce_javascript,
        '*.onmousedown' => $enforce_javascript,
        '*.onmousemove' => $enforce_javascript,
        '*.onmouseout' => $enforce_javascript,
        '*.onmouseover' => $enforce_javascript,
        '*.onmouseup' => $enforce_javascript,
        '*.oncontextmenu' => $enforce_javascript,
        '*.oncopy' => $enforce_javascript,
        '*.oncut' => $enforce_javascript,
        '*.ondrag' => $enforce_javascript,
        '*.ondragend' => $enforce_javascript,
        '*.ondragenter' => $enforce_javascript,
        '*.ondragexit' => $enforce_javascript,
        '*.ondragleave' => $enforce_javascript,
        '*.ondragover' => $enforce_javascript,
        '*.ondragstart' => $enforce_javascript,
        '*.ondrop' => $enforce_javascript,
        '*.oninput' => $enforce_javascript,
        '*.onmouseenter' => $enforce_javascript,
        '*.onmouseleave' => $enforce_javascript,
        '*.onpaste' => $enforce_javascript,
        '*.onpause' => $enforce_javascript,
        '*.onscroll' => $enforce_javascript,
        '*.onwheel' => $enforce_javascript,
        '*.style' => '.*',
        '*.title' => '.*',
        'aria-autocomplete' => '(true|false)',
        'aria-checked' => '(true|false)',
        'aria-disabled' => '(true|false)',
        'aria-expanded' => '(true|false)',
        'aria-haspopup' => '(true|false)',
        'aria-hidden' => '(true|false)',
        'aria-invalid' => '(true|false)',
        'aria-label' => $enforce_id,
        'aria-level' => $enforce_number,
        'aria-multiline' => '(true|false)',
        'aria-multiselectable' => '(true|false)',
        'aria-orientation' => '(scrollbar|separator|slider)',
        'aria-pressed' => '(true|false)',
        'aria-readonly' => '(true|false)',
        'aria-required' => '(true|false)',
        'aria-selected' => '(true|false)',
        'aria-sort' => '(ascending|descending)',
        'aria-valuemax' => '.*',
        'aria-valuemin' => '.*',
        'aria-valuenow' => '.*',
        'aria-valuetext' => '.*',
        'aria-atomic' => '(true|false)',
        'aria-busy' => '(true|false)',
        'aria-live' => '(true|false)',
        'aria-relevant' => '(true|false)',
        'aria-dropeffect' => '(copy|move|link|execute|popup|none)',
        'aria-grabbed' => '(true|false)',
        'aria-activedescendant' => $enforce_id,
        'aria-controls' => $enforce_id,
        'aria-describedby' => $enforce_id,
        'aria-flowto' => $enforce_id,
        'aria-labelledby' => $enforce_id,
        'aria-owns' => $enforce_id,
        'aria-posinset' => '(true|false)',
        'aria-setsize' => $enforce_number,
        // XHTML5
        'rt.rbspan' => $enforce_inumber,
    );

    global $TAG_ATTRIBUTES_REQUIRED;
    $TAG_ATTRIBUTES_REQUIRED = array(
        'base' => array('href'), // XHTML-strict
        //'html' => array('xmlns'/*, 'xml:lang' Not in XHTML5*/),
        'meta' => array('content'),
        'style' => array(/*'type'*/),
        'script' => array(/*'type'*/),
        'bdo' => array('dir'),
        'basefont' => array('size'),
        //'param' => array('name'), Not needed in XHTML strict
        'iframe' => array('src', 'title'),
        'img' => array('src', 'alt'),
        'label' => array('for'),
        'map' => array('id'),
        'area' => array('alt'),
        'form' => array('action', 'title', 'autocomplete'/*not really required but for stability we should always set it*/),
        'textarea' => array('cols', 'rows'),
        //'input' => array('value'), // accessibility, checked somewhere else
        'table' => array(/*'summary' not in html5*/),
        'optgroup' => array('label'),
    );

    // New to HTML5...
    $TAG_ATTRIBUTES += array(
        'a.media' => '.+',
        'ol.reversed' => '(reversed)',
        'fieldset.disabled' => '(disabled)',
        'fieldset.form' => $enforce_name,
        'fieldset.name' => $enforce_name,
        'form.oninput' => '.+',
        'html.manifest' => '.+',
        'input.height' => $enforce_inumber,
        'input.width' => $enforce_inumber,
        'input.step' => 'any|(-?[0-9]+(\.[0-9]+)?)',
        'input.required' => '(required)',
        'input.placeholder' => '.+', // We will allow use of this because it is progressive enhancement. Don't rely on it for core communications within Composr until we can drop IE8/IE9 support (LEGACY note)
        'input.pattern' => '.+', // Don't rely on this being supported, we have our own sanitisation API anyway
        'input.multiple' => '(multiple)',
        'input.min' => '.+',
        'input.max' => '.+',
        'input.list' => $enforce_name,
        'input.formtarget' => $enforce_name,
        'input.formnovalidate' => '(formnovalidate)',
        'input.formmethod' => '(get|post)',
        'input.formenctype' => '(application/x-www-form-urlencoded|multipart/form-data|text/plain)',
        'input.formaction' => '.+',
        'input.form' => $enforce_name,
        'input.autofocus' => '(autofocus)',
        'input.autocomplete' => '(on|off)',
        'button.formtarget' => $enforce_name,
        'button.formnovalidate' => '(formnovalidate)',
        'button.formmethod' => '(get|post)',
        'button.formenctype' => '(application/x-www-form-urlencoded|multipart/form-data|text/plain)',
        'button.formaction' => '.+',
        'button.form' => $enforce_name,
        'label.form' => $enforce_name,
        'link.sizes' => '.+',
        'video.audio' => '(muted)',
        'video.autoplay' => '(autoplay)',
        'video.controls' => '(controls)',
        'video.height' => $enforce_length,
        'video.loop' => '(loop)',
        'video.poster' => '.*',
        'video.preload' => '(auto|metadata|none)',
        'video.src' => $enforce_link,
        'video.width' => $enforce_length,
        'video.onabort' => $enforce_javascript,
        'video.oncanplay' => $enforce_javascript,
        'video.oncanplaythrough' => $enforce_javascript,
        'video.ondurationchange' => $enforce_javascript,
        'video.onemptied' => $enforce_javascript,
        'video.onended' => $enforce_javascript,
        'video.onerror' => $enforce_javascript,
        'video.onload' => $enforce_javascript,
        'video.onloadeddata' => $enforce_javascript,
        'video.onloadedmetadata' => $enforce_javascript,
        'video.onloadstart' => $enforce_javascript,
        'video.onplay' => $enforce_javascript,
        'video.onplaying' => $enforce_javascript,
        'video.onprogress' => $enforce_javascript,
        'video.onratechange' => $enforce_javascript,
        'video.onseeked' => $enforce_javascript,
        'video.onseeking' => $enforce_javascript,
        'video.onstalled' => $enforce_javascript,
        'video.onsuspend' => $enforce_javascript,
        'video.ontimeupdate' => $enforce_javascript,
        'video.onvolumechange' => $enforce_javascript,
        'video.onwaiting' => $enforce_javascript,
        'time.datetime' => '.*',
        'source.media' => '.*',
        'source.src' => $enforce_link,
        'source.type' => '.*',
        'progress.max' => $enforce_number,
        'progress.value' => $enforce_number,
        'output.for' => $enforce_id,
        'output.form' => $enforce_name,
        'output.name' => $enforce_name,
        'meter.form' => $enforce_name,
        'meter.high' => $enforce_number,
        'meter.low' => $enforce_number,
        'meter.max' => $enforce_number,
        'meter.min' => $enforce_number,
        'meter.optimum' => $enforce_number,
        'meter.value' => $enforce_number,
        'keygen.autofocus' => '(autofocus)',
        'keygen.challenge' => '(challenge)',
        'keygen.disabled' => '(disabled)',
        'keygen.keytype' => '(rsa|other)',
        'keygen.name' => $enforce_name,
        'command.checked' => '(checked)',
        'command.disabled' => '(disabled)',
        'command.icon' => '.+',
        'command.label' => $enforce_id,
        'command.radiogroup' => $enforce_name,
        'command.type' => '(checkbox|command|radio)',
        'canvas.height' => $enforce_length,
        'canvas.width' => $enforce_length,
        'audio.autoplay' => '(autoplay)',
        'audio.controls' => '(controls)',
        'audio.loop' => '(loop)',
        'audio.preload' => '(auto|metadata|none)',
        'audio.src' => $enforce_link,
        'audio.onabort' => $enforce_javascript,
        'audio.oncanplay' => $enforce_javascript,
        'audio.oncanplaythrough' => $enforce_javascript,
        'audio.ondurationchange' => $enforce_javascript,
        'audio.onemptied' => $enforce_javascript,
        'audio.onended' => $enforce_javascript,
        'audio.onerror' => $enforce_javascript,
        'audio.onload' => $enforce_javascript,
        'audio.onloadeddata' => $enforce_javascript,
        'audio.onloadedmetadata' => $enforce_javascript,
        'audio.onloadstart' => $enforce_javascript,
        'audio.onplay' => $enforce_javascript,
        'audio.onplaying' => $enforce_javascript,
        'audio.onprogress' => $enforce_javascript,
        'audio.onratechange' => $enforce_javascript,
        'audio.onseeked' => $enforce_javascript,
        'audio.onseeking' => $enforce_javascript,
        'audio.onstalled' => $enforce_javascript,
        'audio.onsuspend' => $enforce_javascript,
        'audio.ontimeupdate' => $enforce_javascript,
        'audio.onvolumechange' => $enforce_javascript,
        'audio.onwaiting' => $enforce_javascript,
        'track.src' => $enforce_link,
        'track.lang' => $enforce_lang,
        'track.label' => '.*',
        'track.default' => '(default)',
        'track.kind' => '(subtitles|captions|descriptions|chapters|metadata)',
        'track.oncuechange' => $enforce_javascript,
        'meta.charset' => '.+',
        'meta.property' => '[a-zA-Z].+',
        'object.form' => $enforce_name,
        'script.async' => '(async)',
        'select.required' => '(required)',
        'select.autofocus' => '(autofocus)',
        'select.form' => $enforce_name,
        'style.scoped' => '(scoped)',
        'textarea.maxlength' => $enforce_inumber,
        'textarea.rows' => $enforce_inumber,
        'textarea.required' => '(required)',
        'textarea.form' => $enforce_name,
        'textarea.dirname' => $enforce_name,
        'textarea.placeholder' => '.+',
        'textarea.autofocus' => '(autofocus)',
        'textarea.wrap' => '(hard|soft)',
        'menu.label' => '.*',
        'menu.type' => '(context|toolbar|list)',
        'menu.onshow' => $enforce_javascript,
        'form.autocomplete' => '(on|off)',
        'form.novalidate' => '(novalidate)',
        'details.open' => '(open)',
        'details.ontoggle' => $enforce_javascript,
        'iframe.srcdoc' => '.+',
        'iframe.sandbox' => '(allow-forms|allow-same-origin|allow-scripts|allow-top-navigation)',
        'iframe.seamless' => '(seamless)',
        '*.draggable' => '(true|false|auto)',
        '*.dropzone' => '(copy|move|link)',
        '*.hidden' => '(hidden)',
        '*.spellcheck' => '(true|false)',
        '*.contenteditable' => '(true|false)',
        '*.contextmenu' => $enforce_id,
        '*.itemscope' => '.*',
        '*.itemtype' => '.*',
        '*.itemprop' => '.*',
    );

    if (!defined('CSS_AT_RULE_BLOCK')) {
        define('CSS_AT_RULE_BLOCK', -4);
        define('CSS_AT_RULE', -3);
        define('CSS_NO_MANS_LAND', -2);
        define('CSS_EXPECTING_IDENTIFIER', -1);
        define('CSS_IN_COMMENT', 0);
        define('CSS_IN_CLASS', 1);
        define('CSS_EXPECTING_SEP_OR_IDENTIFIER_OR_CLASS', 2);
        define('CSS_IN_IDENTIFIER', 3);
        define('CSS_IN_PSEUDOCLASS_EXPRESSION', 6);

        define('_CSS_NO_MANS_LAND', 0);
        define('_CSS_IN_PROPERTY_KEY', 1);
        define('_CSS_IN_PROPERTY_BETWEEN', 2);
        define('_CSS_IN_PROPERTY_VALUE', 3);
        define('_CSS_IN_COMMENT', 4);
        define('_CSS_EXPECTING_END', 5);
    }
}

/**
 * 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 array Array of error information
 *
 * @ignore
 */
function __check_tag($tag, $attributes, $self_close, $close, $errors)
{
    global $XML_CONSTRAIN, $TAG_STACK, $ATT_STACK, $TABS_SEEN, $KEYS_SEEN, $IDS_SO_FAR, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $EXPECTING_TAG, $OUT, $POS, $LAST_A_TAG, $TAG_RANGES;

    // Dodgy mouse events.
    if ((isset($attributes['onclick'])) && (strpos($attributes['onclick'], '/*Access-note: checked*/') === false) && (strpos($attributes['onclick'], '/*Access-note: code has other activation*/') === false) && ((!isset($attributes['onmouseover'])) || (strpos($attributes['onmouseover'], 'activate_rich_semantic_tooltip') === false)) && (!isset($attributes['onkeypress'])) && (!isset($attributes['onkeydown'])) && (!isset($attributes['onkeyup'])) && (!in_array($tag, array('a', 'input', 'textarea', 'select', 'button')))) {
        $errors[] = array('WCAG_MOUSE_EVENT_UNMATCHED', 'onclick');
    }
    if ($GLOBALS['WEBSTANDARDS_MANUAL']) {
        if ((isset($attributes['onmouseover'])) && (strpos($attributes['onmouseover'], '/*Access-note: checked*/') === false) && (!isset($attributes['onfocus'])) && (in_array($tag, array('a', 'area', 'button', 'input', 'label', 'select', 'textarea')))) {
            $errors[] = array('WCAG_MOUSE_EVENT_UNMATCHED', 'onmouseover');
        }
        if ((isset($attributes['onmouseout'])) && (strpos($attributes['onmouseout'], '/*Access-note: checked*/') === false) && (!isset($attributes['onblur'])) && (in_array($tag, array('a', 'area', 'button', 'input', 'label', 'select', 'textarea')))) {
            $errors[] = array('WCAG_MOUSE_EVENT_UNMATCHED', 'onmouseout');
        }
    }

    // Unexpected tags
    if ((!is_null($EXPECTING_TAG)) && ($EXPECTING_TAG != $tag)) {
        if ($EXPECTING_TAG == 'noscript') {
            if ($GLOBALS['WEBSTANDARDS_MANUAL']) {
                $errors[] = array('MANUAL_WCAG_SCRIPT');
            }
        } else {
            $errors[] = array('XHTML_EXPECTING', $EXPECTING_TAG);
        }
    }
    $EXPECTING_TAG = null;

    // Note that we do NOT take into account display:inline, because the W3C one doesn't either - probably because 'display' implies not 'semantic'
    $tmp = _check_blockyness($tag, $attributes, $self_close, $close);
    if (!is_null($tmp)) {
        $errors = array_merge($errors, $tmp);
    }

    if (array_key_exists('xmlns', $attributes)) {
        global $UNDER_XMLNS;
        $UNDER_XMLNS = true;
    }

    // Look for unknown attributes, or bad values
    $tmp = _check_attributes($tag, $attributes, $self_close, $close);
    if (!is_null($tmp)) {
        $errors = array_merge($errors, $tmp);
    }

    if (!$close) {
        if ($GLOBALS['MAIL_MODE']) {
            if (in_array($tag, array('style', 'object', 'applet', 'embed', 'form', 'map'))) {
                $errors[] = array('MAIL_BAD_TAG', $tag);
            }
            if ($tag == 'script') {
                $errors[] = array('MAIL_JAVASCRIPT');
            }
            foreach (array_keys($attributes) as $atr) {
                if (substr(strtolower($atr), 0, 2) == 'on') {
                    $errors[] = array('MAIL_JAVASCRIPT');
                }
            }
            if (($tag == 'body') && (count($attributes) != 0) && ($attributes != array('style' => 'margin: 0'))) {
                $errors[] = array('MAIL_BODY');
            }
        }

        // Check all required attributes are here
        global $TAG_ATTRIBUTES_REQUIRED;
        if ((isset($TAG_ATTRIBUTES_REQUIRED[$tag])) && (($tag != 'html') || ($XML_CONSTRAIN))) {
            $diff = array_diff($TAG_ATTRIBUTES_REQUIRED[$tag], array_keys($attributes));
            foreach ($diff as $attribute) {
                $errors[] = array('XHTML_MISSING_ATTRIBUTE', $tag, $attribute);
            }
        }

        // Iframes and CSS sheets need external checking
        if ($GLOBALS['WEBSTANDARDS_EXT_FILES']) {
            $tmp = _check_externals($tag, $attributes, $self_close, $close);
            if (!is_null($tmp)) {
                $errors = array_merge($errors, $tmp);
            }
        }

        // Check our links are OK
        if (($tag == 'a') && (isset($attributes['href']))) {
            if ((substr($attributes['href'], 0, 5) == 'mailto:') && (strpos($attributes['href'], '&') === false) && (strpos($attributes['href'], 'unsubscribe') !== false)) {
                $errors[] = array('XHTML_SPAM');
            }
            $tmp = _check_link_accessibility($tag, $attributes, $self_close, $close);
            if (!is_null($tmp)) {
                $errors = array_merge($errors, $tmp);
            }
        }

        // Embed is a special case
        //if (($tag == 'embed') && (!$self_close)) $EXPECTING_TAG = 'noembed'; // noembed not valid in (X)HTML5

        if (($tag == 'fieldset') && (!$self_close)) {
            $EXPECTING_TAG = 'legend';
        }
    } else {
        if ($tag == 'a') {
            $LAST_A_TAG = $TAG_RANGES[count($TAG_RANGES) - 1][1];
        }
    }

    // Check our form labelling is OK
    $tmp = _check_labelling($tag, $attributes, $self_close, $close);
    if (!is_null($tmp)) {
        $errors = array_merge($errors, $tmp);
    }

    if (!$close) { // Intentionally placed after labelling is checked
        if (($tag == 'input') || ($tag == 'select')) {
            if (($GLOBALS['WEBSTANDARDS_MANUAL']) && (isset($attributes['name'])) && (stripos($GLOBALS['OUT'], 'privacy') === false)) {
                $privacy = array('dob', 'name', 'age', 'address', 'date_of_birth', 'dateofbirth', 'email', 'e_mail', 'gender', 'salutation');
                foreach ($privacy as $priv) {
                    if (stripos($attributes['name'], $priv) !== false) {
                        $errors[] = array('MANUAL_PRIVACY');
                    }
                }
            }
        }

        switch ($tag) {
            case 'meta':
                if (($GLOBALS['WEBSTANDARDS_MANUAL']) && (isset($attributes['name'])) && ($attributes['name'] == 'robots')) {
                    $errors[] = array('MANUAL_META');
                }
                if ((isset($attributes['http-equiv'])) && (isset($attributes['content'])) && (strtolower($attributes['http-equiv']) == 'content-type') && ((strpos($attributes['content'], 'text/html;') !== false) || (strpos($attributes['content'], 'application/xhtml+xml;') !== false)) && (strpos($attributes['content'], 'charset=') !== false)) {
                    $GLOBALS['FOUND_CONTENTTYPE'] = true;
                }
                if ((isset($attributes['content'])) && ($attributes['content'] != '')) {
                    if ((isset($attributes['name'])) && ($attributes['name'] == 'keywords')) {
                        $GLOBALS['FOUND_KEYWORDS'] = true;
                    }
                    if ((isset($attributes['name'])) && ($attributes['name'] == 'description')) {
                        $GLOBALS['FOUND_DESCRIPTION'] = true;
                    }
                }
                break;

            case 'blockquote':
                if ($GLOBALS['WEBSTANDARDS_MANUAL']) {
                    $errors[] = array('MANUAL_WCAG_SEMANTIC_BLOCKQUOTE');
                }
                break;

            case 'ul':
            case 'ol':
            case 'dl':
                if ($GLOBALS['WEBSTANDARDS_MANUAL']) {
                    $errors[] = array('MANUAL_WCAG_SEMANTIC_LIST');
                }
                break;

            case 'script':
                if ($GLOBALS['WEBSTANDARDS_MANUAL']) {
                    $errors[] = array('MANUAL_WCAG_ANIMATION');
                    $EXPECTING_TAG = 'noscript';
                }
                if (($GLOBALS['WEBSTANDARDS_JAVASCRIPT']) && ((!isset($attributes['type'])) || ((isset($attributes['type'])) && (($attributes['type'] == 'text/javascript') || (($attributes['type'] == 'application/x-javascript')))))) { // Validate CSS
                    if (function_exists('require_code')) {
                        require_code('webstandards_js_lint');
                    }
                    $content = substr($OUT, $POS, strpos($OUT, '</script>', $POS) - $POS); // While the </table> found may not be the closing tag to our table, we do know a <th> should occur before any such one (unless it's a really weird table layout)
                    $content = preg_replace('#((<![CDATA[)|(]]>)|(<!--)|(-->))#', '', $content);
                    $js_conformance = check_js($content, true);
                    if (is_array($js_conformance)) {
                        $errors = array_merge($errors, $js_conformance); // Some kind of error
                    }
                }
                break;

            case 'style':
                if (($GLOBALS['WEBSTANDARDS_CSS']) && ((!isset($attributes['type'])) || ((isset($attributes['type'])) && ($attributes['type'] == 'text/css')))) { // Validate CSS
                    $content = substr($OUT, $POS, strpos($OUT, '</style>', $POS) - $POS); // While the </table> found may not be the closing tag to our table, we do know a <th> should occur before any such one (unless it's a really weird table layout)
                    $content = preg_replace('#((<![CDATA[)|(]]>)|(<!--)|(-->))#', '', $content);
                    $css_conformance = _webstandards_css_sheet($content);
                    if (is_array($css_conformance)) {
                        $errors = array_merge($errors, $css_conformance); // Some kind of error
                    }
                }
                break;

            case 'area':
                global $AREA_LINKS;
                if (isset($attributes['href'])) {
                    $AREA_LINKS[@html_entity_decode($attributes['href'], ENT_QUOTES, get_charset())] = 1;
                }
                break;

            case 'base':
                global $URL_BASE;
                if (isset($attributes['href'])) {
                    $URL_BASE = @html_entity_decode($attributes['href'], ENT_QUOTES, get_charset());
                }
                break;

            case 'form':
                if ((isset($attributes['action'])) && (strpos($attributes['action'], '?') !== false) && (isset($attributes['method'])) && ($attributes['method'] == 'get')) {
                    $errors[] = array('XHTML_FORM_TYPE');
                }
                $GLOBALS['XHTML_FORM_ENCODING'] = isset($attributes['enctype']) ? $attributes['enctype'] : 'application/x-www-form-urlencoded';
                if ((isset($attributes['target'])) && ($attributes['target'] == '_blank') && ((!isset($attributes['title'])) || (strpos($attributes['title'], do_lang('LINK_NEW_WINDOW')) === false))) {
                    $errors[] = array('WCAG_BLANK');
                }
                if (($GLOBALS['XHTML_FORM_ENCODING'] == 'multipart/form-data') && (array_key_exists('method', $attributes)) && ($attributes['method'] == 'get')) {
                    $errors[] = array('XHTML_FORM_ENCODING_2');
                }
            // intentionally rolls on...

            case 'map':
            case 'iframe':
            case 'object': // Check that our 'name' attributes for frames etc are unique between selves and against IDs
                if (isset($attributes['name'])) {
                    global $ANCHORS_SEEN;
                    if (isset($ANCHORS_SEEN[$attributes['name']])) {
                        $errors[] = array('XHTML_A_NAME', $tag);
                    } else {
                        $ANCHORS_SEEN[$attributes['name']] = 1;
                    }

                    if ((!isset($attributes['id'])) || ((isset($attributes['id'])) && ($attributes['id'] != $attributes['name']))) {
                        $errors[] = array('XHTML_NAME_ID_DEPRECATED');
                    }
                }
                break;

            case 'input':
                if (isset($attributes['type'])) {
                    // Special case for missing 'name' in form elements
                    if (($attributes['type'] != 'image') && ($attributes['type'] != 'submit') && ($attributes['type'] != 'button') && ($attributes['type'] != 'reset')) {
                        if (!isset($attributes['name'])) {
                            $errors[] = array('XHTML_MISSING_ATTRIBUTE', $tag, 'name');
                        }
                    }

                    if ((isset($attributes['size'])) && (isset($attributes['type'])) && (($attributes['type'] == 'week') || ($attributes['type'] == 'hidden')/* || ($attributes['type'] == 'color') || ($attributes['type'] == 'number') Size would apply if browser is using non-HTML5 fallback*/ || ($attributes['type'] == 'month') || ($attributes['type'] == 'range') || ($attributes['type'] == 'radio') || ($attributes['type'] == 'checkbox'))) {
                        $errors[] = array('XHTML_NO_SIZE_FOR');
                    }

                    if (($attributes['type'] == 'image') && (!isset($attributes['alt']))) {
                        $errors[] = array('XHTML_MISSING_ATTRIBUTE', 'input', 'alt');
                    }

                    if (($attributes['type'] == 'checkbox') && (isset($attributes['id']))) {
                        $pre_content = substr($OUT, 0, $POS);
                        if (preg_match('#<label for="' . preg_quote($attributes['id'], '#') . '">[^:]+<input[^<>]+id="' . preg_quote($attributes['id'], '#') . '"#', $pre_content) != 0) {
                            //$errors[] = array('ACCESSIB_COLONS_IN_PRE_LABELS');   Annoying
                        }
                    }

                    if ($attributes['type'] == 'file') {
                        if (isset($attributes['value'])) {
                            $errors[] = array('XHTML_FILE_VALUE');
                        }
                        if (($GLOBALS['XHTML_FORM_ENCODING'] != 'multipart/form-data') && ($GLOBALS['XHTML_FORM_ENCODING'] != '')) {
                            $errors[] = array('XHTML_FORM_ENCODING');
                        }
                    } elseif (($attributes['type'] == 'text') && (!isset($attributes['value']))) {
                        $errors[] = array('XHTML_MISSING_ATTRIBUTE', $tag, 'value');
                    }
                }
                break;

            case 'select':
                $webstandards_check = function_exists('get_param_integer') ? get_param_integer('keep_webstandards_check', get_param_integer('webstandards_check', 0)) : 0;
                if ((isset($attributes['onchange'])) && (strpos($attributes['onchange'], 'form.submit()') !== false) && (strpos($attributes['onchange'], '/*guarded*/') === false) && (($webstandards_check == 0) || (!has_js()))) {
                    $errors[] = array('WCAG_AUTO_SUBMIT_LIST');
                }
                break;

            case 'table':
                if ((isset($attributes['summary'])) && (($attributes['summary'] == do_lang('SPREAD_TABLE')) || ($attributes['summary'] == do_lang('MAP_TABLE')))) {
                    $content = strtolower(substr($OUT, $POS, strpos($OUT, '</table>', $POS) - $POS)); // While the </table> found may not be the closing tag to our table, we do know a <th> should occur before any such one (unless it's a really weird table layout)
                    $th_count = substr_count($content, '<th');
                    if (($th_count == 0) && (trim($content) != 'x')) {
                        $errors[] = array('WCAG_MISSING_TH');
                    } else {
                        if (strpos($content, '<thead') === false) {
                            $tr_count = substr_count($content, '<tr');
                            if ($th_count > $tr_count) {
                                $errors[] = array('WCAG_HD_SPECIAL');
                            }
                        }
                    }
                }
                break;

            case 'thead':
                $array_pos = array_search('table', array_reverse($TAG_STACK));
                if ($array_pos !== false) {
                    $array_pos = count($TAG_STACK) - $array_pos - 1;
                }
                if (($array_pos !== false) && (isset($ATT_STACK[$array_pos]['summary'])) && ($ATT_STACK[$array_pos]['summary'] == '')) {
                    $errors[] = array('WCAG_BAD_LAYOUT_TABLE');
                }
                break;

            case 'tfoot':
                $array_pos = array_search('table', array_reverse($TAG_STACK));
                if ($array_pos !== false) {
                    $array_pos = count($TAG_STACK) - $array_pos - 1;
                }
                if (($array_pos !== false) && (isset($ATT_STACK[$array_pos]['summary'])) && ($ATT_STACK[$array_pos]['summary'] == '')) {
                    $errors[] = array('WCAG_BAD_LAYOUT_TABLE');
                }
                break;

            case 'th':
                $array_pos = array_search('table', array_reverse($TAG_STACK));
                if ($array_pos !== false) {
                    $array_pos = count($TAG_STACK) - $array_pos - 1;
                }
                if (($array_pos !== false) && (isset($ATT_STACK[$array_pos]['summary'])) && ($ATT_STACK[$array_pos]['summary'] == '')) {
                    $errors[] = array('WCAG_BAD_LAYOUT_TABLE');
                }

                /* We used to enforce th length for accessibility, but this is impractical since 'abbr' attribute was dropped in HTML5
                if (!isset($attributes['abbr'])) {
                    $content = trim(substr($OUT, $POS, strpos($OUT, '</th>', $POS) - $POS)); // This isn't perfect - In theory a th could contain a table itself: but it's not very semantic if it does
                    if (strlen(trim(@html_entity_decode(strip_tags($content), ENT_QUOTES, get_charset()))) > 40) {
                        $errors[] = array('WCAG_TH_TOO_LONG');
                    }
                }
                */
                break;

            case 'a':
                // Handle empty tag check for <a> (couldn't handle with normal case due to complexity)
                if ((!isset($attributes['id'])) && (!isset($attributes['title'])) && (substr($OUT, $POS, 4) == '</a>')) {
                    $errors[] = array('XHTML_EMPTY_TAG', $tag);
                }
                break;

            case 'img':
                if (($GLOBALS['WEBSTANDARDS_MANUAL']) && (!isset($attributes['width']))) {
                    $errors[] = array('XHTML_WIDTH');
                }
                if ((isset($attributes['longdesc'])) && (!isset($attributes['dlink']))) {
                    $errors[] = array('WCAG_LONGTEXT_DLINK');
                }
                if ((isset($attributes['alt'])) && (isset($attributes['src'])) && ($attributes['alt'] != '') && ($attributes['alt'] == $attributes['src'])) {
                    $errors[] = array('XHTML_MISSING_ATTRIBUTE', 'img', 'alt');
                }
                break;
        }

        /*if (($tag[0]=='h') && (is_numeric(substr($tag,1))))   Excessive check, heading order gaps are okay as long as order is still logical
        {
            global $LAST_HEADING;
            if ($LAST_HEADING < intval(substr($tag, 1)) - 1) {
                $errors[] = array('WCAG_HEADING_ORDER');
            }
            $LAST_HEADING = intval(substr($tag, 1));
        }*/

        if (isset($attributes['accesskey'])) {
            $this_href = isset($attributes['href']) ? $attributes['href'] : uniqid('', true);
            if ((isset($KEYS_SEEN[$attributes['accesskey']])) && ($KEYS_SEEN[$attributes['accesskey']] != $this_href)) {
                $errors[] = array('WCAG_ACCESSKEY_UNIQUE');
            }
            $KEYS_SEEN[$attributes['accesskey']] = $this_href;
        }
        if (isset($attributes['tabindex'])) {
            if ((in_array($attributes['tabindex'], $TABS_SEEN)) && ($attributes['tabindex'] != 'x')) {
                $last = array_pop($TABS_SEEN);
                if ($last != $attributes['tabindex']) { // We do allow repeating of tabindexes as long as they are next to each other
                    $errors[] = array('WCAG_TABINDEX_UNIQUE');
                } else {
                    array_push($TABS_SEEN, $last);
                }
            }
            $TABS_SEEN[] = $attributes['tabindex'];
        }
    }

    return $errors;
}

/**
 * Checks a tag's inline/block/normal nesting situations.
 *
 * @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
 * @return ?list Array of errors (null: none)
 * @ignore
 */
function _check_blockyness($tag, $attributes, $self_close, $close)
{
    global $THE_DOCTYPE, $BLOCK_CONSTRAIN, $XML_CONSTRAIN, $TAGS_DEPRECATE_ALLOW, $PARENT_TAG, $TAGS_INLINE, $TAGS_BLOCK, $TAGS_NORMAL, $TAGS_INLINE_DEPRECATED, $TAGS_BLOCK_DEPRECATED, $TAGS_NORMAL_DEPRECATED, $IDS_SO_FAR, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $EXPECTING_TAG, $OUT, $POS, $LAST_A_TAG, $UNDER_XMLNS;

    $errors = array();

    $dif = $close ? -1 : 1;
    if ($self_close) {
        $dif = 0;
    }
    if ((isset($TAGS_BLOCK[$tag])) || (isset($TAGS_BLOCK_DEPRECATED[$tag]))) {
        if (($ANCESTOR_INLINE != 0) && ($BLOCK_CONSTRAIN)) {
            $errors[] = array('XHTML_ANCESTOR_BLOCK_INLINE', $tag);
        }
        $ANCESTOR_BLOCK += $dif;
        if (isset($TAGS_BLOCK_DEPRECATED[$tag])) {
            $errors[] = array($TAGS_DEPRECATE_ALLOW ? 'XHTML_DEPRECATED_TAG' : 'XHTML_UNKNOWN_TAG', $tag);
        }
    } elseif ((isset($TAGS_INLINE[$tag])) || (isset($TAGS_INLINE_DEPRECATED[$tag]))) {
        //if (($BLOCK_CONSTRAIN) && ($PARENT_TAG != 'span') && ((isset($TAGS_NORMAL[$PARENT_TAG])) || ((isset($TAGS_NORMAL_DEPRECATED[$PARENT_TAG]))))) $errors[] = array('XHTML_ANCESTOR_INLINE_NORMAL', $tag); This restriction isn't really a proper one, some checkers seem to have it but it is not used anymore (XHTML5+) and pretty silly
        if ($tag != 'label') {
            $ANCESTOR_INLINE += $dif;
        }
        if (isset($TAGS_INLINE_DEPRECATED[$tag])) {
            $errors[] = array($TAGS_DEPRECATE_ALLOW ? 'XHTML_DEPRECATED_TAG' : 'XHTML_UNKNOWN_TAG', $tag);
        }
    } elseif ((isset($TAGS_NORMAL[$tag])) || (isset($TAGS_NORMAL_DEPRECATED[$tag]))) {
        if ($tag == 'title') {
            $ANCESTOR_BLOCK += $dif;
        }
        if (($tag == 'iframe') && (($THE_DOCTYPE == DOCTYPE_XHTML_STRICT) || ($THE_DOCTYPE == DOCTYPE_XHTML_11))) {
            $errors[] = array('XHTML_UNKNOWN_TAG', $tag);
        }
        if (isset($TAGS_NORMAL_DEPRECATED[$tag])) {
            $errors[] = array($TAGS_DEPRECATE_ALLOW ? 'XHTML_DEPRECATED_TAG' : 'XHTML_UNKNOWN_TAG', $tag);
        }
    } elseif (!$close) {
        if (!$UNDER_XMLNS) {
            $errors[] = array('XHTML_UNKNOWN_TAG', $tag);
        }
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks a tag's attributes.
 *
 * @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
 * @return ?list Array of errors (null: none)
 * @ignore
 */
function _check_attributes($tag, $attributes, $self_close, $close)
{
    global $PSPELL_LINK, $THE_LANGUAGE, $XML_CONSTRAIN, $TAGS_DEPRECATE_ALLOW, $THE_DOCTYPE, $HYPERLINK_URLS, $CRAWLED_URLS, $EMBED_URLS, $TAGS_INLINE, $TAGS_BLOCK, $TAGS_NORMAL, $TAGS_INLINE_DEPRECATED, $TAGS_BLOCK_DEPRECATED, $TAGS_NORMAL_DEPRECATED, $TAG_ATTRIBUTES, $IDS_SO_FAR, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $EXPECTING_TAG, $OUT, $POS, $LAST_A_TAG, $TAG_ATTRIBUTES_REQUIRED;

    $errors = array();

    $stub = $tag . '.';
    foreach ($attributes as $attribute => $value) {
        $lattribute = strtolower($attribute);
        if ($lattribute != $attribute) {
            if ($XML_CONSTRAIN) {
                $errors[] = array('XHTML_CASE_ATTRIBUTE', $tag, $attribute);
            }
            $attribute = $lattribute;
        }

        if (($attribute == 'lang') || ($attribute == 'xml:lang')) {
            $THE_LANGUAGE = $value;
        }

        if (($GLOBALS['WEBSTANDARDS_MANUAL']) && (($value == 'TODO') || (strpos($value, 'Lorem ') !== false))) {
            $errors[] = array('XHTML_PLACEHOLDER');
        }

        if ((!isset($TAG_ATTRIBUTES[$stub . $attribute])) && (!isset($TAG_ATTRIBUTES['*.' . $attribute])) && (!isset($TAG_ATTRIBUTES_REQUIRED[$stub . $attribute]))) {
            if ((!isset($TAGS_BLOCK[$tag])) && (!isset($TAGS_INLINE[$tag])) && (!isset($TAGS_NORMAL[$tag]))) {
                continue;
            }
            if ((!isset($TAGS_BLOCK_DEPRECATED[$tag])) && (!isset($TAGS_INLINE_DEPRECATED[$tag])) && (!isset($TAGS_NORMAL_DEPRECATED[$tag]))) {
                continue;
            }
            if (strpos($attribute, ':') !== false) {
                continue;
            }
            if (substr($attribute, 0, 5) == 'data-') {
                continue;
            }

            //if ($tag == 'embed') continue; // Hack, to allow rich media to work in multiple browsers. Not needed now that <object> tag is quite stable.
            $errors[] = array('XHTML_UNKNOWN_ATTRIBUTE', $tag, $attribute);
            continue;
        } else {
            if (isset($TAG_ATTRIBUTES_REQUIRED[$stub . $attribute])) {
                $errors[] = array($TAGS_DEPRECATE_ALLOW ? 'XHTML_DEPRECATED_ATTRIBUTE' : 'XHTML_UNKNOWN_ATTRIBUTE', $tag, $attribute);
            }

            if (($attribute == 'target') && (($THE_DOCTYPE == DOCTYPE_XHTML_STRICT) || ($THE_DOCTYPE == DOCTYPE_XHTML_11))) {
                $errors[] = array('XHTML_UNKNOWN_ATTRIBUTE', $tag, $attribute);
            }
        }

        if ((($attribute == 'alt') || ($attribute == 'title') || (($attribute == 'content') && (array_key_exists('http-equiv', $attributes)) && ((strtolower($attributes['http-equiv']) == 'description') || (strtolower($attributes['http-equiv']) == 'keywords'))) || ($attribute == 'summary')) && (function_exists('pspell_new')) && (isset($GLOBALS['SPELLING'])) && ($value != '')) {
            $_value = @html_entity_decode($value, ENT_QUOTES, get_charset());
            $errors = array_merge($errors, check_spelling($_value));
        }

        //if (($attribute == 'alt') && ($tag != 'input') && (strlen(strip_tags($value)) > 150)) $errors[] = array('WCAG_ATTRIBUTE_TOO_LONG', $attribute);

        if (($attribute == 'href') || ($attribute == 'src') || (($attribute == 'data') && ($tag == 'object'))) {
            $CRAWLED_URLS[] = @html_entity_decode($value, ENT_QUOTES, get_charset());
            if ($tag == 'a') {
                $HYPERLINK_URLS[] = @html_entity_decode($value, ENT_QUOTES, get_charset());
            }
        }
        if ((($attribute == 'src') && ($tag == 'embed')) || (($attribute == 'src') && ($tag == 'script')) || (($attribute == 'src') && ($tag == 'iframe')) || (($attribute == 'src') && ($tag == 'img')) || (($attribute == 'href') && ($tag == 'link') && (isset($attributes['rel'])) && ($attributes['rel'] == 'stylesheet')) || (($attribute == 'data') && ($tag == 'object')) || (($attribute == 'code') && ($tag == 'applet'))) {
            $EMBED_URLS[] = @html_entity_decode($value, ENT_QUOTES, get_charset());
        }

        if (($attribute == 'href') && (@strtolower(@$value[0]) == 'j') && (strtolower(substr($value, 0, 11)) == 'javascript:')) {
            $errors[] = array('XHTML_BAD_ATTRIBUTE_VALUE', $attribute, $value, 'no js href');
        }

        if (isset($TAG_ATTRIBUTES[$stub . $attribute])) {
            $reg_exp = $TAG_ATTRIBUTES[$stub . $attribute];
        } else {
            $reg_exp = $TAG_ATTRIBUTES['*.' . $attribute];
        }

        if (($reg_exp != '(.|\n)*') && (preg_match('#^' . $reg_exp . '$#s', $value) == 0) && ($value != 'x')) {
            $errors[] = array('XHTML_BAD_ATTRIBUTE_VALUE', $attribute, $value, $reg_exp);
        }

        if (($attribute == 'style') && ($GLOBALS['WEBSTANDARDS_CSS'])) { // Validate CSS
            if ((!function_exists('do_template')) && (strpos($value, '{') === false) && (strpos($value, 'float:') === false) && (strpos($value, ': none') === false) && (strpos($value, ': inline') === false) && (strpos($value, ': block') === false)) {
                $errors[] = array('CSS_INLINE_STYLES');
            }

            $css_conformance = _webstandards_css_class($value, 0);
            if (is_array($css_conformance)) {
                $errors = array_merge($errors, $css_conformance); // Some kind of error
            }
        }

        if ($attribute == 'id') { // Check we don't have duplicate IDs
            if (isset($IDS_SO_FAR[strtolower($value)])) { // strtolower is for IE - in reality, IDs are not meant to be case insensitive
                $errors[] = array('XHTML_DUPLICATED_ID', $value);
            }
            $IDS_SO_FAR[strtolower($value)] = 1;
        }
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks the spelling of some text.
 *
 * @param  string $value The text
 * @return array Array of errors
 */
function check_spelling($value)
{
    global $THE_LANGUAGE;
    $lang = strtolower($THE_LANGUAGE);

    require_code('spelling');
    $misspellings = run_spellcheck($value, $lang);

    $errors = array();
    foreach (array_keys($misspellings) as $word) {
        $errors[] = array('XHTML_SPELLING', $word);
    }
    return $errors;
}

/**
 * Checks the content under a tag's external references.
 *
 * @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
 * @return ?list Array of errors (null: none)
 * @ignore
 */
function _check_externals($tag, $attributes, $self_close, $close)
{
    if ((function_exists('get_param_integer')) && (get_param_integer('keep_no_ext_check', 0) == 1)) {
        return null;
    }

    unset($self_close);
    unset($close);

    global $VALIDATED_ALREADY, $IDS_SO_FAR, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $EXPECTING_TAG, $OUT, $POS, $LAST_A_TAG;

    $errors = array();

    if (($tag == 'link') && ($GLOBALS['WEBSTANDARDS_CSS']) && (!$GLOBALS['NO_XHTML_LINK_FOLLOW']) && (isset($attributes['href'])) && (isset($attributes['type'])) && ($attributes['type'] == 'text/css') && (!isset($VALIDATED_ALREADY[$attributes['href']]))) { // Validate CSS
        $VALIDATED_ALREADY[$attributes['href']] = 1;
        $url = qualify_url($attributes['href'], $GLOBALS['URL_BASE']);
        if ($url != '') {
            $sheet = http_download_file($url, null, false);
            if (!is_null($sheet)) {
                $css_conformance = _webstandards_css_sheet($sheet);
                if (is_array($css_conformance)) {
                    $errors = array_merge($errors, $css_conformance); // Some kind of error
                }
            }
        }
    }

    if (($GLOBALS['WEBSTANDARDS_JAVASCRIPT']) && ($tag == 'script') && (!$GLOBALS['NO_XHTML_LINK_FOLLOW']) && (isset($attributes['src'])) && ((!isset($attributes['type'])) || (isset($attributes['type'])) && (($attributes['type'] == 'text/javascript') || ($attributes['type'] == 'application/x-javascript'))) && (!isset($VALIDATED_ALREADY[$attributes['src']]))) { // Validate CSS
        $VALIDATED_ALREADY[$attributes['src']] = 1;
        $url = qualify_url($attributes['src'], $GLOBALS['URL_BASE']);
        if ($url != '') {
            $js = http_download_file($url, null, false);
            if (!is_null($js)) {
                require_code('character_sets');

                $js = convert_to_internal_encoding($js);

                $VALIDATED_ALREADY[$attributes['src']] = 1;

                if (function_exists('require_code')) {
                    require_code('webstandards_js_lint');
                }
                $js_conformance = check_js($js, true);
                if (is_array($js_conformance)) {
                    $errors = array_merge($errors, $js_conformance); // Some kind of error
                }
            }
        }
    }

    if (($tag == 'iframe') && (isset($attributes['src'])) && ($attributes['src'] != '') && (!$GLOBALS['NO_XHTML_LINK_FOLLOW']) && (!isset($VALIDATED_ALREADY[$attributes['src']]))) { // Validate iframe's
        $VALIDATED_ALREADY[$attributes['src']] = 1;
        $url = qualify_url($attributes['src'], $GLOBALS['URL_BASE']);
        if ($url != '') {
            $iframe = http_download_file($url, null, false);             //   Sometimes disabled due to my iframe producing a weird PHP exception, that was stopping me working
            if ((!is_null($iframe)) && ($iframe != '')) {
                require_code('character_sets');

                $iframe = convert_to_internal_encoding($iframe);

                global $HTTP_DOWNLOAD_MIME_TYPE;
                if (($HTTP_DOWNLOAD_MIME_TYPE == 'text/html') || ($HTTP_DOWNLOAD_MIME_TYPE == 'application/xhtml+xml')) {
                    global $EXTRA_CHECK;
                    $EXTRA_CHECK[] = $iframe;
                }
            }
        }
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks link accessibility.
 *
 * @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
 * @return ?list Array of errors (null: none)
 * @ignore
 */
function _check_link_accessibility($tag, $attributes, $self_close, $close)
{
    global $IDS_SO_FAR, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $EXPECTING_TAG, $OUT, $POS, $LAST_A_TAG, $TAG_RANGES, $WEBSTANDARDS_MANUAL;

    $errors = array();

    // Check positioning - not anymore "until user agents"
    /*
    if ((!is_null($LAST_A_TAG)) && (isset($attributes['href']))) {
        $between = substr($OUT, $LAST_A_TAG + 1, $TAG_RANGES[count($TAG_RANGES) - 1][0] - $LAST_A_TAG - 2);
        $between = str_replace('&nbsp;', ' ', $between);
        $between = strip_tags($between, '<li><td><img><hr><br><p><th>');
        if (trim($between) == '') {
            $errors[] = array('WCAG_ADJACENT_LINKS');
        }
    }
    */

    // Check captioning
    global $A_LINKS;
    if (!isset($attributes['title'])) {
        $title = '';
    } else {
        $title = $attributes['title'];
    }
    $content = strtolower(substr($OUT, $POS, strpos($OUT, '</a>', $POS) - $POS));
    if ((isset($attributes['target'])) && ($attributes['target'] == '_blank') && (function_exists('do_lang')) && (strpos($content, do_lang('LINK_NEW_WINDOW')) === false) && (strpos($title, do_lang('LINK_NEW_WINDOW')) === false)) {
        $errors[] = array('WCAG_BLANK');
    }
    if (substr($content, 0, 4) != '<img') {
        $filtered_href = str_replace('/index.php', '', $attributes['href']);
        $filtered_href = preg_replace('#&keep_session=[^&]*#', '', $filtered_href);

        if (($WEBSTANDARDS_MANUAL) && (isset($A_LINKS[$title])) && (isset($A_LINKS[$title][$content])) && ($A_LINKS[$title][$content] != $attributes['href']) && ($A_LINKS[$title][$content] != $filtered_href)) {
            $errors[] = array('WCAG_DODGY_LINK', $A_LINKS[$title][$content]);
        }
        $bad_strings = array('click'/*,'here'*/);
        $_content = strip_tags($content);
        if (trim($_content) != $_content) {
            $errors[] = array('XHTML_A_SPACES');
        }
        if (($_content == $content) && (strlen($content) < 12)) {
            $in_strings = str_word_count($_content, 1);
            foreach ($bad_strings as $string) {
                if (in_array($string, $in_strings) !== false) {
                    $errors[] = array('WCAG_DODGY_LINK_2', $string);
                }
            }
        }
        //if ((strlen(@html_entity_decode($_content, ENT_QUOTES, get_charset())) > 40) && (isset($attributes['href'])) && (strpos($attributes['href'], 'tut_') === false)) $errors[] = array('WCAG_ATTRIBUTE_TOO_LONG', 'a');
        if ($title == '') {
            if (strtolower($content) == 'more') {
                $errors[] = array('WCAG_DODGY_LINK_2', $string);
            }
        }
        $A_LINKS[$title][$content] = $filtered_href;
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks form field labelling.
 *
 * @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
 * @return ?list Array of errors (null: none)
 * @ignore
 */
function _check_labelling($tag, $attributes, $self_close, $close)
{
    global $TAG_STACK, $IDS_SO_FAR, $ANCESTOR_BLOCK, $ANCESTOR_INLINE, $EXPECTING_TAG, $OUT, $POS, $LAST_A_TAG;

    $errors = array();

    global $FOR_LABEL_IDS, $FOR_LABEL_IDS_2, $INPUT_TAG_IDS;
    if (($tag == 'td')/* || ($tag == 'div')*/) {
        //$FOR_LABEL_IDS = array(); // Can't work across table cells      Actually this is an ancient and lame restriction that hurts accessibility more than helping it
    }
    if (($tag == 'label') && (isset($attributes['for']))) {
        $FOR_LABEL_IDS[$attributes['for']] = 1;
        $FOR_LABEL_IDS_2[$attributes['for']] = 1;
    } // Check we that all input tags have labels
    elseif ((!$close) && (($tag == 'textarea') || ($tag == 'select') || (($tag == 'input') && ((!isset($attributes['type'])) || (($attributes['type'] != 'hidden') && ($attributes['type'] != 'button') && ($attributes['type'] != 'image') && ($attributes['type'] != 'reset') && ($attributes['type'] != 'submit')))))) {
        if (isset($attributes['id'])) {
            $INPUT_TAG_IDS[$attributes['id']] = 1;
        }

        if ((!isset($attributes['style'])) || (($attributes['style'] != 'display:none') && ($attributes['style'] != 'display: none'))) {
            if ($tag == 'input') {
                if (!isset($attributes['type'])) {
                    return null;
                }

                if ((($attributes['type'] == 'radio') || ($attributes['type'] == 'checkbox')) && (isset($attributes['onchange'])) && ($GLOBALS['WEBSTANDARDS_COMPAT'])) {
                    $errors[] = array('XHTML_IE_ONCHANGE');
                }
            }

            //if ((!in_array('label', $TAG_STACK)))//&& ((!isset($attributes['value']) || ($attributes['value'] == '')))) { // Compromise - sometimes we will use a default value as a substitute for a label. Not strictly allowed in accessibility rules, but writers mention as ok (+ we need it so we don't clutter things unless we start hiding labels, which is not nice)
            if (!isset($attributes['id'])) {
                $attributes['id'] = 'unnamed_' . strval(mt_rand(0, mt_getrandmax()));
            }

            if ((!isset($FOR_LABEL_IDS[$attributes['id']])) && ($attributes['id'] != 'x') && (preg_match('#<label[^<>]+for="' . preg_quote($attributes['id'], '#') . '"#', $OUT) == 0)) {
                $errors[] = array('WCAG_NO_INPUT_LABEL', $attributes['id']);
            }
            //}
        }
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks a CSS style sheet (high level).
 *
 * @param  string $data The data of the style sheet
 * @return array Parse information
 */
function check_css($data)
{
    if (!isset($GLOBALS['MAIL_MODE'])) {
        $GLOBALS['MAIL_MODE'] = false;
    }
    $_errors = _webstandards_css_sheet($data);
    if (is_null($_errors)) {
        $_errors = array();
    }
    $errors = array();
    global $POS, $OUT;
    global $CSS_TAG_RANGES, $CSS_VALUE_RANGES;
    $OUT = $data;

    foreach ($_errors as $error) {
        $POS = 0;
        $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] : '', false, $error['pos']);
    }
    return array('level_ranges' => null, 'tag_ranges' => $CSS_TAG_RANGES, 'value_ranges' => $CSS_VALUE_RANGES, 'errors' => $errors);
}

/**
 * Checks a CSS style sheet.
 *
 * @param  string $data The data of the style sheet
 * @return ?map Error information (null: no error)
 * @ignore
 */
function _webstandards_css_sheet($data)
{
    global $CSS_TAG_RANGES, $CSS_VALUE_RANGES, $VALIDATED_ALREADY;
    $CSS_TAG_RANGES = array();
    $CSS_VALUE_RANGES = array();

    $errors = array();

    $len = strlen($data);
    $status = CSS_NO_MANS_LAND;
    $line = 0;
    $class_before_comment = null;
    $class = '';
    $at_rule = '';
    $at_rule_block = '';
    //$left_no_mans_land_once = false;
    $brace_level = 0;
    $i = 0;
    $class_start_line = null;
    $class_start_i = null;
    $class_name = '';
    $in_comment = false;
    $quoting = false;
    while ($i < $len) {
        $next = $data[$i];
        $val = ord($next);
        if (($next == '_') || ($next == '.') || ($next == '-') || (($val >= 65) && ($val <= 90)) || (($val >= 97) && ($val <= 122)) || (($val >= 48) && ($val <= 57))) {
            $alpha_numeric = true;
            $whitespace = false;
            $comment_starting = false;
        } else {
            $alpha_numeric = false;
            $whitespace = (($next == "\t") || ($val == 13) || ($val == 10) || ($next == ' '));
            $comment_starting = (($next == '/') && ($i < $len - 2) && ($data[$i + 1] == '*'));
        }

        switch ($status) {
            case CSS_AT_RULE:
                if ($next == '{') {
                    $brace_level = 0;
                    $status = CSS_AT_RULE_BLOCK;
                    $at_rule_block = '';
                } elseif ($next == ';') {
                    $status = CSS_NO_MANS_LAND;
                    if (substr($at_rule, 0, 6) == 'import ') {
                        $count = substr_count($at_rule, '"');
                        $first = strpos($at_rule, '"') + 1;
                        if ($count < 2) {
                            $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                            return $errors;
                        }
                        $at_file = substr($at_rule, $first, (strpos($at_rule, '"', $first) - 1) - $first);
                        if (!isset($VALIDATED_ALREADY[$at_file])) {
                            $at_file = qualify_url($at_file, $GLOBALS['URL_BASE']);
                            if ($at_file != '') {
                                $data2 = http_download_file($at_file, null, false);
                                if (!is_null($data2)) {
                                    $css_tag_ranges_backup = $CSS_TAG_RANGES;
                                    $css_value_ranges_backup = $CSS_VALUE_RANGES;
                                    $test = _webstandards_css_sheet($data2);
                                    $CSS_TAG_RANGES = $css_tag_ranges_backup;
                                    $CSS_VALUE_RANGES = $css_value_ranges_backup;
                                    if (is_array($test)) {
                                        foreach ($test as $error) {
                                            $error['pos'] = $i;
                                            $errors[] = $error;
                                        }
                                    }
                                }
                            }
                        }
                    }
                } else {
                    $at_rule .= $next;
                }
                break;

            case CSS_AT_RULE_BLOCK:
                if ($next == '{') {
                    ++$brace_level;
                    $at_rule_block .= $next;
                } elseif (($next == '}') && ($brace_level == 0)) {
                    $status = CSS_NO_MANS_LAND;
                    if (substr($at_rule, 0, 6) == 'media ') {
                        $css_tag_ranges_backup = $CSS_TAG_RANGES;
                        $css_value_ranges_backup = $CSS_VALUE_RANGES;
                        $test = _webstandards_css_sheet($at_rule_block);
                        $CSS_TAG_RANGES = $css_tag_ranges_backup;
                        $CSS_VALUE_RANGES = $css_value_ranges_backup;
                        if (is_array($test)) {
                            foreach ($test as $error) {
                                $error['pos'] = $i;
                                $errors[] = $error;
                            }
                        }
                    }
                } elseif ($next == '}') {
                    $brace_level--;
                    $at_rule_block .= $next;
                } else {
                    $at_rule_block .= $next;
                }
                break;

            case CSS_NO_MANS_LAND:
                if ($whitespace) {
                    // Continuing
                } elseif (($next == '.') || ($next == ':') || ($next == '#') || ($alpha_numeric)) {
                    if ($data[$i + 1] == ':') { // e.g. "::selection"
                        if (isset($GLOBALS['PEDANTIC'])) {
                            $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                        }
                        $i++;
                    }
                    $status = CSS_IN_IDENTIFIER;
                    $class_name = '';
                    //$left_no_mans_land_once = true;
                } elseif ($next == '@') {
                    $status = CSS_AT_RULE;
                    $at_rule = '';
                } elseif ($comment_starting) {
                    $status = CSS_IN_COMMENT;
                    $class_before_comment = CSS_NO_MANS_LAND;
                } elseif ($next == '*') {
                    $status = CSS_IN_IDENTIFIER;
                    $class_name = '*';
                    //$left_no_mans_land_once = true;
                } else {
                    $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                }
                break;

            case CSS_EXPECTING_IDENTIFIER:
                if ($comment_starting) {
                    $status = CSS_IN_COMMENT;
                    $class_before_comment = CSS_EXPECTING_IDENTIFIER;
                } elseif ($whitespace) {
                    // Continuing
                } elseif ($next == '*') {
                    $status = CSS_EXPECTING_SEP_OR_IDENTIFIER_OR_CLASS;
                } elseif (($next == '.') || ($next == ':') || ($next == '#') || ($alpha_numeric)) {
                    $status = CSS_IN_IDENTIFIER;
                    $class_name = $alpha_numeric ? $next : '';
                } else {
                    $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                }
                break;

            case CSS_IN_COMMENT:
                if (($next == '*') && ($i != $len - 1) && ($data[$i + 1] == '/')) {
                    $status = $class_before_comment;
                    ++$i;
                }
                break;

            case CSS_IN_PSEUDOCLASS_EXPRESSION:
                if ($next == ')') {
                    $status = CSS_IN_IDENTIFIER;
                }
                break;

            case CSS_IN_IDENTIFIER:
                if ($next == '(') {
                    $status = CSS_IN_PSEUDOCLASS_EXPRESSION;
                    break;
                }
                if (($alpha_numeric) || ($next == ':') || ($next == '#')) {
                    $class_name .= $next;
                } elseif ($comment_starting) {
                    $status = CSS_IN_COMMENT;
                    $class_before_comment = CSS_EXPECTING_SEP_OR_IDENTIFIER_OR_CLASS;
                } else {
                    // Test class name
                    $cnt = substr_count($class_name, ':');
                    if ($cnt > 0) {
                        $matches = array();
                        $num_matches = preg_match_all('#:([\w\-]+)#', $class_name, $matches);
                        for ($j = 0; $j < $num_matches; $j++) {
                            $pseudo = $matches[1][$j];
                            if (($GLOBALS['WEBSTANDARDS_COMPAT']) && (!in_array($pseudo, array('active', 'hover', 'link', 'visited', 'first-letter', 'first-line', 'first-child', 'last-child', 'before', 'after', 'disabled', 'focus')))) {
                                $errors[] = array(0 => 'CSS_BAD_PSEUDO_CLASS', 1 => $pseudo, 'pos' => $i);
                            }
                        }
                    }

                    if ($whitespace) {
                        $status = CSS_EXPECTING_SEP_OR_IDENTIFIER_OR_CLASS;
                    } elseif (($next == ',') || ($next == '>') || ($next == '+') || ($next == '~')) {
                        $status = CSS_EXPECTING_IDENTIFIER;
                    } elseif ($next == '{') {
                        $status = CSS_IN_CLASS;
                        $class_start_line = $line;
                        $class_start_i = $i;
                        $class = '';
                    } else {
                        $matches = array();
                        if (($next == '[') && (preg_match('#\[(\w+|)?\w+([$*~|^]?="[^;"]*")?\]#', $data, $matches, 0, $i) == 1)) {
                            $i += strlen($matches[0]) - 1;
                        } else {
                            $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                        }
                    }
                }
                break;

            case CSS_EXPECTING_SEP_OR_IDENTIFIER_OR_CLASS:
                if ($next == '{') {
                    $status = CSS_IN_CLASS;
                    $class_start_line = $line;
                    $class_start_i = $i;
                    $class = '';
                } elseif ($next == '*') {
                    // Continuing
                } elseif ($whitespace) {
                    // Continuing
                } elseif (($next == ',') || ($next == '>') || ($next == '+') || ($next == '~')) {
                    $status = CSS_EXPECTING_IDENTIFIER;
                } elseif ($comment_starting) {
                    $status = CSS_IN_COMMENT;
                    $class_before_comment = CSS_EXPECTING_SEP_OR_IDENTIFIER_OR_CLASS;
                } elseif (($next == '.') || ($next == ':') || ($next == '#') || ($alpha_numeric)) {
                    $status = CSS_IN_IDENTIFIER;
                    $class_name = '';
                } elseif ($next == '[') {
                    $i--;
                    $status = CSS_IN_IDENTIFIER;
                    $class_name = '';
                } else {
                    $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                }
                break;

            case CSS_IN_CLASS:
                if ($quoting) {
                    if ((($next == '"') || ($next == "'")) && ($data[$i - 1] != "\\")) {
                        $quoting = !$quoting;
                    }
                    $class .= $next;
                } elseif ($in_comment) {
                    $comment_ending = (($next == '*') && ($i < $len - 2) && ($data[$i + 1] == '/'));
                    if ($comment_ending) {
                        $in_comment = false;
                    }
                    $class .= $next;
                } elseif ($next == '}') {
                    $status = CSS_NO_MANS_LAND;
                    $test = _webstandards_css_class($class, $class_start_i, $class_start_line + 1);
                    if (is_array($test)) {
                        $errors = array_merge($errors, $test);
                    }
                } elseif ($comment_starting) {
                    $in_comment = true;
                    $class .= $next;
                } elseif ((($next == '"') || ($next == "'")) && ($data[$i - 1] != "\\")) {
                    $quoting = true;
                    $class .= $next;
                } elseif ($next == '{') {
                    $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER_CLASS', 1 => $next, 2 => integer_format($line), 'pos' => $i);
                    return $errors;
                } else {
                    $class .= $next;
                }
                break;
        }

        if ($val == 10) {
            ++$line;
        }

        ++$i;
    }

    if ($status != CSS_NO_MANS_LAND) {
        $errors[] = array(0 => 'CSS_UNEXPECTED_TERMINATION', 'pos' => $i);
        return $errors;
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks a CSS class.
 *
 * @param  string $data The data of the CSS class
 * @param  integer $_i Current parse position
 * @param  integer $line The higher-level line number we are checking for (to give better debug output)
 * @return ?map Error information (null: no error)
 * @ignore
 */
function _webstandards_css_class($data, $_i, $line = 0)
{
    $errors = array();

    global $CSS_TAG_RANGES;
    global $CSS_VALUE_RANGES;

    $len = strlen($data);
    $i = 0;
    $key = '';
    $value = '';
    $status = _CSS_NO_MANS_LAND;
    $class_before_comment = null;
    $quoting = false;
    while ($i < $len) {
        $next = $data[$i];
        $val = ord($next);
        $alpha_numeric = ($next == '_') || ($next == '.') || ($next == '-') || (($val >= 65) && ($val <= 90)) || (($val >= 97) && ($val <= 122)) || (($val >= 48) && ($val <= 57));
        $whitespace = (($next == "\t") || ($val == 13) || ($val == 10) || ($next == ' '));
        $comment_starting = (($next == '/') && ($i != $len - 2) && ($i + 1 < $len) && ($data[$i + 1] == '*'));

        switch ($status) {
            case _CSS_NO_MANS_LAND:
                if ($alpha_numeric) {
                    $CSS_TAG_RANGES[] = array($_i + $i + 1, $_i + $i + 2);
                    $status = _CSS_IN_PROPERTY_KEY;
                    $key = $next;
                } elseif (($whitespace) || ($next == ';')) { // ; is unusual here, but allowed; It occurs when we substitute nothing into a part of a style attribute
                    // Continuing
                } elseif ($comment_starting) {
                    $class_before_comment = $status;
                    $status = _CSS_IN_COMMENT;
                } else {
                    $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER_CLASS', 1 => $next, 2 => integer_format($line), 'pos' => $_i);
                }
                break;

            case _CSS_IN_COMMENT:
                if (($next == '*') && ($i != $len - 1) && ($data[$i + 1] == '/')) {
                    $status = $class_before_comment;
                    ++$i;
                }
                break;

            case _CSS_IN_PROPERTY_KEY:
                if ($next == ':') {
                    $status = _CSS_IN_PROPERTY_BETWEEN;
                    $value = '';
                } elseif ($alpha_numeric) {
                    $key .= $next;
                    $CSS_TAG_RANGES[count($CSS_TAG_RANGES) - 1][1]++;
                }
                break;

            case _CSS_IN_PROPERTY_BETWEEN:
                if (!$whitespace) {
                    $status = _CSS_IN_PROPERTY_VALUE;
                    $CSS_VALUE_RANGES[] = array($_i + $i + 1, $_i + $i + 1);
                    $i--;
                }
                break;

            case _CSS_IN_PROPERTY_VALUE:
                if (($next == ';') && (!$quoting)) {
                    $test = _check_css_value($key, $value, $_i);
                    if (is_array($test)) {
                        $errors[] = $test;
                    }
                    $status = _CSS_NO_MANS_LAND;
                } elseif ($comment_starting) {
                    $class_before_comment = $status;
                    $status = _CSS_IN_COMMENT;
                } elseif (($val == 10) || ($val == 13)) {
                    $status = _CSS_EXPECTING_END;
                } else {
                    if ((($next == '"') || ($next == "'")) && ($data[$i - 1] != "\\")) {
                        $quoting = !$quoting;
                    }
                    $value .= $next;
                    $CSS_VALUE_RANGES[count($CSS_VALUE_RANGES) - 1][1]++;
                }
                break;

            case _CSS_EXPECTING_END:
                if (!$whitespace) {
                    $errors[] = array(0 => 'CSS_UNEXPECTED_CHARACTER_CLASS', 1 => $next, 2 => integer_format($line), 'pos' => $_i);
                    return $errors;
                }
                break;
        }

        if ($val == 10) {
            ++$line;
        }

        ++$i;
    }

    if (($status != _CSS_NO_MANS_LAND) && ($status != _CSS_IN_PROPERTY_VALUE) && ($status != _CSS_EXPECTING_END)) {
        $errors[] = array(0 => 'CSS_UNEXPECTED_TERMINATION_PROPERTY', 'pos' => $_i);
        return $errors;
    }

    if ($status == _CSS_IN_PROPERTY_VALUE) {
        $test = _check_css_value($key, $value, $_i);
        if (is_array($test)) {
            $errors[] = $test;
        }
    }

    return ($errors == array()) ? null : $errors;
}

/**
 * Checks a CSS attribute/value combination is appropriate.
 *
 * @param  string $key The name of the attribute
 * @param  string $value The value of the attribute
 * @param  integer $_i Current parse position
 * @return ?map Error information (null: no error)
 * @ignore
 */
function _check_css_value($key, $value, $_i)
{
    $value = str_replace(' !important', '', $value);
    $value = trim($value);

    if (substr($value, 0, 11) == '!important ') {
        $value = substr($value, 11); // Strip off the important flag if it's present
    }

    $error = null;

    global $CSS_PROPERTIES, $CSS_NON_IE_PROPERTIES;
    if (!isset($CSS_PROPERTIES[$key])) {
        if (!isset($CSS_NON_IE_PROPERTIES[$key])) {
            if (substr($key, 0, 1) == '-') {
                return null;
            }
            return array(0 => 'CSS_UNKNOWN_PROPERTY', 1 => $key, 'pos' => $_i);
        } else {
            $reg_exp = $CSS_NON_IE_PROPERTIES[$key];
            if ($GLOBALS['WEBSTANDARDS_COMPAT']) {
                $error = array(0 => 'CSS_NON_IE_PROPERTIES', 1 => $key, 'pos' => $_i);
            }
        }
    } else {
        $reg_exp = $CSS_PROPERTIES[$key];
    }

    if ((preg_match('#^' . $reg_exp . '$#s', $value) == 0) && ($value != 'xpx') && ($value != 'x') && ($value != 'inherit') && ($value != 'initial') && (substr($value, 0, 5) != 'calc(')) {
        return array(0 => 'CSS_BAD_PROPERTY_VALUE', 1 => $key, 2 => $value, 3 => $reg_exp, 'pos' => $_i);
    }

    if ($GLOBALS['MAIL_MODE']) {
        if ($key == 'position') {
            return array(0 => 'MAIL_POSITIONING', 'pos' => $_i);
        }
        if (($key == 'width') && (substr($value, -2) == 'px') && (intval(substr($value, 0, strlen($value) - 2)) > 530)) {
            return array(0 => 'MAIL_WIDTH', 'pos' => $_i);
        }
    } else {
        if (isset($GLOBALS['PEDANTIC'])) {
            if (($key == 'font-size') && (substr($value, -2) == 'px')) {
                return array(0 => 'CSS_PX_FONT', 'pos' => $_i);
            }
        }
    }

    return $error;
}
