PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.1.4
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.1.4
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 / CSSList / CSSList.php

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

480 lines 15.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Sabberworm\CSS\CSSList;
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\SourceException;
10 use Sabberworm\CSS\Parsing\UnexpectedEOFException;
11 use Sabberworm\CSS\Parsing\UnexpectedTokenException;
12 use Sabberworm\CSS\Property\AtRule;
13 use Sabberworm\CSS\Property\Charset;
14 use Sabberworm\CSS\Property\CSSNamespace;
15 use Sabberworm\CSS\Property\Import;
16 use Sabberworm\CSS\Property\Selector;
17 use Sabberworm\CSS\Renderable;
18 use Sabberworm\CSS\RuleSet\AtRuleSet;
19 use Sabberworm\CSS\RuleSet\DeclarationBlock;
20 use Sabberworm\CSS\RuleSet\RuleSet;
21 use Sabberworm\CSS\Settings;
22 use Sabberworm\CSS\Value\CSSString;
23 use Sabberworm\CSS\Value\URL;
24 use Sabberworm\CSS\Value\Value;
25
26 /**
27 * A `CSSList` is the most generic container available. Its contents include `RuleSet` as well as other `CSSList`
28 * objects.
29 *
30 * Also, it may contain `Import` and `Charset` objects stemming from at-rules.
31 */
32 abstract class CSSList implements Renderable, Commentable
33 {
34 /**
35 * @var array<array-key, Comment>
36 */
37 protected $aComments;
38
39 /**
40 * @var array<int, RuleSet|CSSList|Import|Charset>
41 */
42 protected $aContents;
43
44 /**
45 * @var int
46 */
47 protected $iLineNo;
48
49 /**
50 * @param int $iLineNo
51 */
52 public function __construct($iLineNo = 0)
53 {
54 $this->aComments = [];
55 $this->aContents = [];
56 $this->iLineNo = $iLineNo;
57 }
58
59 /**
60 * @return void
61 *
62 * @throws UnexpectedTokenException
63 * @throws SourceException
64 */
65 public static function parseList(ParserState $oParserState, CSSList $oList)
66 {
67 $bIsRoot = $oList instanceof Document;
68 if (is_string($oParserState)) {
69 $oParserState = new ParserState($oParserState, Settings::create());
70 }
71 $bLenientParsing = $oParserState->getSettings()->bLenientParsing;
72 while (!$oParserState->isEnd()) {
73 $comments = $oParserState->consumeWhiteSpace();
74 $oListItem = null;
75 if ($bLenientParsing) {
76 try {
77 $oListItem = self::parseListItem($oParserState, $oList);
78 } catch (UnexpectedTokenException $e) {
79 $oListItem = false;
80 }
81 } else {
82 $oListItem = self::parseListItem($oParserState, $oList);
83 }
84 if ($oListItem === null) {
85 // List parsing finished
86 return;
87 }
88 if ($oListItem) {
89 $oListItem->setComments($comments);
90 $oList->append($oListItem);
91 }
92 $oParserState->consumeWhiteSpace();
93 }
94 if (!$bIsRoot && !$bLenientParsing) {
95 throw new SourceException("Unexpected end of document", $oParserState->currentLine());
96 }
97 }
98
99 /**
100 * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
101 *
102 * @throws SourceException
103 * @throws UnexpectedEOFException
104 * @throws UnexpectedTokenException
105 */
106 private static function parseListItem(ParserState $oParserState, CSSList $oList)
107 {
108 $bIsRoot = $oList instanceof Document;
109 if ($oParserState->comes('@')) {
110 $oAtRule = self::parseAtRule($oParserState);
111 if ($oAtRule instanceof Charset) {
112 if (!$bIsRoot) {
113 throw new UnexpectedTokenException(
114 '@charset may only occur in root document',
115 '',
116 'custom',
117 $oParserState->currentLine()
118 );
119 }
120 if (count($oList->getContents()) > 0) {
121 throw new UnexpectedTokenException(
122 '@charset must be the first parseable token in a document',
123 '',
124 'custom',
125 $oParserState->currentLine()
126 );
127 }
128 $oParserState->setCharset($oAtRule->getCharset()->getString());
129 }
130 return $oAtRule;
131 } elseif ($oParserState->comes('}')) {
132 if (!$oParserState->getSettings()->bLenientParsing) {
133 throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine());
134 } else {
135 if ($bIsRoot) {
136 if ($oParserState->getSettings()->bLenientParsing) {
137 return DeclarationBlock::parse($oParserState);
138 } else {
139 throw new SourceException("Unopened {", $oParserState->currentLine());
140 }
141 } else {
142 return null;
143 }
144 }
145 } else {
146 return DeclarationBlock::parse($oParserState, $oList);
147 }
148 }
149
150 /**
151 * @param ParserState $oParserState
152 *
153 * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
154 *
155 * @throws SourceException
156 * @throws UnexpectedTokenException
157 * @throws UnexpectedEOFException
158 */
159 private static function parseAtRule(ParserState $oParserState)
160 {
161 $oParserState->consume('@');
162 $sIdentifier = $oParserState->parseIdentifier();
163 $iIdentifierLineNum = $oParserState->currentLine();
164 $oParserState->consumeWhiteSpace();
165 if ($sIdentifier === 'import') {
166 $oLocation = URL::parse($oParserState);
167 $oParserState->consumeWhiteSpace();
168 $sMediaQuery = null;
169 if (!$oParserState->comes(';')) {
170 $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF]));
171 }
172 $oParserState->consumeUntil([';', ParserState::EOF], true, true);
173 return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
174 } elseif ($sIdentifier === 'charset') {
175 $sCharset = CSSString::parse($oParserState);
176 $oParserState->consumeWhiteSpace();
177 $oParserState->consumeUntil([';', ParserState::EOF], true, true);
178 return new Charset($sCharset, $iIdentifierLineNum);
179 } elseif (self::identifierIs($sIdentifier, 'keyframes')) {
180 $oResult = new KeyFrame($iIdentifierLineNum);
181 $oResult->setVendorKeyFrame($sIdentifier);
182 $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
183 CSSList::parseList($oParserState, $oResult);
184 if ($oParserState->comes('}')) {
185 $oParserState->consume('}');
186 }
187 return $oResult;
188 } elseif ($sIdentifier === 'namespace') {
189 $sPrefix = null;
190 $mUrl = Value::parsePrimitiveValue($oParserState);
191 if (!$oParserState->comes(';')) {
192 $sPrefix = $mUrl;
193 $mUrl = Value::parsePrimitiveValue($oParserState);
194 }
195 $oParserState->consumeUntil([';', ParserState::EOF], true, true);
196 if ($sPrefix !== null && !is_string($sPrefix)) {
197 throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
198 }
199 if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
200 throw new UnexpectedTokenException(
201 'Wrong namespace url of invalid type',
202 $mUrl,
203 'custom',
204 $iIdentifierLineNum
205 );
206 }
207 return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
208 } else {
209 // Unknown other at rule (font-face or such)
210 $sArgs = trim($oParserState->consumeUntil('{', false, true));
211 if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
212 if ($oParserState->getSettings()->bLenientParsing) {
213 return null;
214 } else {
215 throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
216 }
217 }
218 $bUseRuleSet = true;
219 foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
220 if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
221 $bUseRuleSet = false;
222 break;
223 }
224 }
225 if ($bUseRuleSet) {
226 $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
227 RuleSet::parseRuleSet($oParserState, $oAtRule);
228 } else {
229 $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
230 CSSList::parseList($oParserState, $oAtRule);
231 if ($oParserState->comes('}')) {
232 $oParserState->consume('}');
233 }
234 }
235 return $oAtRule;
236 }
237 }
238
239 /**
240 * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed.
241 * We need to check for these versions too.
242 *
243 * @param string $sIdentifier
244 * @param string $sMatch
245 *
246 * @return bool
247 */
248 private static function identifierIs($sIdentifier, $sMatch)
249 {
250 return (strcasecmp($sIdentifier, $sMatch) === 0)
251 ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
252 }
253
254 /**
255 * @return int
256 */
257 public function getLineNo()
258 {
259 return $this->iLineNo;
260 }
261
262 /**
263 * Prepends an item to the list of contents.
264 *
265 * @param RuleSet|CSSList|Import|Charset $oItem
266 *
267 * @return void
268 */
269 public function prepend($oItem)
270 {
271 array_unshift($this->aContents, $oItem);
272 }
273
274 /**
275 * Appends an item to tje list of contents.
276 *
277 * @param RuleSet|CSSList|Import|Charset $oItem
278 *
279 * @return void
280 */
281 public function append($oItem)
282 {
283 $this->aContents[] = $oItem;
284 }
285
286 /**
287 * Splices the list of contents.
288 *
289 * @param int $iOffset
290 * @param int $iLength
291 * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
292 *
293 * @return void
294 */
295 public function splice($iOffset, $iLength = null, $mReplacement = null)
296 {
297 array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
298 }
299
300 /**
301 * Removes an item from the CSS list.
302 *
303 * @param RuleSet|Import|Charset|CSSList $oItemToRemove
304 * May be a RuleSet (most likely a DeclarationBlock), a Import,
305 * a Charset or another CSSList (most likely a MediaQuery)
306 *
307 * @return bool whether the item was removed
308 */
309 public function remove($oItemToRemove)
310 {
311 $iKey = array_search($oItemToRemove, $this->aContents, true);
312 if ($iKey !== false) {
313 unset($this->aContents[$iKey]);
314 return true;
315 }
316 return false;
317 }
318
319 /**
320 * Replaces an item from the CSS list.
321 *
322 * @param RuleSet|Import|Charset|CSSList $oOldItem
323 * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
324 * or another `CSSList` (most likely a `MediaQuery`)
325 *
326 * @return bool
327 */
328 public function replace($oOldItem, $mNewItem)
329 {
330 $iKey = array_search($oOldItem, $this->aContents, true);
331 if ($iKey !== false) {
332 if (is_array($mNewItem)) {
333 array_splice($this->aContents, $iKey, 1, $mNewItem);
334 } else {
335 array_splice($this->aContents, $iKey, 1, [$mNewItem]);
336 }
337 return true;
338 }
339 return false;
340 }
341
342 /**
343 * @param array<int, RuleSet|Import|Charset|CSSList> $aContents
344 */
345 public function setContents(array $aContents)
346 {
347 $this->aContents = [];
348 foreach ($aContents as $content) {
349 $this->append($content);
350 }
351 }
352
353 /**
354 * Removes a declaration block from the CSS list if it matches all given selectors.
355 *
356 * @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match
357 * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
358 *
359 * @return void
360 */
361 public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false)
362 {
363 if ($mSelector instanceof DeclarationBlock) {
364 $mSelector = $mSelector->getSelectors();
365 }
366 if (!is_array($mSelector)) {
367 $mSelector = explode(',', $mSelector);
368 }
369 foreach ($mSelector as $iKey => &$mSel) {
370 if (!($mSel instanceof Selector)) {
371 if (!Selector::isValid($mSel)) {
372 throw new UnexpectedTokenException(
373 "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
374 $mSel,
375 "custom"
376 );
377 }
378 $mSel = new Selector($mSel);
379 }
380 }
381 foreach ($this->aContents as $iKey => $mItem) {
382 if (!($mItem instanceof DeclarationBlock)) {
383 continue;
384 }
385 if ($mItem->getSelectors() == $mSelector) {
386 unset($this->aContents[$iKey]);
387 if (!$bRemoveAll) {
388 return;
389 }
390 }
391 }
392 }
393
394 /**
395 * @return string
396 */
397 public function __toString()
398 {
399 return $this->render(new OutputFormat());
400 }
401
402 /**
403 * @return string
404 */
405 public function render(OutputFormat $oOutputFormat)
406 {
407 $sResult = '';
408 $bIsFirst = true;
409 $oNextLevel = $oOutputFormat;
410 if (!$this->isRootList()) {
411 $oNextLevel = $oOutputFormat->nextLevel();
412 }
413 foreach ($this->aContents as $oContent) {
414 $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
415 return $oContent->render($oNextLevel);
416 });
417 if ($sRendered === null) {
418 continue;
419 }
420 if ($bIsFirst) {
421 $bIsFirst = false;
422 $sResult .= $oNextLevel->spaceBeforeBlocks();
423 } else {
424 $sResult .= $oNextLevel->spaceBetweenBlocks();
425 }
426 $sResult .= $sRendered;
427 }
428
429 if (!$bIsFirst) {
430 // Had some output
431 $sResult .= $oOutputFormat->spaceAfterBlocks();
432 }
433
434 return $sResult;
435 }
436
437 /**
438 * Return true if the list can not be further outdented. Only important when rendering.
439 *
440 * @return bool
441 */
442 abstract public function isRootList();
443
444 /**
445 * @return array<int, RuleSet|Import|Charset|CSSList>
446 */
447 public function getContents()
448 {
449 return $this->aContents;
450 }
451
452 /**
453 * @param array<array-key, Comment> $aComments
454 *
455 * @return void
456 */
457 public function addComments(array $aComments)
458 {
459 $this->aComments = array_merge($this->aComments, $aComments);
460 }
461
462 /**
463 * @return array<array-key, Comment>
464 */
465 public function getComments()
466 {
467 return $this->aComments;
468 }
469
470 /**
471 * @param array<array-key, Comment> $aComments
472 *
473 * @return void
474 */
475 public function setComments(array $aComments)
476 {
477 $this->aComments = $aComments;
478 }
479 }
480