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