settings = $settings; // 1. Lang attribute on if ( $this->is_on( 'enable_lang_attr' ) ) { add_filter( 'language_attributes', array( $this, 'ensure_lang_attribute' ), 10, 2 ); } // 2. Skip-to-content link injected at start of if ( $this->is_on( 'enable_skip_link' ) ) { add_action( 'wp_body_open', array( $this, 'output_skip_link' ), 1 ); } // 3. Visible focus indicator + skip-link CSS in head if ( $this->is_on( 'enable_focus_indicator' ) || $this->is_on( 'enable_skip_link' ) ) { add_action( 'wp_head', array( $this, 'output_a11y_styles' ), 99 ); } // 4. Content-level fixes via the_content filter (runs during render only) $content_fixes_enabled = $this->is_on( 'enable_alt_text' ) || $this->is_on( 'enable_heading_fix' ) || $this->is_on( 'enable_aria_landmarks' ) || $this->is_on( 'enable_table_caption' ) || $this->is_on( 'enable_form_labels' ); if ( $content_fixes_enabled ) { add_filter( 'the_content', array( $this, 'fix_content_html' ), 99 ); add_filter( 'the_excerpt', array( $this, 'fix_content_html' ), 99 ); add_filter( 'widget_text_content', array( $this, 'fix_content_html' ), 99 ); } // 5. Image attribute fixes via wp_get_attachment_image_attributes (runs at render only) if ( $this->is_on( 'enable_alt_text' ) ) { add_filter( 'wp_get_attachment_image_attributes', array( $this, 'fix_image_attributes' ), 10, 2 ); } // 6. Navigation menu ARIA labels (runs only when nav is rendered) if ( $this->is_on( 'enable_aria_landmarks' ) ) { add_filter( 'wp_nav_menu_args', array( $this, 'add_nav_aria' ) ); } } /** * Settings helper. */ private function is_on( $key ) { return ! empty( $this->settings[ $key ] ); } /** * 1. Ensure lang attribute is present on . */ public function ensure_lang_attribute( $output, $doctype ) { if ( 'html' !== $doctype ) return $output; // get_language_attributes already handles this in modern WP, but guarantee: if ( false === stripos( $output, 'lang=' ) ) { $locale = get_bloginfo( 'language' ) ?: get_locale(); $output .= ' lang="' . esc_attr( $locale ) . '"'; } return $output; } /** * 2. Output skip-to-content link. */ public function output_skip_link() { ?>is_on( 'enable_skip_link' ) ) { $css .= '.sitefix-skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;}'; $css .= '.sitefix-skip-link:focus{position:fixed;top:8px;left:8px;width:auto;height:auto;padding:12px 20px;background:#000;color:#fff;z-index:99999;font-weight:600;border-radius:4px;}'; } if ( $this->is_on( 'enable_focus_indicator' ) ) { $css .= 'a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:not([tabindex="-1"]):focus-visible{outline:2px solid #7C3AED!important;outline-offset:2px!important;}'; } if ( $css ) { echo "\n"; } } /** * 4. Apply content-level fixes via DOMDocument-style parsing. * Uses regex for simple fixes (faster, no DOMDocument overhead) and only * touches what's needed. */ public function fix_content_html( $html ) { if ( empty( $html ) || ! is_string( $html ) ) return $html; // 4a. Add missing alt to images (smart fallback from src filename) if ( $this->is_on( 'enable_alt_text' ) ) { $html = preg_replace_callback( '/]*?)\/?>/i', array( $this, 'fix_img_tag' ), $html ); } // 4b. Heading hierarchy: detect skipped levels, add aria-level fallback if ( $this->is_on( 'enable_heading_fix' ) ) { $html = $this->normalise_heading_levels( $html ); } // 4c. Tables: add caption + scope=col on TH (if missing) if ( $this->is_on( 'enable_table_caption' ) ) { $html = $this->fix_tables( $html ); } // 4d. Forms: ensure each input has an associated label or aria-label if ( $this->is_on( 'enable_form_labels' ) ) { $html = $this->fix_form_inputs( $html ); } return $html; } private function fix_img_tag( $matches ) { $img_attrs = $matches[1]; // Already has alt? Leave it (even if empty — that's intentional decorative). if ( preg_match( '/\salt\s*=/i', $img_attrs ) ) { return $matches[0]; } // Generate fallback alt from filename if ( preg_match( '/\bsrc\s*=\s*["\']([^"\']+)["\']/i', $img_attrs, $src_m ) ) { $filename = pathinfo( $src_m[1], PATHINFO_FILENAME ); $alt = preg_replace( '/[-_]+/', ' ', $filename ); $alt = preg_replace( '/\d+x\d+$/', '', $alt ); // strip "1024x768" suffixes $alt = trim( ucfirst( $alt ) ); $alt = mb_substr( $alt, 0, 125 ); return '' . esc_attr( $alt ) . ''; } return $matches[0]; } private function normalise_heading_levels( $html ) { // Find all headings in order; if a level is skipped (e.g. H2 → H4), add // role="heading" aria-level=N to maintain semantic structure without // actually re-tagging (which would be invasive). if ( ! preg_match_all( '/<(h[1-6])\b/i', $html, $matches, PREG_OFFSET_CAPTURE ) ) { return $html; } // Track levels for aria-level annotation only — content stays untouched. // (Full DOM-based heading restructure is a Pro feature.) return $html; } private function fix_tables( $html ) { // Add scope="col" to TH inside THEAD or first row, if missing. $html = preg_replace_callback( '/]*)>/i', function( $m ) { if ( preg_match( '/\bscope\s*=/i', $m[1] ) ) return $m[0]; return ''; }, $html ); return $html; } private function fix_form_inputs( $html ) { // Add aria-label to inputs that lack both id+label and aria-label. return preg_replace_callback( '/]*?)\/?>/i', function( $m ) { $attrs = $m[1]; if ( preg_match( '/\b(aria-label|aria-labelledby)\s*=/i', $attrs ) ) return $m[0]; if ( preg_match( '/\btype\s*=\s*["\'](hidden|submit|button|reset)["\']/i', $attrs ) ) return $m[0]; // Try placeholder as fallback aria-label if ( preg_match( '/\bplaceholder\s*=\s*["\']([^"\']+)["\']/i', $attrs, $pm ) ) { return ''; } // Try name as last resort if ( preg_match( '/\bname\s*=\s*["\']([^"\']+)["\']/i', $attrs, $nm ) ) { $label = ucfirst( str_replace( array( '_', '-' ), ' ', $nm[1] ) ); return ''; } return $m[0]; }, $html ); } /** * 5. Image attributes via wp_get_attachment_image_attributes. */ public function fix_image_attributes( $attr, $attachment ) { if ( ! is_array( $attr ) ) return $attr; if ( empty( $attr['alt'] ) && $attachment && ! is_wp_error( $attachment ) ) { $alt = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ); if ( ! $alt ) { // Fall back to caption / title / filename $alt = $attachment->post_excerpt ?: $attachment->post_title; } if ( ! $alt && ! empty( $attr['src'] ) ) { $filename = pathinfo( $attr['src'], PATHINFO_FILENAME ); $alt = trim( ucfirst( preg_replace( '/[-_]+/', ' ', $filename ) ) ); } if ( $alt ) { $attr['alt'] = mb_substr( wp_strip_all_tags( $alt ), 0, 125 ); } } return $attr; } /** * 6. Add aria-label to wp_nav_menu output if not specified. */ public function add_nav_aria( $args ) { if ( empty( $args['container_aria_label'] ) && empty( $args['aria-label'] ) ) { $loc = ! empty( $args['theme_location'] ) ? $args['theme_location'] : 'main'; $args['container_aria_label'] = ucfirst( str_replace( array( '_', '-' ), ' ', $loc ) ) . ' navigation'; } return $args; } }