← All changes
|
back/Libraries/Sabberworm/CSS/src/RuleSet/DeclarationBlock.php
+301
-0
10.10.1
→
11.1.0
View file →
| @@ -1,0 +1,301 @@ | ||
| 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\CSSList; | |
| 10 | +use AcyMailing\Libraries\Sabberworm\CSS\CSSList\CSSListItem; | |
| 11 | +use AcyMailing\Libraries\Sabberworm\CSS\CSSList\KeyFrame; | |
| 12 | +use AcyMailing\Libraries\Sabberworm\CSS\OutputFormat; | |
| 13 | +use AcyMailing\Libraries\Sabberworm\CSS\Parsing\OutputException; | |
| 14 | +use AcyMailing\Libraries\Sabberworm\CSS\Parsing\ParserState; | |
| 15 | +use AcyMailing\Libraries\Sabberworm\CSS\Parsing\UnexpectedEOFException; | |
| 16 | +use AcyMailing\Libraries\Sabberworm\CSS\Parsing\UnexpectedTokenException; | |
| 17 | +use AcyMailing\Libraries\Sabberworm\CSS\Position\Position; | |
| 18 | +use AcyMailing\Libraries\Sabberworm\CSS\Position\Positionable; | |
| 19 | +use AcyMailing\Libraries\Sabberworm\CSS\Property\KeyframeSelector; | |
| 20 | +use AcyMailing\Libraries\Sabberworm\CSS\Property\Selector; | |
| 21 | +use AcyMailing\Libraries\Sabberworm\CSS\Rule\Rule; | |
| 22 | + | |
| 23 | +/** | |
| 24 | + * This class represents a `RuleSet` constrained by a `Selector`. | |
| 25 | + * | |
| 26 | + * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the | |
| 27 | + * matching elements. | |
| 28 | + * | |
| 29 | + * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`). | |
| 30 | + * | |
| 31 | + * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. | |
| 32 | + */ | |
| 33 | +class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleContainer | |
| 34 | +{ | |
| 35 | + use CommentContainer; | |
| 36 | + use Position; | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * @var array<Selector|string> | |
| 40 | + */ | |
| 41 | + private $selectors = []; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * @var RuleSet | |
| 45 | + */ | |
| 46 | + private $ruleSet; | |
| 47 | + | |
| 48 | + /** | |
| 49 | + * @param int<1, max>|null $lineNumber | |
| 50 | + */ | |
| 51 | + public function __construct(?int $lineNumber = null) | |
| 52 | + { | |
| 53 | + $this->ruleSet = new RuleSet($lineNumber); | |
| 54 | + $this->setPosition($lineNumber); | |
| 55 | + } | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * @throws UnexpectedTokenException | |
| 59 | + * @throws UnexpectedEOFException | |
| 60 | + * | |
| 61 | + * @internal since V8.8.0 | |
| 62 | + */ | |
| 63 | + public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock | |
| 64 | + { | |
| 65 | + $comments = []; | |
| 66 | + $result = new DeclarationBlock($parserState->currentLine()); | |
| 67 | + try { | |
| 68 | + $selectors = []; | |
| 69 | + $selectorParts = []; | |
| 70 | + $stringWrapperCharacter = null; | |
| 71 | + $functionNestingLevel = 0; | |
| 72 | + $consumedNextCharacter = false; | |
| 73 | + static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; | |
| 74 | + do { | |
| 75 | + if (!$consumedNextCharacter) { | |
| 76 | + $selectorParts[] = $parserState->consume(1); | |
| 77 | + } | |
| 78 | + $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); | |
| 79 | + $nextCharacter = $parserState->peek(); | |
| 80 | + $consumedNextCharacter = false; | |
| 81 | + switch ($nextCharacter) { | |
| 82 | + case '\'': | |
| 83 | + // The fallthrough is intentional. | |
| 84 | + case '"': | |
| 85 | + if (!\is_string($stringWrapperCharacter)) { | |
| 86 | + $stringWrapperCharacter = $nextCharacter; | |
| 87 | + } elseif ($stringWrapperCharacter === $nextCharacter) { | |
| 88 | + if (\substr(\end($selectorParts), -1) !== '\\') { | |
| 89 | + $stringWrapperCharacter = null; | |
| 90 | + } | |
| 91 | + } | |
| 92 | + break; | |
| 93 | + case '(': | |
| 94 | + if (!\is_string($stringWrapperCharacter)) { | |
| 95 | + ++$functionNestingLevel; | |
| 96 | + } | |
| 97 | + break; | |
| 98 | + case ')': | |
| 99 | + if (!\is_string($stringWrapperCharacter)) { | |
| 100 | + if ($functionNestingLevel <= 0) { | |
| 101 | + throw new UnexpectedTokenException('anything but', ')'); | |
| 102 | + } | |
| 103 | + --$functionNestingLevel; | |
| 104 | + } | |
| 105 | + break; | |
| 106 | + case ',': | |
| 107 | + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { | |
| 108 | + $selectors[] = \implode('', $selectorParts); | |
| 109 | + $selectorParts = []; | |
| 110 | + $parserState->consume(1); | |
| 111 | + $consumedNextCharacter = true; | |
| 112 | + } | |
| 113 | + break; | |
| 114 | + } | |
| 115 | + } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter)); | |
| 116 | + if ($functionNestingLevel !== 0) { | |
| 117 | + throw new UnexpectedTokenException(')', $nextCharacter); | |
| 118 | + } | |
| 119 | + $selectors[] = \implode('', $selectorParts); // add final or only selector | |
| 120 | + $result->setSelectors($selectors, $list); | |
| 121 | + if ($parserState->comes('{')) { | |
| 122 | + $parserState->consume(1); | |
| 123 | + } | |
| 124 | + } catch (UnexpectedTokenException $e) { | |
| 125 | + if ($parserState->getSettings()->usesLenientParsing()) { | |
| 126 | + if (!$parserState->comes('}')) { | |
| 127 | + $parserState->consumeUntil('}', false, true); | |
| 128 | + } | |
| 129 | + return null; | |
| 130 | + } else { | |
| 131 | + throw $e; | |
| 132 | + } | |
| 133 | + } | |
| 134 | + $result->setComments($comments); | |
| 135 | + | |
| 136 | + RuleSet::parseRuleSet($parserState, $result->getRuleSet()); | |
| 137 | + | |
| 138 | + return $result; | |
| 139 | + } | |
| 140 | + | |
| 141 | + /** | |
| 142 | + * @param array<Selector|string>|string $selectors | |
| 143 | + * | |
| 144 | + * @throws UnexpectedTokenException | |
| 145 | + */ | |
| 146 | + public function setSelectors($selectors, ?CSSList $list = null): void | |
| 147 | + { | |
| 148 | + if (\is_array($selectors)) { | |
| 149 | + $this->selectors = $selectors; | |
| 150 | + } else { | |
| 151 | + $this->selectors = \explode(',', $selectors); | |
| 152 | + } | |
| 153 | + foreach ($this->selectors as $key => $selector) { | |
| 154 | + if (!($selector instanceof Selector)) { | |
| 155 | + if ($list === null || !($list instanceof KeyFrame)) { | |
| 156 | + if (!Selector::isValid($selector)) { | |
| 157 | + throw new UnexpectedTokenException( | |
| 158 | + "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", | |
| 159 | + $selector, | |
| 160 | + 'custom' | |
| 161 | + ); | |
| 162 | + } | |
| 163 | + $this->selectors[$key] = new Selector($selector); | |
| 164 | + } else { | |
| 165 | + if (!KeyframeSelector::isValid($selector)) { | |
| 166 | + throw new UnexpectedTokenException( | |
| 167 | + "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", | |
| 168 | + $selector, | |
| 169 | + 'custom' | |
| 170 | + ); | |
| 171 | + } | |
| 172 | + $this->selectors[$key] = new KeyframeSelector($selector); | |
| 173 | + } | |
| 174 | + } | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + /** | |
| 179 | + * Remove one of the selectors of the block. | |
| 180 | + * | |
| 181 | + * @param Selector|string $selectorToRemove | |
| 182 | + */ | |
| 183 | + public function removeSelector($selectorToRemove): bool | |
| 184 | + { | |
| 185 | + if ($selectorToRemove instanceof Selector) { | |
| 186 | + $selectorToRemove = $selectorToRemove->getSelector(); | |
| 187 | + } | |
| 188 | + foreach ($this->selectors as $key => $selector) { | |
| 189 | + if ($selector->getSelector() === $selectorToRemove) { | |
| 190 | + unset($this->selectors[$key]); | |
| 191 | + return true; | |
| 192 | + } | |
| 193 | + } | |
| 194 | + return false; | |
| 195 | + } | |
| 196 | + | |
| 197 | + /** | |
| 198 | + * @return array<Selector> | |
| 199 | + */ | |
| 200 | + public function getSelectors(): array | |
| 201 | + { | |
| 202 | + return $this->selectors; | |
| 203 | + } | |
| 204 | + | |
| 205 | + public function getRuleSet(): RuleSet | |
| 206 | + { | |
| 207 | + return $this->ruleSet; | |
| 208 | + } | |
| 209 | + | |
| 210 | + /** | |
| 211 | + * @see RuleSet::addRule() | |
| 212 | + */ | |
| 213 | + public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void | |
| 214 | + { | |
| 215 | + $this->ruleSet->addRule($ruleToAdd, $sibling); | |
| 216 | + } | |
| 217 | + | |
| 218 | + /** | |
| 219 | + * @see RuleSet::getRules() | |
| 220 | + * | |
| 221 | + * @return array<int<0, max>, Rule> | |
| 222 | + */ | |
| 223 | + public function getRules(?string $searchPattern = null): array | |
| 224 | + { | |
| 225 | + return $this->ruleSet->getRules($searchPattern); | |
| 226 | + } | |
| 227 | + | |
| 228 | + /** | |
| 229 | + * @see RuleSet::setRules() | |
| 230 | + * | |
| 231 | + * @param array<Rule> $rules | |
| 232 | + */ | |
| 233 | + public function setRules(array $rules): void | |
| 234 | + { | |
| 235 | + $this->ruleSet->setRules($rules); | |
| 236 | + } | |
| 237 | + | |
| 238 | + /** | |
| 239 | + * @see RuleSet::getRulesAssoc() | |
| 240 | + * | |
| 241 | + * @return array<string, Rule> | |
| 242 | + */ | |
| 243 | + public function getRulesAssoc(?string $searchPattern = null): array | |
| 244 | + { | |
| 245 | + return $this->ruleSet->getRulesAssoc($searchPattern); | |
| 246 | + } | |
| 247 | + | |
| 248 | + /** | |
| 249 | + * @see RuleSet::removeRule() | |
| 250 | + */ | |
| 251 | + public function removeRule(Rule $ruleToRemove): void | |
| 252 | + { | |
| 253 | + $this->ruleSet->removeRule($ruleToRemove); | |
| 254 | + } | |
| 255 | + | |
| 256 | + /** | |
| 257 | + * @see RuleSet::removeMatchingRules() | |
| 258 | + */ | |
| 259 | + public function removeMatchingRules(string $searchPattern): void | |
| 260 | + { | |
| 261 | + $this->ruleSet->removeMatchingRules($searchPattern); | |
| 262 | + } | |
| 263 | + | |
| 264 | + /** | |
| 265 | + * @see RuleSet::removeAllRules() | |
| 266 | + */ | |
| 267 | + public function removeAllRules(): void | |
| 268 | + { | |
| 269 | + $this->ruleSet->removeAllRules(); | |
| 270 | + } | |
| 271 | + | |
| 272 | + /** | |
| 273 | + * @return non-empty-string | |
| 274 | + * | |
| 275 | + * @throws OutputException | |
| 276 | + */ | |
| 277 | + public function render(OutputFormat $outputFormat): string | |
| 278 | + { | |
| 279 | + $formatter = $outputFormat->getFormatter(); | |
| 280 | + $result = $formatter->comments($this); | |
| 281 | + if (\count($this->selectors) === 0) { | |
| 282 | + // If all the selectors have been removed, this declaration block becomes invalid | |
| 283 | + throw new OutputException( | |
| 284 | + 'Attempt to print declaration block with missing selector', | |
| 285 | + $this->getLineNumber() | |
| 286 | + ); | |
| 287 | + } | |
| 288 | + $result .= $outputFormat->getContentBeforeDeclarationBlock(); | |
| 289 | + $result .= $formatter->implode( | |
| 290 | + $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(), | |
| 291 | + $this->selectors | |
| 292 | + ); | |
| 293 | + $result .= $outputFormat->getContentAfterDeclarationBlockSelectors(); | |
| 294 | + $result .= $formatter->spaceBeforeOpeningBrace() . '{'; | |
| 295 | + $result .= $this->ruleSet->render($outputFormat); | |
| 296 | + $result .= '}'; | |
| 297 | + $result .= $outputFormat->getContentAfterDeclarationBlock(); | |
| 298 | + | |
| 299 | + return $result; | |
| 300 | + } | |
| 301 | +} | |