Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
 * This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector),
28
 * `RuleSet`s as well as other `CSSList` objects.
29
 *
30
 * It can also 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
        $aComments = [];
73
        while (!$oParserState->isEnd()) {
74
            $aComments = array_merge($aComments, $oParserState->consumeWhiteSpace());
75
            $oListItem = null;
76
            if ($bLenientParsing) {
77
                try {
78
                    $oListItem = self::parseListItem($oParserState, $oList);
79
                } catch (UnexpectedTokenException $e) {
80
                    $oListItem = false;
81
                }
82
            } else {
83
                $oListItem = self::parseListItem($oParserState, $oList);
84
            }
85
            if ($oListItem === null) {
86
                // List parsing finished
87
                return;
88
            }
89
            if ($oListItem) {
90
                $oListItem->addComments($aComments);
91
                $oList->append($oListItem);
92
            }
93
            $aComments = $oParserState->consumeWhiteSpace();
94
        }
95
        $oList->addComments($aComments);
96
        if (!$bIsRoot && !$bLenientParsing) {
97
            throw new SourceException("Unexpected end of document", $oParserState->currentLine());
98
        }
99
    }
100
 
101
    /**
102
     * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
103
     *
104
     * @throws SourceException
105
     * @throws UnexpectedEOFException
106
     * @throws UnexpectedTokenException
107
     */
108
    private static function parseListItem(ParserState $oParserState, CSSList $oList)
109
    {
110
        $bIsRoot = $oList instanceof Document;
111
        if ($oParserState->comes('@')) {
112
            $oAtRule = self::parseAtRule($oParserState);
113
            if ($oAtRule instanceof Charset) {
114
                if (!$bIsRoot) {
115
                    throw new UnexpectedTokenException(
116
                        '@charset may only occur in root document',
117
                        '',
118
                        'custom',
119
                        $oParserState->currentLine()
120
                    );
121
                }
122
                if (count($oList->getContents()) > 0) {
123
                    throw new UnexpectedTokenException(
124
                        '@charset must be the first parseable token in a document',
125
                        '',
126
                        'custom',
127
                        $oParserState->currentLine()
128
                    );
129
                }
130
                $oParserState->setCharset($oAtRule->getCharset());
131
            }
132
            return $oAtRule;
133
        } elseif ($oParserState->comes('}')) {
134
            if ($bIsRoot) {
135
                if ($oParserState->getSettings()->bLenientParsing) {
136
                    return DeclarationBlock::parse($oParserState);
137
                } else {
138
                    throw new SourceException("Unopened {", $oParserState->currentLine());
139
                }
140
            } else {
141
                // End of list
142
                return null;
143
            }
144
        } else {
145
            return DeclarationBlock::parse($oParserState, $oList);
146
        }
147
    }
148
 
149
    /**
150
     * @param ParserState $oParserState
151
     *
152
     * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
153
     *
154
     * @throws SourceException
155
     * @throws UnexpectedTokenException
156
     * @throws UnexpectedEOFException
157
     */
158
    private static function parseAtRule(ParserState $oParserState)
159
    {
160
        $oParserState->consume('@');
161
        $sIdentifier = $oParserState->parseIdentifier();
162
        $iIdentifierLineNum = $oParserState->currentLine();
163
        $oParserState->consumeWhiteSpace();
164
        if ($sIdentifier === 'import') {
165
            $oLocation = URL::parse($oParserState);
166
            $oParserState->consumeWhiteSpace();
167
            $sMediaQuery = null;
168
            if (!$oParserState->comes(';')) {
169
                $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF]));
170
            }
171
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
172
            return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
173
        } elseif ($sIdentifier === 'charset') {
174
            $oCharsetString = CSSString::parse($oParserState);
175
            $oParserState->consumeWhiteSpace();
176
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
177
            return new Charset($oCharsetString, $iIdentifierLineNum);
178
        } elseif (self::identifierIs($sIdentifier, 'keyframes')) {
179
            $oResult = new KeyFrame($iIdentifierLineNum);
180
            $oResult->setVendorKeyFrame($sIdentifier);
181
            $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
182
            CSSList::parseList($oParserState, $oResult);
183
            if ($oParserState->comes('}')) {
184
                $oParserState->consume('}');
185
            }
186
            return $oResult;
187
        } elseif ($sIdentifier === 'namespace') {
188
            $sPrefix = null;
189
            $mUrl = Value::parsePrimitiveValue($oParserState);
190
            if (!$oParserState->comes(';')) {
191
                $sPrefix = $mUrl;
192
                $mUrl = Value::parsePrimitiveValue($oParserState);
193
            }
194
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
195
            if ($sPrefix !== null && !is_string($sPrefix)) {
196
                throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
197
            }
198
            if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
199
                throw new UnexpectedTokenException(
200
                    'Wrong namespace url of invalid type',
201
                    $mUrl,
202
                    'custom',
203
                    $iIdentifierLineNum
204
                );
205
            }
206
            return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
207
        } else {
208
            // Unknown other at rule (font-face or such)
209
            $sArgs = trim($oParserState->consumeUntil('{', false, true));
210
            if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
211
                if ($oParserState->getSettings()->bLenientParsing) {
212
                    return null;
213
                } else {
214
                    throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
215
                }
216
            }
217
            $bUseRuleSet = true;
218
            foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
219
                if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
220
                    $bUseRuleSet = false;
221
                    break;
222
                }
223
            }
224
            if ($bUseRuleSet) {
225
                $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
226
                RuleSet::parseRuleSet($oParserState, $oAtRule);
227
            } else {
228
                $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
229
                CSSList::parseList($oParserState, $oAtRule);
230
                if ($oParserState->comes('}')) {
231
                    $oParserState->consume('}');
232
                }
233
            }
234
            return $oAtRule;
235
        }
236
    }
237
 
238
    /**
239
     * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed.
240
     * We need to check for these versions too.
241
     *
242
     * @param string $sIdentifier
243
     * @param string $sMatch
244
     *
245
     * @return bool
246
     */
247
    private static function identifierIs($sIdentifier, $sMatch)
248
    {
249
        return (strcasecmp($sIdentifier, $sMatch) === 0)
250
            ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
251
    }
252
 
253
    /**
254
     * @return int
255
     */
256
    public function getLineNo()
257
    {
258
        return $this->iLineNo;
259
    }
260
 
261
    /**
262
     * Prepends an item to the list of contents.
263
     *
264
     * @param RuleSet|CSSList|Import|Charset $oItem
265
     *
266
     * @return void
267
     */
268
    public function prepend($oItem)
269
    {
270
        array_unshift($this->aContents, $oItem);
271
    }
272
 
273
    /**
274
     * Appends an item to the list of contents.
275
     *
276
     * @param RuleSet|CSSList|Import|Charset $oItem
277
     *
278
     * @return void
279
     */
280
    public function append($oItem)
281
    {
282
        $this->aContents[] = $oItem;
283
    }
284
 
285
    /**
286
     * Splices the list of contents.
287
     *
288
     * @param int $iOffset
289
     * @param int $iLength
290
     * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
291
     *
292
     * @return void
293
     */
294
    public function splice($iOffset, $iLength = null, $mReplacement = null)
295
    {
296
        array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
297
    }
298
 
299
    /**
300
     * Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
301
     * the item is appended at the end.
302
     *
303
     * @param RuleSet|CSSList|Import|Charset $item
304
     * @param RuleSet|CSSList|Import|Charset $sibling
305
     */
306
    public function insertBefore($item, $sibling)
307
    {
308
        if (in_array($sibling, $this->aContents, true)) {
309
            $this->replace($sibling, [$item, $sibling]);
310
        } else {
311
            $this->append($item);
312
        }
313
    }
314
 
315
    /**
316
     * Removes an item from the CSS list.
317
     *
318
     * @param RuleSet|Import|Charset|CSSList $oItemToRemove
319
     *        May be a RuleSet (most likely a DeclarationBlock), a Import,
320
     *        a Charset or another CSSList (most likely a MediaQuery)
321
     *
322
     * @return bool whether the item was removed
323
     */
324
    public function remove($oItemToRemove)
325
    {
326
        $iKey = array_search($oItemToRemove, $this->aContents, true);
327
        if ($iKey !== false) {
328
            unset($this->aContents[$iKey]);
329
            return true;
330
        }
331
        return false;
332
    }
333
 
334
    /**
335
     * Replaces an item from the CSS list.
336
     *
337
     * @param RuleSet|Import|Charset|CSSList $oOldItem
338
     *        May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
339
     *        or another `CSSList` (most likely a `MediaQuery`)
340
     *
341
     * @return bool
342
     */
343
    public function replace($oOldItem, $mNewItem)
344
    {
345
        $iKey = array_search($oOldItem, $this->aContents, true);
346
        if ($iKey !== false) {
347
            if (is_array($mNewItem)) {
348
                array_splice($this->aContents, $iKey, 1, $mNewItem);
349
            } else {
350
                array_splice($this->aContents, $iKey, 1, [$mNewItem]);
351
            }
352
            return true;
353
        }
354
        return false;
355
    }
356
 
357
    /**
358
     * @param array<int, RuleSet|Import|Charset|CSSList> $aContents
359
     */
360
    public function setContents(array $aContents)
361
    {
362
        $this->aContents = [];
363
        foreach ($aContents as $content) {
364
            $this->append($content);
365
        }
366
    }
367
 
368
    /**
369
     * Removes a declaration block from the CSS list if it matches all given selectors.
370
     *
371
     * @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match
372
     * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
373
     *
374
     * @return void
375
     */
376
    public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false)
377
    {
378
        if ($mSelector instanceof DeclarationBlock) {
379
            $mSelector = $mSelector->getSelectors();
380
        }
381
        if (!is_array($mSelector)) {
382
            $mSelector = explode(',', $mSelector);
383
        }
384
        foreach ($mSelector as $iKey => &$mSel) {
385
            if (!($mSel instanceof Selector)) {
386
                if (!Selector::isValid($mSel)) {
387
                    throw new UnexpectedTokenException(
388
                        "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
389
                        $mSel,
390
                        "custom"
391
                    );
392
                }
393
                $mSel = new Selector($mSel);
394
            }
395
        }
396
        foreach ($this->aContents as $iKey => $mItem) {
397
            if (!($mItem instanceof DeclarationBlock)) {
398
                continue;
399
            }
400
            if ($mItem->getSelectors() == $mSelector) {
401
                unset($this->aContents[$iKey]);
402
                if (!$bRemoveAll) {
403
                    return;
404
                }
405
            }
406
        }
407
    }
408
 
409
    /**
410
     * @return string
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
}