MDL-65759 library: Update php-css-parser to 8.3.0
[moodle.git] / lib / php-css-parser / CSSList / CSSList.php
1 <?php
3 namespace Sabberworm\CSS\CSSList;
5 use Sabberworm\CSS\Comment\Commentable;
6 use Sabberworm\CSS\Parsing\ParserState;
7 use Sabberworm\CSS\Parsing\SourceException;
8 use Sabberworm\CSS\Parsing\UnexpectedTokenException;
9 use Sabberworm\CSS\Property\AtRule;
10 use Sabberworm\CSS\Property\Charset;
11 use Sabberworm\CSS\Property\CSSNamespace;
12 use Sabberworm\CSS\Property\Import;
13 use Sabberworm\CSS\Property\Selector;
14 use Sabberworm\CSS\Renderable;
15 use Sabberworm\CSS\RuleSet\AtRuleSet;
16 use Sabberworm\CSS\RuleSet\DeclarationBlock;
17 use Sabberworm\CSS\RuleSet\RuleSet;
18 use Sabberworm\CSS\Value\CSSString;
19 use Sabberworm\CSS\Value\URL;
20 use Sabberworm\CSS\Value\Value;
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  */
26 abstract class CSSList implements Renderable, Commentable {
28         protected $aComments;
29         protected $aContents;
30         protected $iLineNo;
32         public function __construct($iLineNo = 0) {
33                 $this->aComments = array();
34                 $this->aContents = array();
35                 $this->iLineNo = $iLineNo;
36         }
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         }
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         }
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         }
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         }
179         /**
180          * @return int
181          */
182         public function getLineNo() {
183                 return $this->iLineNo;
184         }
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         }
195         /**
196          * Append item to list of contents.
197          *
198          * @param object $oItem Item.
199          */
200         public function append($oItem) {
201                 $this->aContents[] = $oItem;
202         }
204         /**
205          * Splice the list of contents.
206          *
207          * @param int       $iOffset      Offset.
208          * @param int       $iLength      Length. Optional.
209          * @param RuleSet[] $mReplacement Replacement. Optional.
210          */
211         public function splice($iOffset, $iLength = null, $mReplacement = null) {
212                 array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
213         }
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)
218          * @return bool Whether the item was removed.
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         }
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         }
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         }
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         }
283         public function __toString() {
284                 return $this->render(new \Sabberworm\CSS\OutputFormat());
285         }
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                 }
310                 if(!$bIsFirst) {
311                         // Had some output
312                         $sResult .= $oOutputFormat->spaceAfterBlocks();
313                 }
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();
323         public function getContents() {
324                 return $this->aContents;
325         }
327         /**
328          * @param array $aComments Array of comments.
329          */
330         public function addComments(array $aComments) {
331                 $this->aComments = array_merge($this->aComments, $aComments);
332         }
334         /**
335          * @return array
336          */
337         public function getComments() {
338                 return $this->aComments;
339         }
341         /**
342          * @param array $aComments Array containing Comment objects.
343          */
344         public function setComments(array $aComments) {
345                 $this->aComments = $aComments;
346         }