Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
        }
93
        if (!$bIsRoot && !$bLenientParsing) {
94
            throw new SourceException("Unexpected end of document", $oParserState->currentLine());
95
        }
96
    }
97
 
98
    /**
99
     * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
100
     *
101
     * @throws SourceException
102
     * @throws UnexpectedEOFException
103
     * @throws UnexpectedTokenException
104
     */
105
    private static function parseListItem(ParserState $oParserState, CSSList $oList)
106
    {
107
        $bIsRoot = $oList instanceof Document;
108
        if ($oParserState->comes('@')) {
109
            $oAtRule = self::parseAtRule($oParserState);
110
            if ($oAtRule instanceof Charset) {
111
                if (!$bIsRoot) {
112
                    throw new UnexpectedTokenException(
113
                        '@charset may only occur in root document',
114
                        '',
115
                        'custom',
116
                        $oParserState->currentLine()
117
                    );
118
                }
119
                if (count($oList->getContents()) > 0) {
120
                    throw new UnexpectedTokenException(
121
                        '@charset must be the first parseable token in a document',
122
                        '',
123
                        'custom',
124
                        $oParserState->currentLine()
125
                    );
126
                }
127
                $oParserState->setCharset($oAtRule->getCharset()->getString());
128
            }
129
            return $oAtRule;
130
        } elseif ($oParserState->comes('}')) {
131
            if (!$oParserState->getSettings()->bLenientParsing) {
132
                throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine());
133
            } else {
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
                    return null;
142
                }
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
            $sCharset = CSSString::parse($oParserState);
175
            $oParserState->consumeWhiteSpace();
176
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
177
            return new Charset($sCharset, $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 tje 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
     * Insert an item before its sibling.
287
     *
288
     * @param mixed $oItem The item.
289
     * @param mixed $oSibling The sibling.
290
     */
291
    public function insert($oItem, $oSibling) {
292
        $iIndex = array_search($oSibling, $this->aContents);
293
        if ($iIndex === false) {
294
            return $this->append($oItem);
295
        }
296
        array_splice($this->aContents, $iIndex, 0, array($oItem));
297
    }
298
 
299
    /**
300
     * Splices the list of contents.
301
     *
302
     * @param int $iOffset
303
     * @param int $iLength
304
     * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
305
     *
306
     * @return void
307
     */
308
    public function splice($iOffset, $iLength = null, $mReplacement = null)
309
    {
310
        array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
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
    public function __toString()
411
    {
412
        return $this->render(new OutputFormat());
413
    }
414
 
415
    /**
416
     * @return string
417
     */
418
    public function render(OutputFormat $oOutputFormat)
419
    {
420
        $sResult = '';
421
        $bIsFirst = true;
422
        $oNextLevel = $oOutputFormat;
423
        if (!$this->isRootList()) {
424
            $oNextLevel = $oOutputFormat->nextLevel();
425
        }
426
        foreach ($this->aContents as $oContent) {
427
            $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
428
                return $oContent->render($oNextLevel);
429
            });
430
            if ($sRendered === null) {
431
                continue;
432
            }
433
            if ($bIsFirst) {
434
                $bIsFirst = false;
435
                $sResult .= $oNextLevel->spaceBeforeBlocks();
436
            } else {
437
                $sResult .= $oNextLevel->spaceBetweenBlocks();
438
            }
439
            $sResult .= $sRendered;
440
        }
441
 
442
        if (!$bIsFirst) {
443
            // Had some output
444
            $sResult .= $oOutputFormat->spaceAfterBlocks();
445
        }
446
 
447
        return $sResult;
448
    }
449
 
450
    /**
451
     * Return true if the list can not be further outdented. Only important when rendering.
452
     *
453
     * @return bool
454
     */
455
    abstract public function isRootList();
456
 
457
    /**
458
     * @return array<int, RuleSet|Import|Charset|CSSList>
459
     */
460
    public function getContents()
461
    {
462
        return $this->aContents;
463
    }
464
 
465
    /**
466
     * @param array<array-key, Comment> $aComments
467
     *
468
     * @return void
469
     */
470
    public function addComments(array $aComments)
471
    {
472
        $this->aComments = array_merge($this->aComments, $aComments);
473
    }
474
 
475
    /**
476
     * @return array<array-key, Comment>
477
     */
478
    public function getComments()
479
    {
480
        return $this->aComments;
481
    }
482
 
483
    /**
484
     * @param array<array-key, Comment> $aComments
485
     *
486
     * @return void
487
     */
488
    public function setComments(array $aComments)
489
    {
490
        $this->aComments = $aComments;
491
    }
492
}