← All changes
|
back/Libraries/Sabberworm/CSS/src/RuleSet/RuleSet.php
+0
-375
11.0.2
→
11.1.1
View file →
| @@ -1,375 +1,0 @@ | ||
| 1 | -<?php | |
| 2 | - | |
| 3 | -declare(strict_types=1); | |
| 4 | - | |
| 5 | -namespace AcyMailing\Libraries\Sabberworm\CSS\RuleSet; | |
| 6 | - | |
| 7 | -use AcyMailing\Libraries\Sabberworm\CSS\Comment\CommentContainer; | |
| 8 | -use AcyMailing\Libraries\Sabberworm\CSS\CSSElement; | |
| 9 | -use AcyMailing\Libraries\Sabberworm\CSS\CSSList\CSSListItem; | |
| 10 | -use AcyMailing\Libraries\Sabberworm\CSS\OutputFormat; | |
| 11 | -use AcyMailing\Libraries\Sabberworm\CSS\Parsing\ParserState; | |
| 12 | -use AcyMailing\Libraries\Sabberworm\CSS\Parsing\UnexpectedEOFException; | |
| 13 | -use AcyMailing\Libraries\Sabberworm\CSS\Parsing\UnexpectedTokenException; | |
| 14 | -use AcyMailing\Libraries\Sabberworm\CSS\Position\Position; | |
| 15 | -use AcyMailing\Libraries\Sabberworm\CSS\Position\Positionable; | |
| 16 | -use AcyMailing\Libraries\Sabberworm\CSS\Rule\Rule; | |
| 17 | - | |
| 18 | -/** | |
| 19 | - * This class is a container for individual 'Rule's. | |
| 20 | - * | |
| 21 | - * The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`. | |
| 22 | - * However, unknown `AtRule`s (like `@font-face`) are rule sets as well. | |
| 23 | - * | |
| 24 | - * If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` | |
| 25 | - * (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules). | |
| 26 | - * | |
| 27 | - * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. | |
| 28 | - */ | |
| 29 | -class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer | |
| 30 | -{ | |
| 31 | - use CommentContainer; | |
| 32 | - use Position; | |
| 33 | - | |
| 34 | - /** | |
| 35 | - * the rules in this rule set, using the property name as the key, | |
| 36 | - * with potentially multiple rules per property name. | |
| 37 | - * | |
| 38 | - * @var array<string, array<int<0, max>, Rule>> | |
| 39 | - */ | |
| 40 | - private $rules = []; | |
| 41 | - | |
| 42 | - /** | |
| 43 | - * @param int<1, max>|null $lineNumber | |
| 44 | - */ | |
| 45 | - public function __construct(?int $lineNumber = null) | |
| 46 | - { | |
| 47 | - $this->setPosition($lineNumber); | |
| 48 | - } | |
| 49 | - | |
| 50 | - /** | |
| 51 | - * @throws UnexpectedTokenException | |
| 52 | - * @throws UnexpectedEOFException | |
| 53 | - * | |
| 54 | - * @internal since V8.8.0 | |
| 55 | - */ | |
| 56 | - public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void | |
| 57 | - { | |
| 58 | - while ($parserState->comes(';')) { | |
| 59 | - $parserState->consume(';'); | |
| 60 | - } | |
| 61 | - while (true) { | |
| 62 | - $commentsBeforeRule = $parserState->consumeWhiteSpace(); | |
| 63 | - if ($parserState->comes('}')) { | |
| 64 | - break; | |
| 65 | - } | |
| 66 | - $rule = null; | |
| 67 | - if ($parserState->getSettings()->usesLenientParsing()) { | |
| 68 | - try { | |
| 69 | - $rule = Rule::parse($parserState, $commentsBeforeRule); | |
| 70 | - } catch (UnexpectedTokenException $e) { | |
| 71 | - try { | |
| 72 | - $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true); | |
| 73 | - // We need to “unfind” the matches to the end of the ruleSet as this will be matched later | |
| 74 | - if ($parserState->streql(\substr($consumedText, -1), '}')) { | |
| 75 | - $parserState->backtrack(1); | |
| 76 | - } else { | |
| 77 | - while ($parserState->comes(';')) { | |
| 78 | - $parserState->consume(';'); | |
| 79 | - } | |
| 80 | - } | |
| 81 | - } catch (UnexpectedTokenException $e) { | |
| 82 | - // We’ve reached the end of the document. Just close the RuleSet. | |
| 83 | - return; | |
| 84 | - } | |
| 85 | - } | |
| 86 | - } else { | |
| 87 | - $rule = Rule::parse($parserState, $commentsBeforeRule); | |
| 88 | - } | |
| 89 | - if ($rule instanceof Rule) { | |
| 90 | - $ruleSet->addRule($rule); | |
| 91 | - } | |
| 92 | - } | |
| 93 | - $parserState->consume('}'); | |
| 94 | - } | |
| 95 | - | |
| 96 | - /** | |
| 97 | - * @throws \UnexpectedValueException | |
| 98 | - * if the last `Rule` is needed as a basis for setting position, but does not have a valid position, | |
| 99 | - * which should never happen | |
| 100 | - */ | |
| 101 | - public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void | |
| 102 | - { | |
| 103 | - $propertyName = $ruleToAdd->getRule(); | |
| 104 | - if (!isset($this->rules[$propertyName])) { | |
| 105 | - $this->rules[$propertyName] = []; | |
| 106 | - } | |
| 107 | - | |
| 108 | - $position = \count($this->rules[$propertyName]); | |
| 109 | - | |
| 110 | - if ($sibling !== null) { | |
| 111 | - $siblingIsInSet = false; | |
| 112 | - $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true); | |
| 113 | - if ($siblingPosition !== false) { | |
| 114 | - $siblingIsInSet = true; | |
| 115 | - $position = $siblingPosition; | |
| 116 | - } else { | |
| 117 | - $siblingIsInSet = $this->hasRule($sibling); | |
| 118 | - if ($siblingIsInSet) { | |
| 119 | - // Maintain ordering within `$this->rules[$propertyName]` | |
| 120 | - // by inserting before first `Rule` with a same-or-later position than the sibling. | |
| 121 | - foreach ($this->rules[$propertyName] as $index => $rule) { | |
| 122 | - if (self::comparePositionable($rule, $sibling) >= 0) { | |
| 123 | - $position = $index; | |
| 124 | - break; | |
| 125 | - } | |
| 126 | - } | |
| 127 | - } | |
| 128 | - } | |
| 129 | - if ($siblingIsInSet) { | |
| 130 | - // Increment column number of all existing rules on same line, starting at sibling | |
| 131 | - $siblingLineNumber = $sibling->getLineNumber(); | |
| 132 | - $siblingColumnNumber = $sibling->getColumnNumber(); | |
| 133 | - foreach ($this->rules as $rulesForAProperty) { | |
| 134 | - foreach ($rulesForAProperty as $rule) { | |
| 135 | - if ( | |
| 136 | - $rule->getLineNumber() === $siblingLineNumber && | |
| 137 | - $rule->getColumnNumber() >= $siblingColumnNumber | |
| 138 | - ) { | |
| 139 | - $rule->setPosition($siblingLineNumber, $rule->getColumnNumber() + 1); | |
| 140 | - } | |
| 141 | - } | |
| 142 | - } | |
| 143 | - $ruleToAdd->setPosition($siblingLineNumber, $siblingColumnNumber); | |
| 144 | - } | |
| 145 | - } | |
| 146 | - | |
| 147 | - if ($ruleToAdd->getLineNumber() === null) { | |
| 148 | - //this node is added manually, give it the next best line | |
| 149 | - $columnNumber = $ruleToAdd->getColumnNumber() ?? 0; | |
| 150 | - $rules = $this->getRules(); | |
| 151 | - $rulesCount = \count($rules); | |
| 152 | - if ($rulesCount > 0) { | |
| 153 | - $last = $rules[$rulesCount - 1]; | |
| 154 | - $lastsLineNumber = $last->getLineNumber(); | |
| 155 | - if (!\is_int($lastsLineNumber)) { | |
| 156 | - throw new \UnexpectedValueException( | |
| 157 | - 'A Rule without a line number was found during addRule', | |
| 158 | - 1750718399 | |
| 159 | - ); | |
| 160 | - } | |
| 161 | - $ruleToAdd->setPosition($lastsLineNumber + 1, $columnNumber); | |
| 162 | - } else { | |
| 163 | - $ruleToAdd->setPosition(1, $columnNumber); | |
| 164 | - } | |
| 165 | - } elseif ($ruleToAdd->getColumnNumber() === null) { | |
| 166 | - $ruleToAdd->setPosition($ruleToAdd->getLineNumber(), 0); | |
| 167 | - } | |
| 168 | - | |
| 169 | - \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]); | |
| 170 | - } | |
| 171 | - | |
| 172 | - /** | |
| 173 | - * Returns all rules matching the given rule name | |
| 174 | - * | |
| 175 | - * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array(). | |
| 176 | - * | |
| 177 | - * @example $ruleSet->getRules('font-') | |
| 178 | - * //returns an array of all rules either beginning with font- or matching font. | |
| 179 | - * | |
| 180 | - * @param string|null $searchPattern | |
| 181 | - * Pattern to search for. If null, returns all rules. | |
| 182 | - * If the pattern ends with a dash, all rules starting with the pattern are returned | |
| 183 | - * as well as one matching the pattern with the dash excluded. | |
| 184 | - * | |
| 185 | - * @return array<int<0, max>, Rule> | |
| 186 | - */ | |
| 187 | - public function getRules(?string $searchPattern = null): array | |
| 188 | - { | |
| 189 | - $result = []; | |
| 190 | - foreach ($this->rules as $propertyName => $rules) { | |
| 191 | - // Either no search rule is given or the search rule matches the found rule exactly | |
| 192 | - // or the search rule ends in “-” and the found rule starts with the search rule. | |
| 193 | - if ( | |
| 194 | - $searchPattern === null || $propertyName === $searchPattern | |
| 195 | - || ( | |
| 196 | - \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') | |
| 197 | - && (\strpos($propertyName, $searchPattern) === 0 | |
| 198 | - || $propertyName === \substr($searchPattern, 0, -1)) | |
| 199 | - ) | |
| 200 | - ) { | |
| 201 | - $result = \array_merge($result, $rules); | |
| 202 | - } | |
| 203 | - } | |
| 204 | - \usort($result, [self::class, 'comparePositionable']); | |
| 205 | - | |
| 206 | - return $result; | |
| 207 | - } | |
| 208 | - | |
| 209 | - /** | |
| 210 | - * Overrides all the rules of this set. | |
| 211 | - * | |
| 212 | - * @param array<Rule> $rules The rules to override with. | |
| 213 | - */ | |
| 214 | - public function setRules(array $rules): void | |
| 215 | - { | |
| 216 | - $this->rules = []; | |
| 217 | - foreach ($rules as $rule) { | |
| 218 | - $this->addRule($rule); | |
| 219 | - } | |
| 220 | - } | |
| 221 | - | |
| 222 | - /** | |
| 223 | - * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name | |
| 224 | - * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. | |
| 225 | - * | |
| 226 | - * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block | |
| 227 | - * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array | |
| 228 | - * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. | |
| 229 | - * | |
| 230 | - * @param string|null $searchPattern | |
| 231 | - * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, | |
| 232 | - * all rules starting with the pattern are returned as well as one matching the pattern with the dash | |
| 233 | - * excluded. | |
| 234 | - * | |
| 235 | - * @return array<string, Rule> | |
| 236 | - */ | |
| 237 | - public function getRulesAssoc(?string $searchPattern = null): array | |
| 238 | - { | |
| 239 | - /** @var array<string, Rule> $result */ | |
| 240 | - $result = []; | |
| 241 | - foreach ($this->getRules($searchPattern) as $rule) { | |
| 242 | - $result[$rule->getRule()] = $rule; | |
| 243 | - } | |
| 244 | - | |
| 245 | - return $result; | |
| 246 | - } | |
| 247 | - | |
| 248 | - /** | |
| 249 | - * Removes a `Rule` from this `RuleSet` by identity. | |
| 250 | - */ | |
| 251 | - public function removeRule(Rule $ruleToRemove): void | |
| 252 | - { | |
| 253 | - $nameOfPropertyToRemove = $ruleToRemove->getRule(); | |
| 254 | - if (!isset($this->rules[$nameOfPropertyToRemove])) { | |
| 255 | - return; | |
| 256 | - } | |
| 257 | - foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) { | |
| 258 | - if ($rule === $ruleToRemove) { | |
| 259 | - unset($this->rules[$nameOfPropertyToRemove][$key]); | |
| 260 | - } | |
| 261 | - } | |
| 262 | - } | |
| 263 | - | |
| 264 | - /** | |
| 265 | - * Removes rules by property name or search pattern. | |
| 266 | - * | |
| 267 | - * @param string $searchPattern | |
| 268 | - * pattern to remove. | |
| 269 | - * If the pattern ends in a dash, | |
| 270 | - * all rules starting with the pattern are removed as well as one matching the pattern with the dash | |
| 271 | - * excluded. | |
| 272 | - */ | |
| 273 | - public function removeMatchingRules(string $searchPattern): void | |
| 274 | - { | |
| 275 | - foreach ($this->rules as $propertyName => $rules) { | |
| 276 | - // Either the search rule matches the found rule exactly | |
| 277 | - // or the search rule ends in “-” and the found rule starts with the search rule or equals it | |
| 278 | - // (without the trailing dash). | |
| 279 | - if ( | |
| 280 | - $propertyName === $searchPattern | |
| 281 | - || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') | |
| 282 | - && (\strpos($propertyName, $searchPattern) === 0 | |
| 283 | - || $propertyName === \substr($searchPattern, 0, -1))) | |
| 284 | - ) { | |
| 285 | - unset($this->rules[$propertyName]); | |
| 286 | - } | |
| 287 | - } | |
| 288 | - } | |
| 289 | - | |
| 290 | - public function removeAllRules(): void | |
| 291 | - { | |
| 292 | - $this->rules = []; | |
| 293 | - } | |
| 294 | - | |
| 295 | - /** | |
| 296 | - * @internal | |
| 297 | - */ | |
| 298 | - public function render(OutputFormat $outputFormat): string | |
| 299 | - { | |
| 300 | - return $this->renderRules($outputFormat); | |
| 301 | - } | |
| 302 | - | |
| 303 | - protected function renderRules(OutputFormat $outputFormat): string | |
| 304 | - { | |
| 305 | - $result = ''; | |
| 306 | - $isFirst = true; | |
| 307 | - $nextLevelFormat = $outputFormat->nextLevel(); | |
| 308 | - foreach ($this->getRules() as $rule) { | |
| 309 | - $nextLevelFormatter = $nextLevelFormat->getFormatter(); | |
| 310 | - $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string { | |
| 311 | - return $rule->render($nextLevelFormat); | |
| 312 | - }); | |
| 313 | - if ($renderedRule === null) { | |
| 314 | - continue; | |
| 315 | - } | |
| 316 | - if ($isFirst) { | |
| 317 | - $isFirst = false; | |
| 318 | - $result .= $nextLevelFormatter->spaceBeforeRules(); | |
| 319 | - } else { | |
| 320 | - $result .= $nextLevelFormatter->spaceBetweenRules(); | |
| 321 | - } | |
| 322 | - $result .= $renderedRule; | |
| 323 | - } | |
| 324 | - | |
| 325 | - $formatter = $outputFormat->getFormatter(); | |
| 326 | - if (!$isFirst) { | |
| 327 | - // Had some output | |
| 328 | - $result .= $formatter->spaceAfterRules(); | |
| 329 | - } | |
| 330 | - | |
| 331 | - return $formatter->removeLastSemicolon($result); | |
| 332 | - } | |
| 333 | - | |
| 334 | - /** | |
| 335 | - * @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise | |
| 336 | - * | |
| 337 | - * @throws \UnexpectedValueException if either argument does not have a valid position, which should never happen | |
| 338 | - */ | |
| 339 | - private static function comparePositionable(Positionable $first, Positionable $second): int | |
| 340 | - { | |
| 341 | - $firstsLineNumber = $first->getLineNumber(); | |
| 342 | - $secondsLineNumber = $second->getLineNumber(); | |
| 343 | - if (!\is_int($firstsLineNumber) || !\is_int($secondsLineNumber)) { | |
| 344 | - throw new \UnexpectedValueException( | |
| 345 | - 'A Rule without a line number was passed to comparePositionable', | |
| 346 | - 1750637683 | |
| 347 | - ); | |
| 348 | - } | |
| 349 | - | |
| 350 | - if ($firstsLineNumber === $secondsLineNumber) { | |
| 351 | - $firstsColumnNumber = $first->getColumnNumber(); | |
| 352 | - $secondsColumnNumber = $second->getColumnNumber(); | |
| 353 | - if (!\is_int($firstsColumnNumber) || !\is_int($secondsColumnNumber)) { | |
| 354 | - throw new \UnexpectedValueException( | |
| 355 | - 'A Rule without a column number was passed to comparePositionable', | |
| 356 | - 1750637761 | |
| 357 | - ); | |
| 358 | - } | |
| 359 | - return $firstsColumnNumber - $secondsColumnNumber; | |
| 360 | - } | |
| 361 | - | |
| 362 | - return $firstsLineNumber - $secondsLineNumber; | |
| 363 | - } | |
| 364 | - | |
| 365 | - private function hasRule(Rule $rule): bool | |
| 366 | - { | |
| 367 | - foreach ($this->rules as $rulesForAProperty) { | |
| 368 | - if (\in_array($rule, $rulesForAProperty, true)) { | |
| 369 | - return true; | |
| 370 | - } | |
| 371 | - } | |
| 372 | - | |
| 373 | - return false; | |
| 374 | - } | |
| 375 | -} | |