]+))/i', $tag, $m ) ) { $value = ''; foreach ( array( 1, 2, 3 ) as $i ) { if ( isset( $m[ $i ] ) && '' !== $m[ $i ] ) { $value = $m[ $i ]; break; } } return html_entity_decode( $value, ENT_QUOTES, 'UTF-8' ); } if ( preg_match( '/\s' . preg_quote( $name, '/' ) . '(?=[\s>\/])/i', $tag ) ) { return ''; } return null; } /** * Checks a fragment of HTML (post content). * * @param string $html Post content. * @return array[] List of issues: array( 'code' => string, 'level' => 'error'|'warning', 'detail' => string ). */ function agevole_a11y_check_html( $html ) { $issues = array(); $html = (string) $html; // Images: missing alt attribute (error) or empty alt (fine only for decorative images). if ( preg_match_all( '/]*>/i', $html, $imgs ) ) { foreach ( $imgs[0] as $img ) { $alt = agevole_a11y_attr( $img, 'alt' ); $src = (string) agevole_a11y_attr( $img, 'src' ); $src = wp_basename( (string) wp_parse_url( $src, PHP_URL_PATH ) ); if ( null === $alt ) { $issues[] = array( 'code' => 'img_no_alt', 'level' => 'error', 'detail' => $src, ); } elseif ( '' === trim( $alt ) && 'presentation' !== agevole_a11y_attr( $img, 'role' ) ) { $issues[] = array( 'code' => 'img_empty_alt', 'level' => 'warning', 'detail' => $src, ); } } } // Headings: levels must not be skipped going down (h2 -> h4); h1 is usually the page title. if ( preg_match_all( '/ 'heading_h1', 'level' => 'warning', 'detail' => 'H1', ); } elseif ( $level > $prev + 1 ) { $issues[] = array( 'code' => 'heading_skip', 'level' => 'error', 'detail' => 'H' . $prev . ' → H' . $level, ); } $prev = $level; } } // Links: empty links and texts that do not describe the destination. if ( preg_match_all( '/]*)>(.*?)<\/a>/is', $html, $links, PREG_SET_ORDER ) ) { $generic = agevole_a11y_generic_link_texts(); foreach ( $links as $link ) { $tag = ''; if ( null === agevole_a11y_attr( $tag, 'href' ) ) { continue; } $label = trim( (string) agevole_a11y_attr( $tag, 'aria-label' ) ); $text = trim( preg_replace( '/\s+/u', ' ', html_entity_decode( wp_strip_all_tags( $link[2] ), ENT_QUOTES, 'UTF-8' ) ) ); if ( '' === $text && preg_match_all( '/]*>/i', $link[2], $inner ) ) { foreach ( $inner[0] as $img ) { $text .= trim( (string) agevole_a11y_attr( $img, 'alt' ) ); } } if ( '' === $text && '' === $label ) { $issues[] = array( 'code' => 'link_empty', 'level' => 'error', 'detail' => (string) agevole_a11y_attr( $tag, 'href' ), ); continue; } $normalized = trim( function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text ), " \t\n\r\0\x0B.:!?»›→>…" ); if ( '' === $label && in_array( $normalized, $generic, true ) ) { $issues[] = array( 'code' => 'link_generic', 'level' => 'warning', 'detail' => $text, ); } } } // Data tables without header cells. if ( preg_match_all( '//is', $html, $tables ) ) { foreach ( $tables[0] as $table ) { if ( ! preg_match( '/' ) . '>', 'role' ) ) { $issues[] = array( 'code' => 'table_no_th', 'level' => 'warning', 'detail' => '', ); } } } return $issues; } /** * Human-readable message for an issue. * * @param array $issue Issue from agevole_a11y_check_html(). * @return string */ function agevole_a11y_issue_message( $issue ) { switch ( $issue['code'] ) { case 'img_no_alt': /* translators: %s: image file name. */ return sprintf( __( 'Image without alternative text: %s', 'agevole-accessibility' ), $issue['detail'] ); case 'img_empty_alt': /* translators: %s: image file name. */ return sprintf( __( 'Image with empty alternative text (fine only if it is decorative): %s', 'agevole-accessibility' ), $issue['detail'] ); case 'heading_h1': return __( 'H1 heading in the content: the page title is usually already the H1.', 'agevole-accessibility' ); case 'heading_skip': /* translators: %s: heading levels, for example "H2 → H4". */ return sprintf( __( 'Skipped heading level: %s', 'agevole-accessibility' ), $issue['detail'] ); case 'link_empty': /* translators: %s: link address. */ return sprintf( __( 'Link without text: %s', 'agevole-accessibility' ), $issue['detail'] ); case 'link_generic': /* translators: %s: link text. */ return sprintf( __( 'Link text does not say where it goes: "%s"', 'agevole-accessibility' ), $issue['detail'] ); case 'table_no_th': return __( 'Table without header cells.', 'agevole-accessibility' ); } return $issue['code']; }