See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
1 <?php 2 3 namespace Sabberworm\CSS\RuleSet; 4 5 use Sabberworm\CSS\Comment\Comment; 6 use Sabberworm\CSS\Comment\Commentable; 7 use Sabberworm\CSS\OutputFormat; 8 use Sabberworm\CSS\Parsing\ParserState; 9 use Sabberworm\CSS\Parsing\UnexpectedEOFException; 10 use Sabberworm\CSS\Parsing\UnexpectedTokenException; 11 use Sabberworm\CSS\Renderable; 12 use Sabberworm\CSS\Rule\Rule; 13 14 /** 15 * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block. 16 * However, unknown At-Rules (like `@font-face`) are also rule sets. 17 */ 18 abstract class RuleSet implements Renderable, Commentable 19 { 20 /** 21 * @var array<string, Rule> 22 */ 23 private $aRules; 24 25 /** 26 * @var int 27 */ 28 protected $iLineNo; 29 30 /** 31 * @var array<array-key, Comment> 32 */ 33 protected $aComments; 34 35 /** 36 * @param int $iLineNo 37 */ 38 public function __construct($iLineNo = 0) 39 { 40 $this->aRules = []; 41 $this->iLineNo = $iLineNo; 42 $this->aComments = []; 43 } 44 45 /** 46 * @return void 47 * 48 * @throws UnexpectedTokenException 49 * @throws UnexpectedEOFException 50 */ 51 public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) 52 { 53 while ($oParserState->comes(';')) { 54 $oParserState->consume(';'); 55 } 56 while (!$oParserState->comes('}')) { 57 $oRule = null; 58 if ($oParserState->getSettings()->bLenientParsing) { 59 try { 60 $oRule = Rule::parse($oParserState); 61 } catch (UnexpectedTokenException $e) { 62 try { 63 $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true); 64 // We need to “unfind” the matches to the end of the ruleSet as this will be matched later 65 if ($oParserState->streql(substr($sConsume, -1), '}')) { 66 $oParserState->backtrack(1); 67 } else { 68 while ($oParserState->comes(';')) { 69 $oParserState->consume(';'); 70 } 71 } 72 } catch (UnexpectedTokenException $e) { 73 // We’ve reached the end of the document. Just close the RuleSet. 74 return; 75 } 76 } 77 } else { 78 $oRule = Rule::parse($oParserState); 79 } 80 if ($oRule) { 81 $oRuleSet->addRule($oRule); 82 } 83 } 84 $oParserState->consume('}'); 85 } 86 87 /** 88 * @return int 89 */ 90 public function getLineNo() 91 { 92 return $this->iLineNo; 93 } 94 95 /** 96 * @param Rule|null $oSibling 97 * 98 * @return void 99 */ 100 public function addRule(Rule $oRule, Rule $oSibling = null) 101 { 102 $sRule = $oRule->getRule(); 103 if (!isset($this->aRules[$sRule])) { 104 $this->aRules[$sRule] = []; 105 } 106 107 $iPosition = count($this->aRules[$sRule]); 108 109 if ($oSibling !== null) { 110 $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); 111 if ($iSiblingPos !== false) { 112 $iPosition = $iSiblingPos; 113 $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); 114 } 115 } 116 if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) { 117 //this node is added manually, give it the next best line 118 $rules = $this->getRules(); 119 $pos = count($rules); 120 if ($pos > 0) { 121 $last = $rules[$pos - 1]; 122 $oRule->setPosition($last->getLineNo() + 1, 0); 123 } 124 } 125 126 array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); 127 } 128 129 /** 130 * Returns all rules matching the given rule name 131 * 132 * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). 133 * 134 * @example $oRuleSet->getRules('font-') 135 * //returns an array of all rules either beginning with font- or matching font. 136 * 137 * @param Rule|string|null $mRule 138 * Pattern to search for. If null, returns all rules. 139 * If the pattern ends with a dash, all rules starting with the pattern are returned 140 * as well as one matching the pattern with the dash excluded. 141 * Passing a Rule behaves like calling `getRules($mRule->getRule())`. 142 * 143 * @return array<int, Rule> 144 */ 145 public function getRules($mRule = null) 146 { 147 if ($mRule instanceof Rule) { 148 $mRule = $mRule->getRule(); 149 } 150 /** @var array<int, Rule> $aResult */ 151 $aResult = []; 152 foreach ($this->aRules as $sName => $aRules) { 153 // Either no search rule is given or the search rule matches the found rule exactly 154 // or the search rule ends in “-” and the found rule starts with the search rule. 155 if ( 156 !$mRule || $sName === $mRule 157 || ( 158 strrpos($mRule, '-') === strlen($mRule) - strlen('-') 159 && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)) 160 ) 161 ) { 162 $aResult = array_merge($aResult, $aRules); 163 } 164 } 165 usort($aResult, function (Rule $first, Rule $second) { 166 if ($first->getLineNo() === $second->getLineNo()) { 167 return $first->getColNo() - $second->getColNo(); 168 } 169 return $first->getLineNo() - $second->getLineNo(); 170 }); 171 return $aResult; 172 } 173 174 /** 175 * Overrides all the rules of this set. 176 * 177 * @param array<array-key, Rule> $aRules The rules to override with. 178 * 179 * @return void 180 */ 181 public function setRules(array $aRules) 182 { 183 $this->aRules = []; 184 foreach ($aRules as $rule) { 185 $this->addRule($rule); 186 } 187 } 188 189 /** 190 * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name 191 * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. 192 * 193 * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block 194 * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array 195 * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. 196 * 197 * @param Rule|string|null $mRule $mRule 198 * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, 199 * all rules starting with the pattern are returned as well as one matching the pattern with the dash 200 * excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`. 201 * 202 * @return array<string, Rule> 203 */ 204 public function getRulesAssoc($mRule = null) 205 { 206 /** @var array<string, Rule> $aResult */ 207 $aResult = []; 208 foreach ($this->getRules($mRule) as $oRule) { 209 $aResult[$oRule->getRule()] = $oRule; 210 } 211 return $aResult; 212 } 213 214 /** 215 * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts. 216 * 217 * If given a Rule, it will only remove this particular rule (by identity). 218 * If given a name, it will remove all rules by that name. 219 * 220 * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would 221 * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`. 222 * 223 * @param Rule|string|null $mRule 224 * pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, 225 * all rules starting with the pattern are removed as well as one matching the pattern with the dash 226 * excluded. Passing a Rule behaves matches by identity. 227 * 228 * @return void 229 */ 230 public function removeRule($mRule) 231 { 232 if ($mRule instanceof Rule) { 233 $sRule = $mRule->getRule(); 234 if (!isset($this->aRules[$sRule])) { 235 return; 236 } 237 foreach ($this->aRules[$sRule] as $iKey => $oRule) { 238 if ($oRule === $mRule) { 239 unset($this->aRules[$sRule][$iKey]); 240 } 241 } 242 } else { 243 foreach ($this->aRules as $sName => $aRules) { 244 // Either no search rule is given or the search rule matches the found rule exactly 245 // or the search rule ends in “-” and the found rule starts with the search rule or equals it 246 // (without the trailing dash). 247 if ( 248 !$mRule || $sName === $mRule 249 || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') 250 && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))) 251 ) { 252 unset($this->aRules[$sName]); 253 } 254 } 255 } 256 } 257 258 /** 259 * @return string 260 */ 261 public function __toString() 262 { 263 return $this->render(new OutputFormat()); 264 } 265 266 /** 267 * @return string 268 */ 269 public function render(OutputFormat $oOutputFormat) 270 { 271 $sResult = ''; 272 $bIsFirst = true; 273 foreach ($this->aRules as $aRules) { 274 foreach ($aRules as $oRule) { 275 $sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) { 276 return $oRule->render($oOutputFormat->nextLevel()); 277 }); 278 if ($sRendered === null) { 279 continue; 280 } 281 if ($bIsFirst) { 282 $bIsFirst = false; 283 $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); 284 } else { 285 $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); 286 } 287 $sResult .= $sRendered; 288 } 289 } 290 291 if (!$bIsFirst) { 292 // Had some output 293 $sResult .= $oOutputFormat->spaceAfterRules(); 294 } 295 296 return $oOutputFormat->removeLastSemicolon($sResult); 297 } 298 299 /** 300 * @param array<string, Comment> $aComments 301 * 302 * @return void 303 */ 304 public function addComments(array $aComments) 305 { 306 $this->aComments = array_merge($this->aComments, $aComments); 307 } 308 309 /** 310 * @return array<string, Comment> 311 */ 312 public function getComments() 313 { 314 return $this->aComments; 315 } 316 317 /** 318 * @param array<string, Comment> $aComments 319 * 320 * @return void 321 */ 322 public function setComments(array $aComments) 323 { 324 $this->aComments = $aComments; 325 } 326 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body