|
3 | 3 | namespace EA11y\Modules\Remediation\Actions; |
4 | 4 |
|
5 | 5 | use DOMDocument; |
| 6 | +use DOMElement; |
6 | 7 | use EA11y\Modules\Remediation\Classes\Remediation_Base; |
7 | 8 |
|
8 | 9 | if ( ! defined( 'ABSPATH' ) ) { |
|
14 | 15 | */ |
15 | 16 | class Styles extends Remediation_Base { |
16 | 17 | public static string $type = 'styles'; |
| 18 | + public static string $style_id = 'ea11y-remediation-styles'; |
17 | 19 |
|
18 | | - public function __construct( DOMDocument $dom, $data ) { |
19 | | - parent::__construct( $dom, $data ); |
20 | 20 |
|
21 | | - $this->use_frontend = true; |
| 21 | + /** |
| 22 | + * Build a CSS selector string for a given DOMElement. |
| 23 | + * |
| 24 | + * @param DOMElement|null $element |
| 25 | + * @return string|null |
| 26 | + */ |
| 27 | + public function get_element_css_selector( ?DOMElement $element ): ?string { |
| 28 | + if ( ! $element ) { |
| 29 | + return null; |
| 30 | + } |
| 31 | + |
| 32 | + $parts = []; |
| 33 | + |
| 34 | + while ( $element && XML_ELEMENT_NODE === $element->nodeType ) { |
| 35 | + $selector = strtolower( $element->tagName ); |
| 36 | + |
| 37 | + // If element has ID, stop here |
| 38 | + if ( $element->hasAttribute( 'id' ) ) { |
| 39 | + $selector .= '#' . $element->getAttribute( 'id' ); |
| 40 | + array_unshift( $parts, $selector ); |
| 41 | + break; |
| 42 | + } |
| 43 | + |
| 44 | + // Add classes unless body |
| 45 | + if ( $element->hasAttribute( 'class' ) && strtolower( $element->tagName ) !== 'body' ) { |
| 46 | + $classes = preg_split( '/\s+/', trim( $element->getAttribute( 'class' ) ) ); |
| 47 | + if ( ! empty( $classes ) ) { |
| 48 | + $selector .= '.' . implode( '.', $classes ); |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + // Add nth-of-type if needed |
| 53 | + $parent = $element->parentNode; |
| 54 | + if ( $parent instanceof DOMElement ) { |
| 55 | + $tag_name = $element->tagName; |
| 56 | + $siblings = []; |
| 57 | + foreach ( $parent->childNodes as $child ) { |
| 58 | + if ( $child instanceof DOMElement && $child->tagName === $tag_name ) { |
| 59 | + $siblings[] = $child; |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + if ( count( $siblings ) > 1 ) { |
| 64 | + foreach ( $siblings as $i => $sibling ) { |
| 65 | + if ( $sibling->isSameNode( $element ) ) { |
| 66 | + $selector .= ':nth-of-type(' . ( $i + 1 ) . ')'; |
| 67 | + break; |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + array_unshift( $parts, $selector ); |
| 74 | + $element = $element->parentNode instanceof DOMElement ? $element->parentNode : null; |
| 75 | + } |
| 76 | + |
| 77 | + return implode( ' > ', $parts ); |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Replace CSS selectors for color and background-color rules. |
| 82 | + * |
| 83 | + * @param string $css The original CSS string |
| 84 | + * @param string|null $color_selector New selector for color rule |
| 85 | + * @param string|null $bg_selector New selector for background-color rule |
| 86 | + * @return string The modified CSS |
| 87 | + */ |
| 88 | + public function replace_css_selectors( |
| 89 | + string $css, |
| 90 | + ?string $color_selector = null, |
| 91 | + ?string $bg_selector = null |
| 92 | + ): string { |
| 93 | + // Match full CSS blocks like "selector { ... }" |
| 94 | + preg_match_all( '/([^{]+)\{([^}]+)\}/', $css, $matches, PREG_SET_ORDER ); |
| 95 | + |
| 96 | + $result = ''; |
| 97 | + |
| 98 | + foreach ( $matches as $match ) { |
| 99 | + $rules = trim( $match[2] ); |
| 100 | + |
| 101 | + // Find color value |
| 102 | + if ( $color_selector && preg_match( '/(?<!-)\bcolor\s*:\s*([#a-zA-Z0-9(),.\s%-]+)/i', $rules, $color_match ) ) { |
| 103 | + $color_value = trim( $color_match[1] ); |
| 104 | + $result .= "{$color_selector} { color: {$color_value} !important; }\n"; |
| 105 | + } |
| 106 | + |
| 107 | + // Find background-color value |
| 108 | + if ( $bg_selector && preg_match( '/background-color\s*:\s*([#a-zA-Z0-9(),.\s%-]+)/i', $rules, $bg_match ) ) { |
| 109 | + $bg_value = trim( $bg_match[1] ); |
| 110 | + $result .= "{$bg_selector} { background-color: {$bg_value} !important; }\n"; |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + return trim( $result ); |
22 | 115 | } |
23 | 116 |
|
| 117 | + |
24 | 118 | public function run() : ?DOMDocument { |
| 119 | + $rule = $this->data['rule']; |
| 120 | + if ( $this->data['global'] ) { |
| 121 | + $el_color = $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] ); |
| 122 | + $el_bg = $this->get_element_by_xpath_with_snippet_fallback( $this->data['parentXPath'], $this->data['parentFind'] ); |
| 123 | + $color_css_selector = $this->get_element_css_selector( $el_color ); |
| 124 | + $bg_css_selector = $this->get_element_css_selector( $el_bg ); |
| 125 | + |
| 126 | + if ( ! $color_css_selector && ! $bg_css_selector ) { |
| 127 | + $this->use_frontend = true; |
| 128 | + return null; |
| 129 | + } |
| 130 | + |
| 131 | + $rule = $this->replace_css_selectors( $rule, $color_css_selector, $bg_css_selector ); |
| 132 | + } |
| 133 | + |
| 134 | + // Find or create <head> element |
| 135 | + $head = $this->dom->getElementsByTagName( 'head' )->item( 0 ); |
| 136 | + if ( ! $head ) { |
| 137 | + $head = $this->dom->createElement( 'head' ); |
| 138 | + $html_element = $this->dom->getElementsByTagName( 'html' )->item( 0 ); |
| 139 | + if ( $html_element ) { |
| 140 | + $html_element->insertBefore( $head, $this->dom->getElementsByTagName( 'body' )->item( 0 ) ); |
| 141 | + } else { |
| 142 | + $this->dom->appendChild( $head ); |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // Create <style> tag |
| 147 | + $style = $this->dom->getElementById( self::$style_id ); |
| 148 | + if ( ! $style ) { |
| 149 | + $style = $this->dom->createElement( 'style' ); |
| 150 | + } |
| 151 | + $style->setAttribute( 'id', self::$style_id ); |
| 152 | + $style->appendChild( $this->dom->createTextNode( $rule ) ); |
| 153 | + |
| 154 | + // Append to the end of <head> |
| 155 | + $head->appendChild( $style ); |
| 156 | + |
25 | 157 | return $this->dom; |
26 | 158 | } |
27 | 159 | } |
0 commit comments