MDL-65759 library: Update php-css-parser to 8.3.0
[moodle.git] / lib / php-css-parser / CSSList / CSSList.php
CommitLineData
fbe18cc0
FM
1<?php
2
3namespace Sabberworm\CSS\CSSList;
4
376eb156
MM
5use Sabberworm\CSS\Comment\Commentable;
6use Sabberworm\CSS\Parsing\ParserState;
7use Sabberworm\CSS\Parsing\SourceException;
8use Sabberworm\CSS\Parsing\UnexpectedTokenException;
9use Sabberworm\CSS\Property\AtRule;
10use Sabberworm\CSS\Property\Charset;
11use Sabberworm\CSS\Property\CSSNamespace;
12use Sabberworm\CSS\Property\Import;
13use Sabberworm\CSS\Property\Selector;
fbe18cc0 14use Sabberworm\CSS\Renderable;
376eb156 15use Sabberworm\CSS\RuleSet\AtRuleSet;
fbe18cc0
FM
16use Sabberworm\CSS\RuleSet\DeclarationBlock;
17use Sabberworm\CSS\RuleSet\RuleSet;
376eb156
MM
18use Sabberworm\CSS\Value\CSSString;
19use Sabberworm\CSS\Value\URL;
20use Sabberworm\CSS\Value\Value;
fbe18cc0
FM
21
22/**
23 * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
24 * Also, it may contain Import and Charset objects stemming from @-rules.
25 */
26abstract class CSSList implements Renderable, Commentable {
27
28 protected $aComments;
29 protected $aContents;
30 protected $iLineNo;
31
32 public function __construct($iLineNo = 0) {
33 $this->aComments = array();
34 $this->aContents = array();
35 $this->iLineNo = $iLineNo;
36 }
37
376eb156
MM
38 public static function parseList(ParserState $oParserState, CSSList $oList) {
39 $bIsRoot = $oList instanceof Document;
40 if(is_string($oParserState)) {
41 $oParserState = new ParserState($oParserState);
42 }
43 $bLenientParsing = $oParserState->getSettings()->bLenientParsing;
44 while(!$oParserState->isEnd()) {
45 $comments = $oParserState->consumeWhiteSpace();
46 $oListItem = null;
47 if($bLenientParsing) {
48 try {
49 $oListItem = self::parseListItem($oParserState, $oList);
50 } catch (UnexpectedTokenException $e) {
51 $oListItem = false;
52 }
53 } else {
54 $oListItem = self::parseListItem($oParserState, $oList);
55 }
56 if($oListItem === null) {
57 // List parsing finished
58 return;
59 }
60 if($oListItem) {
61 $oListItem->setComments($comments);
62 $oList->append($oListItem);
63 }
64 $oParserState->consumeWhiteSpace();
65 }
66 if(!$bIsRoot && !$bLenientParsing) {
67 throw new SourceException("Unexpected end of document", $oParserState->currentLine());
68 }
69 }
70
71 private static function parseListItem(ParserState $oParserState, CSSList $oList) {
72 $bIsRoot = $oList instanceof Document;
73 if ($oParserState->comes('@')) {
74 $oAtRule = self::parseAtRule($oParserState);
75 if($oAtRule instanceof Charset) {
76 if(!$bIsRoot) {
77 throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
78 }
79 if(count($oList->getContents()) > 0) {
80 throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
81 }
82 $oParserState->setCharset($oAtRule->getCharset()->getString());
83 }
84 return $oAtRule;
85 } else if ($oParserState->comes('}')) {
86 $oParserState->consume('}');
87 if ($bIsRoot) {
88 if ($oParserState->getSettings()->bLenientParsing) {
89 while ($oParserState->comes('}')) $oParserState->consume('}');
90 return DeclarationBlock::parse($oParserState);
91 } else {
92 throw new SourceException("Unopened {", $oParserState->currentLine());
93 }
94 } else {
95 return null;
96 }
97 } else {
98 return DeclarationBlock::parse($oParserState);
99 }
100 }
101
102 private static function parseAtRule(ParserState $oParserState) {
103 $oParserState->consume('@');
104 $sIdentifier = $oParserState->parseIdentifier();
105 $iIdentifierLineNum = $oParserState->currentLine();
106 $oParserState->consumeWhiteSpace();
107 if ($sIdentifier === 'import') {
108 $oLocation = URL::parse($oParserState);
109 $oParserState->consumeWhiteSpace();
110 $sMediaQuery = null;
111 if (!$oParserState->comes(';')) {
112 $sMediaQuery = $oParserState->consumeUntil(';');
113 }
114 $oParserState->consume(';');
115 return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
116 } else if ($sIdentifier === 'charset') {
117 $sCharset = CSSString::parse($oParserState);
118 $oParserState->consumeWhiteSpace();
119 $oParserState->consume(';');
120 return new Charset($sCharset, $iIdentifierLineNum);
121 } else if (self::identifierIs($sIdentifier, 'keyframes')) {
122 $oResult = new KeyFrame($iIdentifierLineNum);
123 $oResult->setVendorKeyFrame($sIdentifier);
124 $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
125 CSSList::parseList($oParserState, $oResult);
126 return $oResult;
127 } else if ($sIdentifier === 'namespace') {
128 $sPrefix = null;
129 $mUrl = Value::parsePrimitiveValue($oParserState);
130 if (!$oParserState->comes(';')) {
131 $sPrefix = $mUrl;
132 $mUrl = Value::parsePrimitiveValue($oParserState);
133 }
134 $oParserState->consume(';');
135 if ($sPrefix !== null && !is_string($sPrefix)) {
136 throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
137 }
138 if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
139 throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
140 }
141 return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
142 } else {
143 //Unknown other at rule (font-face or such)
144 $sArgs = trim($oParserState->consumeUntil('{', false, true));
145 if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
146 if($oParserState->getSettings()->bLenientParsing) {
147 return NULL;
148 } else {
149 throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
150 }
151 }
152 $bUseRuleSet = true;
153 foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
154 if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
155 $bUseRuleSet = false;
156 break;
157 }
158 }
159 if($bUseRuleSet) {
160 $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
161 RuleSet::parseRuleSet($oParserState, $oAtRule);
162 } else {
163 $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
164 CSSList::parseList($oParserState, $oAtRule);
165 }
166 return $oAtRule;
167 }
168 }
169
170 /**
171 * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
172 */
173 private static function identifierIs($sIdentifier, $sMatch) {
174 return (strcasecmp($sIdentifier, $sMatch) === 0)
175 ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
176 }
177
178
fbe18cc0
FM
179 /**
180 * @return int
181 */
182 public function getLineNo() {
183 return $this->iLineNo;
184 }
185
376eb156
MM
186 /**
187 * Prepend item to list of contents.
188 *
189 * @param object $oItem Item.
190 */
191 public function prepend($oItem) {
192 array_unshift($this->aContents, $oItem);
193 }
194
195 /**
196 * Append item to list of contents.
197 *
198 * @param object $oItem Item.
199 */
fbe18cc0
FM
200 public function append($oItem) {
201 $this->aContents[] = $oItem;
202 }
203
515ceadd 204 /**
376eb156 205 * Splice the list of contents.
515ceadd 206 *
376eb156
MM
207 * @param int $iOffset Offset.
208 * @param int $iLength Length. Optional.
209 * @param RuleSet[] $mReplacement Replacement. Optional.
515ceadd 210 */
376eb156
MM
211 public function splice($iOffset, $iLength = null, $mReplacement = null) {
212 array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
515ceadd
FM
213 }
214
fbe18cc0
FM
215 /**
216 * Removes an item from the CSS list.
217 * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
376eb156 218 * @return bool Whether the item was removed.
fbe18cc0
FM
219 */
220 public function remove($oItemToRemove) {
221 $iKey = array_search($oItemToRemove, $this->aContents, true);
222 if ($iKey !== false) {
223 unset($this->aContents[$iKey]);
224 return true;
225 }
226 return false;
227 }
228
376eb156
MM
229 /**
230 * Replaces an item from the CSS list.
231 * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
232 */
233 public function replace($oOldItem, $oNewItem) {
234 $iKey = array_search($oOldItem, $this->aContents, true);
235 if ($iKey !== false) {
236 array_splice($this->aContents, $iKey, 1, $oNewItem);
237 return true;
238 }
239 return false;
240 }
241
fbe18cc0
FM
242 /**
243 * Set the contents.
244 * @param array $aContents Objects to set as content.
245 */
246 public function setContents(array $aContents) {
247 $this->aContents = array();
248 foreach ($aContents as $content) {
249 $this->append($content);
250 }
251 }
252
253 /**
254 * Removes a declaration block from the CSS list if it matches all given selectors.
255 * @param array|string $mSelector The selectors to match.
256 * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks
257 */
258 public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) {
259 if ($mSelector instanceof DeclarationBlock) {
260 $mSelector = $mSelector->getSelectors();
261 }
262 if (!is_array($mSelector)) {
263 $mSelector = explode(',', $mSelector);
264 }
265 foreach ($mSelector as $iKey => &$mSel) {
266 if (!($mSel instanceof Selector)) {
267 $mSel = new Selector($mSel);
268 }
269 }
270 foreach ($this->aContents as $iKey => $mItem) {
271 if (!($mItem instanceof DeclarationBlock)) {
272 continue;
273 }
274 if ($mItem->getSelectors() == $mSelector) {
275 unset($this->aContents[$iKey]);
276 if (!$bRemoveAll) {
277 return;
278 }
279 }
280 }
281 }
282
283 public function __toString() {
284 return $this->render(new \Sabberworm\CSS\OutputFormat());
285 }
286
287 public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
288 $sResult = '';
289 $bIsFirst = true;
290 $oNextLevel = $oOutputFormat;
291 if(!$this->isRootList()) {
292 $oNextLevel = $oOutputFormat->nextLevel();
293 }
294 foreach ($this->aContents as $oContent) {
295 $sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) {
296 return $oContent->render($oNextLevel);
297 });
298 if($sRendered === null) {
299 continue;
300 }
301 if($bIsFirst) {
302 $bIsFirst = false;
303 $sResult .= $oNextLevel->spaceBeforeBlocks();
304 } else {
305 $sResult .= $oNextLevel->spaceBetweenBlocks();
306 }
307 $sResult .= $sRendered;
308 }
309
310 if(!$bIsFirst) {
311 // Had some output
312 $sResult .= $oOutputFormat->spaceAfterBlocks();
313 }
314
315 return $sResult;
316 }
317
318 /**
319 * Return true if the list can not be further outdented. Only important when rendering.
320 */
321 public abstract function isRootList();
322
323 public function getContents() {
324 return $this->aContents;
325 }
326
327 /**
328 * @param array $aComments Array of comments.
329 */
330 public function addComments(array $aComments) {
331 $this->aComments = array_merge($this->aComments, $aComments);
332 }
333
334 /**
335 * @return array
336 */
337 public function getComments() {
338 return $this->aComments;
339 }
340
341 /**
342 * @param array $aComments Array containing Comment objects.
343 */
344 public function setComments(array $aComments) {
345 $this->aComments = $aComments;
346 }
347
348}