]+charset/i', substr( $html, 0, 4096 ) ) ? '' : '';
$loaded = $doc->loadHTML( $prefix . $html, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NONET | LIBXML_COMPACT );
libxml_clear_errors();
libxml_use_internal_errors( $previous );
if ( ! $loaded ) {
return null;
}
$xp = new \DOMXPath( $doc );
$base = $url;
$base_node = $xp->query( '//head/base[@href]' )->item( 0 );
if ( $base_node instanceof \DOMElement ) {
$abs = Http::absolutize( $base_node->getAttribute( 'href' ), $url );
if ( $abs ) {
$base = $abs;
}
}
$facts = array(
'lang' => '',
'titles' => array(),
'meta_desc' => array(),
'canonicals' => array(),
'robots' => array(),
'meta_refresh' => '',
'viewport' => '',
'headings' => array(),
'images' => array(
'total' => 0,
'missing_alt' => array(),
'empty_alt_in_link' => array(),
),
'links' => array(),
'unnamed_links' => array(),
'generic_links' => array(),
'unlabeled_fields' => array(),
'placeholder_only' => array(),
'unnamed_buttons' => array(),
'invalid_roles' => array(),
'hidden_focusable' => array(),
'broken_aria_refs' => array(),
'unknown_aria' => array(),
'duplicate_ids' => array(),
'positive_tabindex' => 0,
'click_handlers' => array(),
'iframes_untitled' => array(),
'autoplay_media' => 0,
'contrast' => array(),
'mixed_content' => array(),
'jsonld' => array(
'count' => 0,
'invalid' => 0,
'types' => array(),
),
'word_count' => 0,
'text_hash' => '',
'script_count' => 0,
'forms_without_submit' => 0,
'required_without_hint' => 0,
);
$html_el = $doc->getElementsByTagName( 'html' )->item( 0 );
if ( $html_el instanceof \DOMElement ) {
$facts['lang'] = trim( $html_el->getAttribute( 'lang' ) ? $html_el->getAttribute( 'lang' ) : $html_el->getAttribute( 'xml:lang' ) );
}
foreach ( $xp->query( '//title' ) as $node ) {
// Ignore
inside inline SVG.
if ( self::inside( $node, 'svg' ) ) {
continue;
}
$facts['titles'][] = self::clean( $node->textContent, 300 );
}
foreach ( $xp->query( '//meta' ) as $meta ) {
$name = strtolower( trim( $meta->getAttribute( 'name' ) ) );
$equiv = strtolower( trim( $meta->getAttribute( 'http-equiv' ) ) );
$content = trim( $meta->getAttribute( 'content' ) );
if ( 'description' === $name ) {
$facts['meta_desc'][] = self::clean( $content, 500 );
} elseif ( 'robots' === $name || 'googlebot' === $name ) {
$facts['robots'][] = strtolower( $content );
} elseif ( 'viewport' === $name ) {
$facts['viewport'] = strtolower( $content );
} elseif ( 'refresh' === $equiv ) {
$facts['meta_refresh'] = self::clean( $content, 300 );
}
}
foreach ( $xp->query( '//link[@rel]' ) as $link ) {
$rels = preg_split( '/\s+/', strtolower( $link->getAttribute( 'rel' ) ) );
if ( in_array( 'canonical', $rels, true ) ) {
$abs = Http::absolutize( $link->getAttribute( 'href' ), $base );
$facts['canonicals'][] = $abs ? $abs : $link->getAttribute( 'href' );
}
}
// Headings in document order.
foreach ( $xp->query( '//h1|//h2|//h3|//h4|//h5|//h6' ) as $h ) {
$facts['headings'][] = array( (int) substr( $h->nodeName, 1 ), self::clean( self::accessible_text( $h ), 120 ) );
if ( count( $facts['headings'] ) >= 200 ) {
break;
}
}
// Images.
foreach ( $xp->query( '//img' ) as $img ) {
++$facts['images']['total'];
$role = strtolower( $img->getAttribute( 'role' ) );
$hidden = 'true' === strtolower( $img->getAttribute( 'aria-hidden' ) ) || 'presentation' === $role || 'none' === $role;
$src = $img->getAttribute( 'src' ) ? $img->getAttribute( 'src' ) : $img->getAttribute( 'data-src' );
if ( ! $img->hasAttribute( 'alt' ) && ! $hidden && '' === trim( $img->getAttribute( 'aria-label' ) ) && '' === trim( $img->getAttribute( 'aria-labelledby' ) ) ) {
if ( count( $facts['images']['missing_alt'] ) < 50 ) {
$facts['images']['missing_alt'][] = self::clean( $src, 300 );
} else {
$facts['images']['missing_alt'][] = '';
}
}
}
// Links.
$generic = array( 'click here', 'here', 'read more', 'more', 'learn more', 'link', 'this', 'continue', 'details', 'click', 'go' );
foreach ( $xp->query( '//a[@href]' ) as $a ) {
$href = trim( $a->getAttribute( 'href' ) );
$name = self::accessible_name( $a, $xp );
if ( '' === $name && 'true' !== strtolower( $a->getAttribute( 'aria-hidden' ) ) ) {
if ( count( $facts['unnamed_links'] ) < 30 ) {
$facts['unnamed_links'][] = self::clean( $href, 200 );
}
} elseif ( in_array( strtolower( trim( $name, " .:!>\xC2\xA0" ) ), $generic, true ) ) {
if ( count( $facts['generic_links'] ) < 30 ) {
$facts['generic_links'][] = self::clean( $name, 60 ) . ' → ' . self::clean( $href, 150 );
}
}
$abs = Http::absolutize( $href, $base );
if ( $abs && count( $facts['links'] ) < 500 ) {
$rel = strtolower( $a->getAttribute( 'rel' ) );
$facts['links'][] = array( $abs, self::clean( $name, 100 ), false !== strpos( $rel, 'nofollow' ) );
}
}
// Form controls.
$ids = array();
foreach ( $xp->query( '//*[@id]' ) as $el ) {
$id = $el->getAttribute( 'id' );
if ( '' !== $id ) {
$ids[ $id ] = isset( $ids[ $id ] ) ? $ids[ $id ] + 1 : 1;
}
}
foreach ( $ids as $id => $count ) {
if ( $count > 1 && count( $facts['duplicate_ids'] ) < 20 ) {
$facts['duplicate_ids'][] = self::clean( $id, 80 ) . ' ×' . $count;
}
}
$label_for = array();
foreach ( $xp->query( '//label[@for]' ) as $label ) {
if ( '' !== trim( self::accessible_text( $label ) ) ) {
$label_for[ $label->getAttribute( 'for' ) ] = true;
}
}
foreach ( $xp->query( '//input|//select|//textarea' ) as $field ) {
$type = strtolower( $field->getAttribute( 'type' ) );
if ( 'input' === $field->nodeName && in_array( $type, array( 'hidden', 'submit', 'button', 'reset', 'image' ), true ) ) {
if ( in_array( $type, array( 'submit', 'button', 'reset' ), true ) && '' === trim( $field->getAttribute( 'value' ) ) && '' === trim( $field->getAttribute( 'aria-label' ) ) && 'submit' !== $type && 'reset' !== $type ) {
$facts['unnamed_buttons'][] = 'input[type=' . $type . ']';
}
if ( 'image' === $type && '' === trim( $field->getAttribute( 'alt' ) ) && '' === trim( $field->getAttribute( 'aria-label' ) ) ) {
$facts['unnamed_buttons'][] = 'input[type=image]';
}
continue;
}
if ( 'true' === strtolower( $field->getAttribute( 'aria-hidden' ) ) || $field->hasAttribute( 'disabled' ) && 'none' === $field->getAttribute( 'role' ) ) {
continue;
}
$id = $field->getAttribute( 'id' );
$labelled = ( '' !== $id && isset( $label_for[ $id ] ) )
|| self::inside( $field, 'label' )
|| '' !== trim( $field->getAttribute( 'aria-label' ) )
|| self::labelledby_text( $field, $xp ) !== ''
|| '' !== trim( $field->getAttribute( 'title' ) );
if ( ! $labelled ) {
$desc = $field->nodeName . ( $type ? '[type=' . $type . ']' : '' ) . ( $field->getAttribute( 'name' ) ? '[name=' . $field->getAttribute( 'name' ) . ']' : '' );
if ( '' !== trim( $field->getAttribute( 'placeholder' ) ) ) {
$facts['placeholder_only'][] = self::clean( $desc, 120 );
} else {
$facts['unlabeled_fields'][] = self::clean( $desc, 120 );
}
}
if ( $field->hasAttribute( 'required' ) && '' === trim( $field->getAttribute( 'aria-describedby' ) ) ) {
++$facts['required_without_hint'];
}
}
foreach ( $xp->query( '//button|//*[@role="button"]' ) as $button ) {
if ( 'true' === strtolower( $button->getAttribute( 'aria-hidden' ) ) ) {
continue;
}
if ( '' === self::accessible_name( $button, $xp ) && count( $facts['unnamed_buttons'] ) < 30 ) {
$cls = $button->getAttribute( 'class' );
$facts['unnamed_buttons'][] = self::clean( $button->nodeName . ( $cls ? '.' . preg_replace( '/\s+/', '.', trim( $cls ) ) : '' ), 120 );
}
}
// ARIA validity.
$valid_roles = self::valid_roles();
$aria_attrs = self::valid_aria_attributes();
foreach ( $xp->query( '//*[@role]' ) as $el ) {
foreach ( preg_split( '/\s+/', strtolower( trim( $el->getAttribute( 'role' ) ) ) ) as $role ) {
if ( '' !== $role && ! in_array( $role, $valid_roles, true ) && count( $facts['invalid_roles'] ) < 20 ) {
$facts['invalid_roles'][] = $el->nodeName . '[role=' . self::clean( $role, 40 ) . ']';
}
}
}
foreach ( $xp->query( '//*[@*[starts-with(name(),"aria-")]]' ) as $el ) {
foreach ( $el->attributes as $attr ) {
$n = strtolower( $attr->nodeName );
if ( 0 === strpos( $n, 'aria-' ) && ! in_array( $n, $aria_attrs, true ) && count( $facts['unknown_aria'] ) < 20 ) {
$facts['unknown_aria'][] = $el->nodeName . '[' . self::clean( $n, 40 ) . ']';
}
}
if ( 'true' === strtolower( $el->getAttribute( 'aria-hidden' ) ) && self::is_focusable( $el ) && count( $facts['hidden_focusable'] ) < 20 ) {
$facts['hidden_focusable'][] = $el->nodeName . ( $el->getAttribute( 'href' ) ? '[href=' . self::clean( $el->getAttribute( 'href' ), 80 ) . ']' : '' );
}
foreach ( array( 'aria-labelledby', 'aria-describedby' ) as $ref_attr ) {
if ( $el->hasAttribute( $ref_attr ) ) {
foreach ( preg_split( '/\s+/', trim( $el->getAttribute( $ref_attr ) ) ) as $ref ) {
if ( '' !== $ref && ! isset( $ids[ $ref ] ) && count( $facts['broken_aria_refs'] ) < 20 ) {
$facts['broken_aria_refs'][] = $ref_attr . '="' . self::clean( $ref, 60 ) . '"';
}
}
}
}
}
foreach ( $xp->query( '//*[@tabindex]' ) as $el ) {
if ( (int) $el->getAttribute( 'tabindex' ) > 0 ) {
++$facts['positive_tabindex'];
}
}
foreach ( $xp->query( '//div[@onclick]|//span[@onclick]|//li[@onclick]|//img[@onclick]' ) as $el ) {
if ( ! $el->hasAttribute( 'role' ) && ! $el->hasAttribute( 'tabindex' ) && count( $facts['click_handlers'] ) < 20 ) {
$facts['click_handlers'][] = $el->nodeName . ( $el->getAttribute( 'class' ) ? '.' . self::clean( preg_replace( '/\s+/', '.', trim( $el->getAttribute( 'class' ) ) ), 60 ) : '' );
}
}
foreach ( $xp->query( '//iframe' ) as $iframe ) {
if ( '' === trim( $iframe->getAttribute( 'title' ) ) && '' === trim( $iframe->getAttribute( 'aria-label' ) ) && 'true' !== strtolower( $iframe->getAttribute( 'aria-hidden' ) ) ) {
$facts['iframes_untitled'][] = self::clean( $iframe->getAttribute( 'src' ), 200 );
}
}
foreach ( $xp->query( '//video[@autoplay]|//audio[@autoplay]' ) as $media ) {
if ( ! $media->hasAttribute( 'muted' ) ) {
++$facts['autoplay_media'];
}
}
// Inline-style contrast (only when both colours are explicitly declared).
foreach ( $xp->query( '//*[@style]' ) as $el ) {
$style = strtolower( $el->getAttribute( 'style' ) );
if ( false === strpos( $style, 'color' ) ) {
continue;
}
$fg = self::style_color( $style, 'color' );
$bg = self::style_color( $style, 'background-color' );
if ( null === $bg ) {
$bg = self::style_color( $style, 'background' );
}
$text = trim( $el->textContent );
if ( null !== $fg && null !== $bg && '' !== $text ) {
$ratio = self::contrast_ratio( $fg, $bg );
if ( $ratio < 4.5 && count( $facts['contrast'] ) < 20 ) {
$facts['contrast'][] = sprintf( '%s "%s" ratio %.2f:1', $el->nodeName, self::clean( $text, 40 ), $ratio );
}
}
}
// Mixed content (only meaningful on HTTPS pages).
if ( 0 === stripos( $url, 'https://' ) ) {
$q = '//img[@src]|//script[@src]|//iframe[@src]|//video[@src]|//audio[@src]|//source[@src]|//link[@href][contains(translate(@rel,"STYLESHEET","stylesheet"),"stylesheet")]';
foreach ( $xp->query( $q ) as $el ) {
$ref = $el->hasAttribute( 'src' ) ? $el->getAttribute( 'src' ) : $el->getAttribute( 'href' );
if ( 0 === stripos( trim( $ref ), 'http://' ) && count( $facts['mixed_content'] ) < 30 ) {
$facts['mixed_content'][] = $el->nodeName . ': ' . self::clean( $ref, 200 );
}
}
}
// Structured data.
foreach ( $xp->query( '//script[@type="application/ld+json"]' ) as $script ) {
++$facts['jsonld']['count'];
$json = json_decode( trim( $script->textContent ), true );
if ( null === $json && JSON_ERROR_NONE !== json_last_error() ) {
++$facts['jsonld']['invalid'];
continue;
}
foreach ( self::jsonld_types( $json ) as $type ) {
$facts['jsonld']['types'][ $type ] = true;
}
}
$facts['jsonld']['types'] = array_slice( array_keys( $facts['jsonld']['types'] ), 0, 20 );
$facts['script_count'] = $xp->query( '//script' )->length;
// Visible text of the main content area (falls back to ) so that
// shared navigation/footer text does not distort word counts or duplicates.
$body = null;
foreach ( array( '//main', '//*[@role="main"]', '//article' ) as $main_query ) {
$candidate = $xp->query( $main_query )->item( 0 );
if ( $candidate && strlen( trim( $candidate->textContent ) ) > 0 ) {
$body = $candidate;
break;
}
}
if ( ! $body ) {
$body = $doc->getElementsByTagName( 'body' )->item( 0 );
}
if ( $body ) {
foreach ( array( 'script', 'style', 'noscript', 'template', 'svg' ) as $tag ) {
$nodes = array();
foreach ( $body->getElementsByTagName( $tag ) as $n ) {
$nodes[] = $n;
}
foreach ( $nodes as $n ) {
if ( $n->parentNode ) {
$n->parentNode->removeChild( $n );
}
}
}
$text = preg_replace( '/\s+/u', ' ', $body->textContent );
$words = preg_split( '/[\s\p{P}]+/u', trim( (string) $text ), -1, PREG_SPLIT_NO_EMPTY );
$facts['word_count'] = is_array( $words ) ? count( $words ) : 0;
if ( $facts['word_count'] >= 50 ) {
$facts['text_hash'] = md5( mb_strtolower( (string) $text ) );
}
}
return $facts;
}
/**
* Whether a node has an ancestor with the given tag.
*
* @param \DOMNode $node Node.
* @param string $tag Tag.
* @return bool
*/
private static function inside( $node, $tag ) {
for ( $p = $node->parentNode; $p; $p = $p->parentNode ) {
if ( $p instanceof \DOMElement && strtolower( $p->nodeName ) === $tag ) {
return true;
}
}
return false;
}
/**
* Text content including img alt text (approximate accessible text).
*
* @param \DOMNode $node Node.
* @return string
*/
private static function accessible_text( $node ) {
$out = '';
foreach ( $node->childNodes as $child ) {
if ( XML_TEXT_NODE === $child->nodeType ) {
$out .= $child->nodeValue;
} elseif ( $child instanceof \DOMElement ) {
if ( 'true' === strtolower( $child->getAttribute( 'aria-hidden' ) ) || in_array( strtolower( $child->nodeName ), array( 'script', 'style' ), true ) ) {
continue;
}
if ( 'img' === strtolower( $child->nodeName ) ) {
$out .= ' ' . $child->getAttribute( 'alt' ) . ' ';
} elseif ( 'svg' === strtolower( $child->nodeName ) ) {
$out .= ' ' . $child->getAttribute( 'aria-label' ) . ' ';
foreach ( $child->getElementsByTagName( 'title' ) as $t ) {
$out .= ' ' . $t->textContent . ' ';
}
} else {
$label = $child->getAttribute( 'aria-label' );
$out .= '' !== trim( $label ) ? ' ' . $label . ' ' : self::accessible_text( $child );
}
}
}
return trim( preg_replace( '/\s+/u', ' ', $out ) );
}
/**
* Text referenced by aria-labelledby.
*
* @param \DOMElement $el Element.
* @param \DOMXPath $xp XPath.
* @return string
*/
private static function labelledby_text( $el, $xp ) {
$ref = trim( $el->getAttribute( 'aria-labelledby' ) );
if ( '' === $ref ) {
return '';
}
$text = '';
foreach ( preg_split( '/\s+/', $ref ) as $id ) {
if ( '' === $id || false !== strpos( $id, '"' ) ) {
continue;
}
$node = $xp->query( '//*[@id="' . $id . '"]' )->item( 0 );
if ( $node ) {
$text .= ' ' . $node->textContent;
}
}
return trim( $text );
}
/**
* Approximate accessible name for links/buttons.
*
* @param \DOMElement $el Element.
* @param \DOMXPath $xp XPath.
* @return string
*/
private static function accessible_name( $el, $xp ) {
$label = trim( $el->getAttribute( 'aria-label' ) );
if ( '' !== $label ) {
return $label;
}
$by = self::labelledby_text( $el, $xp );
if ( '' !== $by ) {
return $by;
}
$text = self::accessible_text( $el );
if ( '' !== $text ) {
return $text;
}
return trim( $el->getAttribute( 'title' ) );
}
/**
* Whether an element is natively focusable.
*
* @param \DOMElement $el Element.
* @return bool
*/
private static function is_focusable( $el ) {
$tag = strtolower( $el->nodeName );
if ( $el->hasAttribute( 'tabindex' ) && (int) $el->getAttribute( 'tabindex' ) >= 0 ) {
return true;
}
if ( 'a' === $tag && $el->hasAttribute( 'href' ) ) {
return true;
}
return in_array( $tag, array( 'button', 'input', 'select', 'textarea' ), true ) && ! $el->hasAttribute( 'disabled' );
}
/**
* Collect @type values from JSON-LD.
*
* @param mixed $json Decoded JSON.
* @param int $depth Depth guard.
* @return array
*/
private static function jsonld_types( $json, $depth = 0 ) {
$types = array();
if ( ! is_array( $json ) || $depth > 4 ) {
return $types;
}
if ( isset( $json['@type'] ) ) {
foreach ( (array) $json['@type'] as $t ) {
if ( is_string( $t ) ) {
$types[] = sanitize_text_field( $t );
}
}
}
foreach ( array( '@graph' ) as $k ) {
if ( isset( $json[ $k ] ) && is_array( $json[ $k ] ) ) {
foreach ( $json[ $k ] as $node ) {
$types = array_merge( $types, self::jsonld_types( $node, $depth + 1 ) );
}
}
}
if ( isset( $json[0] ) ) {
foreach ( $json as $node ) {
$types = array_merge( $types, self::jsonld_types( $node, $depth + 1 ) );
}
}
return $types;
}
/**
* Parse a colour declared in an inline style.
*
* @param string $style Style attribute (lowercase).
* @param string $prop Property.
* @return array|null RGB.
*/
private static function style_color( $style, $prop ) {
if ( ! preg_match( '/(?:^|;)\s*' . preg_quote( $prop, '/' ) . '\s*:\s*([^;!]+)/', $style, $m ) ) {
return null;
}
$value = trim( $m[1] );
if ( preg_match( '/#([0-9a-f]{3}|[0-9a-f]{6})\b/', $value, $hex ) ) {
$h = $hex[1];
if ( 3 === strlen( $h ) ) {
$h = $h[0] . $h[0] . $h[1] . $h[1] . $h[2] . $h[2];
}
return array( hexdec( substr( $h, 0, 2 ) ), hexdec( substr( $h, 2, 2 ) ), hexdec( substr( $h, 4, 2 ) ) );
}
if ( preg_match( '/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?/', $value, $rgb ) ) {
if ( isset( $rgb[4] ) && '' !== $rgb[4] && (float) $rgb[4] < 1 ) {
return null; // Transparency makes the result unreliable.
}
return array( min( 255, (int) $rgb[1] ), min( 255, (int) $rgb[2] ), min( 255, (int) $rgb[3] ) );
}
$named = array(
'white' => array( 255, 255, 255 ),
'black' => array( 0, 0, 0 ),
);
return isset( $named[ $value ] ) ? $named[ $value ] : null;
}
/**
* WCAG relative-luminance contrast ratio.
*
* @param array $a RGB.
* @param array $b RGB.
* @return float
*/
public static function contrast_ratio( array $a, array $b ) {
$lum = static function ( array $c ) {
$out = array();
foreach ( $c as $v ) {
$v = $v / 255;
$out[] = $v <= 0.03928 ? $v / 12.92 : pow( ( $v + 0.055 ) / 1.055, 2.4 );
}
return 0.2126 * $out[0] + 0.7152 * $out[1] + 0.0722 * $out[2];
};
$l1 = $lum( $a );
$l2 = $lum( $b );
return ( max( $l1, $l2 ) + 0.05 ) / ( min( $l1, $l2 ) + 0.05 );
}
/**
* Normalize whitespace and truncate.
*
* @param string $text Text.
* @param int $max Max chars.
* @return string
*/
private static function clean( $text, $max ) {
$text = trim( preg_replace( '/\s+/u', ' ', (string) $text ) );
return mb_strlen( $text ) > $max ? mb_substr( $text, 0, $max - 1 ) . '…' : $text;
}
/**
* WAI-ARIA 1.2 roles (plus common DPUB/graphics roles).
*
* @return array
*/
private static function valid_roles() {
return array( 'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', 'gridcell', 'group', 'heading', 'img', 'image', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math', 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', 'region', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider', 'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'graphics-document', 'graphics-object', 'graphics-symbol', 'doc-abstract', 'doc-acknowledgments', 'doc-afterword', 'doc-appendix', 'doc-backlink', 'doc-biblioentry', 'doc-bibliography', 'doc-biblioref', 'doc-chapter', 'doc-colophon', 'doc-conclusion', 'doc-cover', 'doc-credit', 'doc-credits', 'doc-dedication', 'doc-endnote', 'doc-endnotes', 'doc-epigraph', 'doc-epilogue', 'doc-errata', 'doc-example', 'doc-footnote', 'doc-foreword', 'doc-glossary', 'doc-glossref', 'doc-index', 'doc-introduction', 'doc-noteref', 'doc-notice', 'doc-pagebreak', 'doc-pagelist', 'doc-part', 'doc-preface', 'doc-prologue', 'doc-pullquote', 'doc-qna', 'doc-subtitle', 'doc-tip', 'doc-toc' );
}
/**
* WAI-ARIA 1.2 attributes.
*
* @return array
*/
private static function valid_aria_attributes() {
return array( 'aria-activedescendant', 'aria-atomic', 'aria-autocomplete', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-checked', 'aria-colcount', 'aria-colindex', 'aria-colindextext', 'aria-colspan', 'aria-controls', 'aria-current', 'aria-describedby', 'aria-description', 'aria-details', 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-expanded', 'aria-flowto', 'aria-grabbed', 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-level', 'aria-live', 'aria-modal', 'aria-multiline', 'aria-multiselectable', 'aria-orientation', 'aria-owns', 'aria-placeholder', 'aria-posinset', 'aria-pressed', 'aria-readonly', 'aria-relevant', 'aria-required', 'aria-roledescription', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-selected', 'aria-setsize', 'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext' );
}
}