PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.3
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.3
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Libraries / Sabberworm / CSS / src / RuleSet / DeclarationBlock.php

DeclarationBlock.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.0.3, at back/Libraries/Sabberworm/CSS/src/RuleSet/DeclarationBlock.php

302 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
302