PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
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
← All changes | back/Libraries/Sabberworm/CSS/src/RuleSet/RuleSet.php +239 -190 10.0.1 → 11.1.0 View file →
@@ -1,73 +1,82 @@
1 1 <?php
2 2
3 -namespace Sabberworm\CSS\RuleSet;
3 +declare(strict_types=1);
4 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;
5 +namespace AcyMailing\Libraries\Sabberworm\CSS\RuleSet;
13 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 +
14 18 /**
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.
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.
17 28 */
18 -abstract class RuleSet implements Renderable, Commentable
29 +class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer
19 30 {
20 - /**
21 - * @var array<string, Rule>
22 - */
23 - private $aRules;
31 + use CommentContainer;
32 + use Position;
24 33
25 34 /**
26 - * @var int
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>>
27 39 */
28 - protected $iLineNo;
40 + private $rules = [];
29 41
30 42 /**
31 - * @var array<array-key, Comment>
43 + * @param int<1, max>|null $lineNumber
32 44 */
33 - protected $aComments;
34 -
35 - /**
36 - * @param int $iLineNo
37 - */
38 - public function __construct($iLineNo = 0)
45 + public function __construct(?int $lineNumber = null)
39 46 {
40 - $this->aRules = [];
41 - $this->iLineNo = $iLineNo;
42 - $this->aComments = [];
47 + $this->setPosition($lineNumber);
43 48 }
44 49
45 50 /**
46 - * @return void
47 - *
48 51 * @throws UnexpectedTokenException
49 52 * @throws UnexpectedEOFException
53 + *
54 + * @internal since V8.8.0
50 55 */
51 - public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
56 + public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void
52 57 {
53 - while ($oParserState->comes(';')) {
54 - $oParserState->consume(';');
58 + while ($parserState->comes(';')) {
59 + $parserState->consume(';');
55 60 }
56 - while (!$oParserState->comes('}')) {
57 - $oRule = null;
58 - if ($oParserState->getSettings()->bLenientParsing) {
61 + while (true) {
62 + $commentsBeforeRule = $parserState->consumeWhiteSpace();
63 + if ($parserState->comes('}')) {
64 + break;
65 + }
66 + $rule = null;
67 + if ($parserState->getSettings()->usesLenientParsing()) {
59 68 try {
60 - $oRule = Rule::parse($oParserState);
69 + $rule = Rule::parse($parserState, $commentsBeforeRule);
61 70 } catch (UnexpectedTokenException $e) {
62 71 try {
63 - $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
72 + $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true);
64 73 // 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);
74 + if ($parserState->streql(\substr($consumedText, -1), '}')) {
75 + $parserState->backtrack(1);
67 76 } else {
68 - while ($oParserState->comes(';')) {
69 - $oParserState->consume(';');
77 + while ($parserState->comes(';')) {
78 + $parserState->consume(';');
70 79 }
71 80 }
72 81 } catch (UnexpectedTokenException $e) {
73 82 // We’ve reached the end of the document. Just close the RuleSet.
@@ -74,115 +83,139 @@
74 83 return;
75 84 }
76 85 }
77 86 } else {
78 - $oRule = Rule::parse($oParserState);
87 + $rule = Rule::parse($parserState, $commentsBeforeRule);
79 88 }
80 - if ($oRule) {
81 - $oRuleSet->addRule($oRule);
89 + if ($rule instanceof Rule) {
90 + $ruleSet->addRule($rule);
82 91 }
83 92 }
84 - $oParserState->consume('}');
93 + $parserState->consume('}');
85 94 }
86 95
87 96 /**
88 - * @return int
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
89 100 */
90 - public function getLineNo()
101 + public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
91 102 {
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] = [];
103 + $propertyName = $ruleToAdd->getRule();
104 + if (!isset($this->rules[$propertyName])) {
105 + $this->rules[$propertyName] = [];
105 106 }
106 107
107 - $iPosition = count($this->aRules[$sRule]);
108 + $position = \count($this->rules[$propertyName]);
108 109
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);
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 + }
114 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 + }
115 145 }
116 - if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
146 +
147 + if ($ruleToAdd->getLineNumber() === null) {
117 148 //this node is added manually, give it the next best line
149 + $columnNumber = $ruleToAdd->getColumnNumber() ?? 0;
118 150 $rules = $this->getRules();
119 - $pos = count($rules);
120 - if ($pos > 0) {
121 - $last = $rules[$pos - 1];
122 - $oRule->setPosition($last->getLineNo() + 1, 0);
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);
123 164 }
165 + } elseif ($ruleToAdd->getColumnNumber() === null) {
166 + $ruleToAdd->setPosition($ruleToAdd->getLineNumber(), 0);
124 167 }
125 168
126 - array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
169 + \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]);
127 170 }
128 171
129 172 /**
130 173 * Returns all rules matching the given rule name
131 174 *
132 - * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
175 + * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array().
133 176 *
134 - * @example $oRuleSet->getRules('font-')
177 + * @example $ruleSet->getRules('font-')
135 178 * //returns an array of all rules either beginning with font- or matching font.
136 179 *
137 - * @param Rule|string|null $mRule
180 + * @param string|null $searchPattern
138 181 * Pattern to search for. If null, returns all rules.
139 182 * If the pattern ends with a dash, all rules starting with the pattern are returned
140 183 * as well as one matching the pattern with the dash excluded.
141 - * Passing a Rule behaves like calling `getRules($mRule->getRule())`.
142 184 *
143 - * @return array<int, Rule>
185 + * @return array<int<0, max>, Rule>
144 186 */
145 - public function getRules($mRule = null)
187 + public function getRules(?string $searchPattern = null): array
146 188 {
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) {
189 + $result = [];
190 + foreach ($this->rules as $propertyName => $rules) {
153 191 // Either no search rule is given or the search rule matches the found rule exactly
154 192 // or the search rule ends in “-” and the found rule starts with the search rule.
155 193 if (
156 - !$mRule || $sName === $mRule
194 + $searchPattern === null || $propertyName === $searchPattern
157 195 || (
158 - strrpos($mRule, '-') === strlen($mRule) - strlen('-')
159 - && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))
196 + \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
197 + && (\strpos($propertyName, $searchPattern) === 0
198 + || $propertyName === \substr($searchPattern, 0, -1))
160 199 )
161 200 ) {
162 - $aResult = array_merge($aResult, $aRules);
201 + $result = \array_merge($result, $rules);
163 202 }
164 203 }
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;
204 + \usort($result, [self::class, 'comparePositionable']);
205 +
206 + return $result;
172 207 }
173 208
174 209 /**
175 210 * Overrides all the rules of this set.
176 211 *
177 - * @param array<array-key, Rule> $aRules The rules to override with.
178 - *
179 - * @return void
212 + * @param array<Rule> $rules The rules to override with.
180 213 */
181 - public function setRules(array $aRules)
214 + public function setRules(array $rules): void
182 215 {
183 - $this->aRules = [];
184 - foreach ($aRules as $rule) {
216 + $this->rules = [];
217 + foreach ($rules as $rule) {
185 218 $this->addRule($rule);
186 219 }
187 220 }
188 221
@@ -193,134 +226,150 @@
193 226 * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block
194 227 * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array
195 228 * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both.
196 229 *
197 - * @param Rule|string|null $mRule $mRule
230 + * @param string|null $searchPattern
198 231 * Pattern to search for. If null, returns all rules. If the pattern ends with a dash,
199 232 * 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())`.
233 + * excluded.
201 234 *
202 235 * @return array<string, Rule>
203 236 */
204 - public function getRulesAssoc($mRule = null)
237 + public function getRulesAssoc(?string $searchPattern = null): array
205 238 {
206 - /** @var array<string, Rule> $aResult */
207 - $aResult = [];
208 - foreach ($this->getRules($mRule) as $oRule) {
209 - $aResult[$oRule->getRule()] = $oRule;
239 + /** @var array<string, Rule> $result */
240 + $result = [];
241 + foreach ($this->getRules($searchPattern) as $rule) {
242 + $result[$rule->getRule()] = $rule;
210 243 }
211 - return $aResult;
244 +
245 + return $result;
212 246 }
213 247
214 248 /**
215 - * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts.
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.
216 266 *
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,
267 + * @param string $searchPattern
268 + * pattern to remove.
269 + * If the pattern ends in a dash,
225 270 * 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
271 + * excluded.
229 272 */
230 - public function removeRule($mRule)
273 + public function removeMatchingRules(string $searchPattern): void
231 274 {
232 - if ($mRule instanceof Rule) {
233 - $sRule = $mRule->getRule();
234 - if (!isset($this->aRules[$sRule])) {
235 - return;
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]);
236 286 }
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 287 }
256 288 }
257 289
290 + public function removeAllRules(): void
291 + {
292 + $this->rules = [];
293 + }
294 +
258 295 /**
259 - * @return string
296 + * @internal
260 297 */
261 - public function __toString()
298 + public function render(OutputFormat $outputFormat): string
262 299 {
263 - return $this->render(new OutputFormat());
300 + return $this->renderRules($outputFormat);
264 301 }
265 302
266 - /**
267 - * @return string
268 - */
269 - public function render(OutputFormat $oOutputFormat)
303 + protected function renderRules(OutputFormat $outputFormat): string
270 304 {
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;
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;
288 315 }
316 + if ($isFirst) {
317 + $isFirst = false;
318 + $result .= $nextLevelFormatter->spaceBeforeRules();
319 + } else {
320 + $result .= $nextLevelFormatter->spaceBetweenRules();
321 + }
322 + $result .= $renderedRule;
289 323 }
290 324
291 - if (!$bIsFirst) {
325 + $formatter = $outputFormat->getFormatter();
326 + if (!$isFirst) {
292 327 // Had some output
293 - $sResult .= $oOutputFormat->spaceAfterRules();
328 + $result .= $formatter->spaceAfterRules();
294 329 }
295 330
296 - return $oOutputFormat->removeLastSemicolon($sResult);
331 + return $formatter->removeLastSemicolon($result);
297 332 }
298 333
299 334 /**
300 - * @param array<string, Comment> $aComments
335 + * @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise
301 336 *
302 - * @return void
337 + * @throws \UnexpectedValueException if either argument does not have a valid position, which should never happen
303 338 */
304 - public function addComments(array $aComments)
339 + private static function comparePositionable(Positionable $first, Positionable $second): int
305 340 {
306 - $this->aComments = array_merge($this->aComments, $aComments);
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;
307 363 }
308 364
309 - /**
310 - * @return array<string, Comment>
311 - */
312 - public function getComments()
365 + private function hasRule(Rule $rule): bool
313 366 {
314 - return $this->aComments;
315 - }
367 + foreach ($this->rules as $rulesForAProperty) {
368 + if (\in_array($rule, $rulesForAProperty, true)) {
369 + return true;
370 + }
371 + }
316 372
317 - /**
318 - * @param array<string, Comment> $aComments
319 - *
320 - * @return void
321 - */
322 - public function setComments(array $aComments)
323 - {
324 - $this->aComments = $aComments;
373 + return false;
325 374 }
326 375 }