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\RuleSet;
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\UnexpectedEOFException;
10
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
11
use Sabberworm\CSS\Renderable;
12
use Sabberworm\CSS\Rule\Rule;
13
 
14
/**
15
 * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
16
 * However, unknown At-Rules (like `@font-face`) are also rule sets.
17
 */
18
abstract class RuleSet implements Renderable, Commentable
19
{
20
    /**
21
     * @var array<string, Rule>
22
     */
23
    private $aRules;
24
 
25
    /**
26
     * @var int
27
     */
28
    protected $iLineNo;
29
 
30
    /**
31
     * @var array<array-key, Comment>
32
     */
33
    protected $aComments;
34
 
35
    /**
36
     * @param int $iLineNo
37
     */
38
    public function __construct($iLineNo = 0)
39
    {
40
        $this->aRules = [];
41
        $this->iLineNo = $iLineNo;
42
        $this->aComments = [];
43
    }
44
 
45
    /**
46
     * @return void
47
     *
48
     * @throws UnexpectedTokenException
49
     * @throws UnexpectedEOFException
50
     */
51
    public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
52
    {
53
        while ($oParserState->comes(';')) {
54
            $oParserState->consume(';');
55
        }
56
        while (!$oParserState->comes('}')) {
57
            $oRule = null;
58
            if ($oParserState->getSettings()->bLenientParsing) {
59
                try {
60
                    $oRule = Rule::parse($oParserState);
61
                } catch (UnexpectedTokenException $e) {
62
                    try {
63
                        $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
64
                        // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
65
                        if ($oParserState->streql(substr($sConsume, -1), '}')) {
66
                            $oParserState->backtrack(1);
67
                        } else {
68
                            while ($oParserState->comes(';')) {
69
                                $oParserState->consume(';');
70
                            }
71
                        }
72
                    } catch (UnexpectedTokenException $e) {
73
                        // We’ve reached the end of the document. Just close the RuleSet.
74
                        return;
75
                    }
76
                }
77
            } else {
78
                $oRule = Rule::parse($oParserState);
79
            }
80
            if ($oRule) {
81
                $oRuleSet->addRule($oRule);
82
            }
83
        }
84
        $oParserState->consume('}');
85
    }
86
 
87
    /**
88
     * @return int
89
     */
90
    public function getLineNo()
91
    {
92
        return $this->iLineNo;
93
    }
94
 
95
    /**
96
     * @param Rule|null $oSibling
97
     *
98
     * @return void
99
     */
100
    public function addRule(Rule $oRule, Rule $oSibling = null)
101
    {
102
        $sRule = $oRule->getRule();
103
        if (!isset($this->aRules[$sRule])) {
104
            $this->aRules[$sRule] = [];
105
        }
106
 
107
        $iPosition = count($this->aRules[$sRule]);
108
 
109
        if ($oSibling !== null) {
110
            $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
111
            if ($iSiblingPos !== false) {
112
                $iPosition = $iSiblingPos;
113
                $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
114
            }
115
        }
116
        if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
117
            //this node is added manually, give it the next best line
118
            $rules = $this->getRules();
119
            $pos = count($rules);
120
            if ($pos > 0) {
121
                $last = $rules[$pos - 1];
122
                $oRule->setPosition($last->getLineNo() + 1, 0);
123
            }
124
        }
125
 
126
        array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
127
    }
128
 
129
    /**
130
     * Returns all rules matching the given rule name
131
     *
132
     * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
133
     *
134
     * @example $oRuleSet->getRules('font-')
135
     *          //returns an array of all rules either beginning with font- or matching font.
136
     *
137
     * @param Rule|string|null $mRule
138
     *        Pattern to search for. If null, returns all rules.
139
     *        If the pattern ends with a dash, all rules starting with the pattern are returned
140
     *        as well as one matching the pattern with the dash excluded.
141
     *        Passing a Rule behaves like calling `getRules($mRule->getRule())`.
142
     *
143
     * @return array<int, Rule>
144
     */
145
    public function getRules($mRule = null)
146
    {
147
        if ($mRule instanceof Rule) {
148
            $mRule = $mRule->getRule();
149
        }
150
        /** @var array<int, Rule> $aResult */
151
        $aResult = [];
152
        foreach ($this->aRules as $sName => $aRules) {
153
            // Either no search rule is given or the search rule matches the found rule exactly
154
            // or the search rule ends in “-” and the found rule starts with the search rule.
155
            if (
156
                !$mRule || $sName === $mRule
157
                || (
158
                    strrpos($mRule, '-') === strlen($mRule) - strlen('-')
159
                    && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))
160
                )
161
            ) {
162
                $aResult = array_merge($aResult, $aRules);
163
            }
164
        }
165
        usort($aResult, function (Rule $first, Rule $second) {
166
            if ($first->getLineNo() === $second->getLineNo()) {
167
                return $first->getColNo() - $second->getColNo();
168
            }
169
            return $first->getLineNo() - $second->getLineNo();
170
        });
171
        return $aResult;
172
    }
173
 
174
    /**
175
     * Overrides all the rules of this set.
176
     *
177
     * @param array<array-key, Rule> $aRules The rules to override with.
178
     *
179
     * @return void
180
     */
181
    public function setRules(array $aRules)
182
    {
183
        $this->aRules = [];
184
        foreach ($aRules as $rule) {
185
            $this->addRule($rule);
186
        }
187
    }
188
 
189
    /**
190
     * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name
191
     * as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
192
     *
193
     * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block
194
     * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array
195
     * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both.
196
     *
197
     * @param Rule|string|null $mRule $mRule
198
     *        Pattern to search for. If null, returns all rules. If the pattern ends with a dash,
199
     *        all rules starting with the pattern are returned as well as one matching the pattern with the dash
200
     *        excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`.
201
     *
202
     * @return array<string, Rule>
203
     */
204
    public function getRulesAssoc($mRule = null)
205
    {
206
        /** @var array<string, Rule> $aResult */
207
        $aResult = [];
208
        foreach ($this->getRules($mRule) as $oRule) {
209
            $aResult[$oRule->getRule()] = $oRule;
210
        }
211
        return $aResult;
212
    }
213
 
214
    /**
215
     * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts.
216
     *
217
     * If given a Rule, it will only remove this particular rule (by identity).
218
     * If given a name, it will remove all rules by that name.
219
     *
220
     * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would
221
     * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`.
222
     *
223
     * @param Rule|string|null $mRule
224
     *        pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash,
225
     *        all rules starting with the pattern are removed as well as one matching the pattern with the dash
226
     *        excluded. Passing a Rule behaves matches by identity.
227
     *
228
     * @return void
229
     */
230
    public function removeRule($mRule)
231
    {
232
        if ($mRule instanceof Rule) {
233
            $sRule = $mRule->getRule();
234
            if (!isset($this->aRules[$sRule])) {
235
                return;
236
            }
237
            foreach ($this->aRules[$sRule] as $iKey => $oRule) {
238
                if ($oRule === $mRule) {
239
                    unset($this->aRules[$sRule][$iKey]);
240
                }
241
            }
242
        } else {
243
            foreach ($this->aRules as $sName => $aRules) {
244
                // Either no search rule is given or the search rule matches the found rule exactly
245
                // or the search rule ends in “-” and the found rule starts with the search rule or equals it
246
                // (without the trailing dash).
247
                if (
248
                    !$mRule || $sName === $mRule
249
                    || (strrpos($mRule, '-') === strlen($mRule) - strlen('-')
250
                        && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))
251
                ) {
252
                    unset($this->aRules[$sName]);
253
                }
254
            }
255
        }
256
    }
257
 
258
    /**
259
     * @return string
260
     */
261
    public function __toString()
262
    {
263
        return $this->render(new OutputFormat());
264
    }
265
 
266
    /**
267
     * @return string
268
     */
269
    public function render(OutputFormat $oOutputFormat)
270
    {
271
        $sResult = '';
272
        $bIsFirst = true;
273
        foreach ($this->aRules as $aRules) {
274
            foreach ($aRules as $oRule) {
275
                $sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) {
276
                    return $oRule->render($oOutputFormat->nextLevel());
277
                });
278
                if ($sRendered === null) {
279
                    continue;
280
                }
281
                if ($bIsFirst) {
282
                    $bIsFirst = false;
283
                    $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
284
                } else {
285
                    $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
286
                }
287
                $sResult .= $sRendered;
288
            }
289
        }
290
 
291
        if (!$bIsFirst) {
292
            // Had some output
293
            $sResult .= $oOutputFormat->spaceAfterRules();
294
        }
295
 
296
        return $oOutputFormat->removeLastSemicolon($sResult);
297
    }
298
 
299
    /**
300
     * @param array<string, Comment> $aComments
301
     *
302
     * @return void
303
     */
304
    public function addComments(array $aComments)
305
    {
306
        $this->aComments = array_merge($this->aComments, $aComments);
307
    }
308
 
309
    /**
310
     * @return array<string, Comment>
311
     */
312
    public function getComments()
313
    {
314
        return $this->aComments;
315
    }
316
 
317
    /**
318
     * @param array<string, Comment> $aComments
319
     *
320
     * @return void
321
     */
322
    public function setComments(array $aComments)
323
    {
324
        $this->aComments = $aComments;
325
    }
326
}