1<?php 2namespace Crossjoin\Css\Format\Rule\Style; 3 4use Crossjoin\Css\Format\Rule\DeclarationAbstract; 5use Crossjoin\Css\Format\Rule\RuleAbstract; 6use Crossjoin\Css\Format\Rule\RuleGroupableInterface; 7use Crossjoin\Css\Format\Rule\TraitDeclarations; 8use Crossjoin\Css\Format\StyleSheet\StyleSheet; 9use Crossjoin\Css\Helper\Placeholder; 10 11class StyleRuleSet 12extends RuleAbstract 13implements RuleGroupableInterface 14{ 15 use TraitDeclarations; 16 17 /** 18 * @var StyleSelector[] Array of selectors for the style rule 19 */ 20 protected $selectors = []; 21 22 /** 23 * @param string|null $ruleString 24 * @param StyleSheet $styleSheet 25 */ 26 public function __construct($ruleString = null, StyleSheet $styleSheet = null) 27 { 28 if ($styleSheet !== null) { 29 $this->setStyleSheet($styleSheet); 30 } 31 if ($ruleString !== null) { 32 $ruleString = Placeholder::replaceStringsAndComments($ruleString); 33 $this->parseRuleString($ruleString); 34 } 35 } 36 37 /** 38 * Sets the selectors for the style rule. 39 * 40 * @param StyleSelector[]|StyleSelector $selectors 41 * @return $this 42 */ 43 public function setSelectors($selectors) 44 { 45 $this->selectors = []; 46 if (!is_array($selectors)) { 47 $selectors = [$selectors]; 48 } 49 foreach ($selectors as $selector) { 50 $this->addSelector($selector); 51 } 52 53 return $this; 54 } 55 56 /** 57 * Adds a selector for the style rule. 58 * 59 * @param StyleSelector $selector 60 * @return $this 61 */ 62 public function addSelector(StyleSelector $selector) 63 { 64 $this->selectors[] = $selector; 65 66 return $this; 67 } 68 69 /** 70 * Gets the selectors for the style rule. 71 * 72 * @return StyleSelector[] 73 */ 74 public function getSelectors() 75 { 76 return $this->selectors; 77 } 78 79 /** 80 * Adds a declaration to the rule. 81 * 82 * @param StyleDeclaration $declaration 83 * @return $this 84 */ 85 public function addDeclaration(DeclarationAbstract $declaration) 86 { 87 if ($declaration instanceof StyleDeclaration) { 88 $this->declarations[] = $declaration; 89 } else { 90 throw new \InvalidArgumentException( 91 "Invalid declaration instance. Instance of 'StyleDeclaration' expected." 92 ); 93 } 94 95 return $this; 96 } 97 98 /** 99 * Parses the selector rule. 100 * 101 * @param string $ruleString 102 */ 103 protected function parseRuleString($ruleString) 104 { 105 foreach ($this->getSelectorStrings($ruleString) as $selectorString) 106 { 107 // Check for invalid selector (e.g. if starting with a comma, like in this example from 108 // the spec ",all { body { background:lime } }") 109 if ($selectorString === "") { 110 $this->setIsValid(false); 111 $this->addValidationError("Invalid selector at '$ruleString'."); 112 break; 113 } 114 115 $this->addSelector(new StyleSelector($selectorString, $this->getStyleSheet())); 116 } 117 } 118 119 /** 120 * Helper method to parse the style rule. 121 * 122 * @param string $selectorList 123 * @return array 124 */ 125 protected function getSelectorStrings($selectorList) 126 { 127 $charset = $this->getCharset(); 128 129 $groupsOpened = 0; 130 $ignoreGroupOpenCloseChar = false; 131 $enclosedChar = null; 132 133 $selectors = []; 134 $subSelectorList = ""; 135 $currentSelectors = [""]; 136 $currentSelectorKeys = [0]; 137 $lastChar = null; 138 139 if (preg_match('/[^\x00-\x7f]/', $selectorList)) { 140 $isAscii = false; 141 $strLen = mb_strlen($selectorList, $charset); 142 } else { 143 $isAscii = true; 144 $strLen = strlen($selectorList); 145 } 146 147 for ($i = 0, $j = $strLen; $i < $j; $i++) { 148 if ($isAscii === true) { 149 $char = $selectorList[$i]; 150 } else { 151 $char = mb_substr($selectorList, $i, 1, $charset); 152 } 153 154 if ($char === "(") { 155 if ($groupsOpened > 0) { 156 $subSelectorList .= $char; 157 } else { 158 if ($ignoreGroupOpenCloseChar === false) { 159 foreach ($currentSelectorKeys as $index) { 160 $currentSelectors[$index] .= $char; 161 } 162 } 163 } 164 $groupsOpened++; 165 } else if ($char === ")") { 166 $groupsOpened--; 167 if ($groupsOpened > 0) { 168 $subSelectorList .= $char; 169 } else { 170 if ($subSelectorList != "") { 171 $subSelectors = $this->getSelectorStrings($subSelectorList); 172 $newSelectors = []; 173 foreach ($subSelectors as $subSelector) { 174 foreach ($currentSelectors as $currentSelector) { 175 $concat = $lastChar === "(" ? "" : " "; 176 $newSelectors[] = $currentSelector . $concat . $subSelector; 177 } 178 } 179 $currentSelectors = $newSelectors; 180 $currentSelectorKeys = array_keys($currentSelectors); 181 } 182 183 if ($ignoreGroupOpenCloseChar === false) { 184 foreach ($currentSelectorKeys as $index) { 185 $currentSelectors[$index] .= $char; 186 } 187 } else { 188 $ignoreGroupOpenCloseChar = false; 189 } 190 $subSelectorList = ""; 191 } 192 } else if ($char === ",") { 193 if ($groupsOpened > 0) { 194 $subSelectorList .= $char; 195 } else { 196 foreach ($currentSelectors as $currentSelector) { 197 $selectors[] = trim($currentSelector, " \r\n\t\f"); 198 } 199 $currentSelectors = [""]; 200 $currentSelectorKeys = [0]; 201 } 202 } else if ($char === ":") { 203 if ($groupsOpened > 0) { 204 $subSelectorList .= $char; 205 } else { 206 if ($isAscii === true) { 207 $nextChars = substr($selectorList, $i); 208 } else { 209 $nextChars = mb_substr($selectorList, $i, null, $charset); 210 } 211 $nextChars = strtolower(preg_replace('/^(\:[-a-z]+)[^-a-z]*.*/', '\\1', $nextChars)); 212 213 if ($nextChars === ":matches") { 214 $i += (9 - 2); 215 $ignoreGroupOpenCloseChar = true; 216 } elseif ($nextChars === ":has") { 217 $i += (5 - 2); 218 $ignoreGroupOpenCloseChar = true; 219 } else { 220 foreach ($currentSelectorKeys as $index) { 221 $currentSelectors[$index] .= $char; 222 } 223 } 224 } 225 } else { 226 if ($groupsOpened > 0) { 227 $subSelectorList .= $char; 228 } else { 229 foreach ($currentSelectorKeys as $index) { 230 $currentSelectors[$index] .= $char; 231 } 232 } 233 } 234 235 // Save last char (to avoid costly mb_substr() call) 236 $lastChar = $char; 237 } 238 foreach ($currentSelectors as $currentSelector) { 239 $selectors[] = trim($currentSelector, " \r\n\t\f"); 240 } 241 242 return $selectors; 243 } 244}