PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.2
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.2
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 / RuleSet.php

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

376 lines 14.2 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\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 }
376