MDL-65759 library: Update php-css-parser to 8.3.0
[moodle.git] / lib / php-css-parser / RuleSet / DeclarationBlock.php
CommitLineData
fbe18cc0
FM
1<?php
2
3namespace Sabberworm\CSS\RuleSet;
4
376eb156
MM
5use Sabberworm\CSS\Parsing\ParserState;
6use Sabberworm\CSS\Parsing\OutputException;
fbe18cc0
FM
7use Sabberworm\CSS\Property\Selector;
8use Sabberworm\CSS\Rule\Rule;
9use Sabberworm\CSS\Value\RuleValueList;
10use Sabberworm\CSS\Value\Value;
11use Sabberworm\CSS\Value\Size;
12use Sabberworm\CSS\Value\Color;
13use Sabberworm\CSS\Value\URL;
fbe18cc0
FM
14
15/**
16 * Declaration blocks are the parts of a css file which denote the rules belonging to a selector.
17 * Declaration blocks usually appear directly inside a Document or another CSSList (mostly a MediaQuery).
18 */
19class DeclarationBlock extends RuleSet {
20
21 private $aSelectors;
22
23 public function __construct($iLineNo = 0) {
24 parent::__construct($iLineNo);
25 $this->aSelectors = array();
26 }
27
376eb156
MM
28 public static function parse(ParserState $oParserState) {
29 $aComments = array();
30 $oResult = new DeclarationBlock($oParserState->currentLine());
31 $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments));
32 $oResult->setComments($aComments);
33 RuleSet::parseRuleSet($oParserState, $oResult);
34 return $oResult;
35 }
36
37
fbe18cc0
FM
38 public function setSelectors($mSelector) {
39 if (is_array($mSelector)) {
40 $this->aSelectors = $mSelector;
41 } else {
42 $this->aSelectors = explode(',', $mSelector);
43 }
44 foreach ($this->aSelectors as $iKey => $mSelector) {
45 if (!($mSelector instanceof Selector)) {
46 $this->aSelectors[$iKey] = new Selector($mSelector);
47 }
48 }
49 }
50
51 // remove one of the selector of the block
52 public function removeSelector($mSelector) {
53 if($mSelector instanceof Selector) {
54 $mSelector = $mSelector->getSelector();
55 }
56 foreach($this->aSelectors as $iKey => $oSelector) {
57 if($oSelector->getSelector() === $mSelector) {
58 unset($this->aSelectors[$iKey]);
59 return true;
60 }
61 }
62 return false;
63 }
64
65 /**
66 * @deprecated use getSelectors()
67 */
68 public function getSelector() {
69 return $this->getSelectors();
70 }
71
72 /**
73 * @deprecated use setSelectors()
74 */
75 public function setSelector($mSelector) {
76 $this->setSelectors($mSelector);
77 }
78
376eb156
MM
79 /**
80 * Get selectors.
81 *
82 * @return Selector[] Selectors.
83 */
fbe18cc0
FM
84 public function getSelectors() {
85 return $this->aSelectors;
86 }
87
88 /**
89 * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
90 * */
91 public function expandShorthands() {
92 // border must be expanded before dimensions
93 $this->expandBorderShorthand();
94 $this->expandDimensionsShorthand();
95 $this->expandFontShorthand();
96 $this->expandBackgroundShorthand();
97 $this->expandListStyleShorthand();
98 }
99
100 /**
101 * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
102 * */
103 public function createShorthands() {
104 $this->createBackgroundShorthand();
105 $this->createDimensionsShorthand();
106 // border must be shortened after dimensions
107 $this->createBorderShorthand();
108 $this->createFontShorthand();
109 $this->createListStyleShorthand();
110 }
111
112 /**
113 * Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
114 * Additional splitting happens in expandDimensionsShorthand
115 * Multiple borders are not yet supported as of 3
116 * */
117 public function expandBorderShorthand() {
118 $aBorderRules = array(
119 'border', 'border-left', 'border-right', 'border-top', 'border-bottom'
120 );
121 $aBorderSizes = array(
122 'thin', 'medium', 'thick'
123 );
124 $aRules = $this->getRulesAssoc();
125 foreach ($aBorderRules as $sBorderRule) {
126 if (!isset($aRules[$sBorderRule]))
127 continue;
128 $oRule = $aRules[$sBorderRule];
129 $mRuleValue = $oRule->getValue();
130 $aValues = array();
131 if (!$mRuleValue instanceof RuleValueList) {
132 $aValues[] = $mRuleValue;
133 } else {
134 $aValues = $mRuleValue->getListComponents();
135 }
136 foreach ($aValues as $mValue) {
137 if ($mValue instanceof Value) {
138 $mNewValue = clone $mValue;
139 } else {
140 $mNewValue = $mValue;
141 }
142 if ($mValue instanceof Size) {
143 $sNewRuleName = $sBorderRule . "-width";
144 } else if ($mValue instanceof Color) {
145 $sNewRuleName = $sBorderRule . "-color";
146 } else {
147 if (in_array($mValue, $aBorderSizes)) {
148 $sNewRuleName = $sBorderRule . "-width";
149 } else/* if(in_array($mValue, $aBorderStyles)) */ {
150 $sNewRuleName = $sBorderRule . "-style";
151 }
152 }
153 $oNewRule = new Rule($sNewRuleName, $this->iLineNo);
154 $oNewRule->setIsImportant($oRule->getIsImportant());
155 $oNewRule->addValue(array($mNewValue));
156 $this->addRule($oNewRule);
157 }
158 $this->removeRule($sBorderRule);
159 }
160 }
161
162 /**
163 * Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
164 * into their constituent parts.
165 * Handles margin, padding, border-color, border-style and border-width.
166 * */
167 public function expandDimensionsShorthand() {
168 $aExpansions = array(
169 'margin' => 'margin-%s',
170 'padding' => 'padding-%s',
171 'border-color' => 'border-%s-color',
172 'border-style' => 'border-%s-style',
173 'border-width' => 'border-%s-width'
174 );
175 $aRules = $this->getRulesAssoc();
176 foreach ($aExpansions as $sProperty => $sExpanded) {
177 if (!isset($aRules[$sProperty]))
178 continue;
179 $oRule = $aRules[$sProperty];
180 $mRuleValue = $oRule->getValue();
181 $aValues = array();
182 if (!$mRuleValue instanceof RuleValueList) {
183 $aValues[] = $mRuleValue;
184 } else {
185 $aValues = $mRuleValue->getListComponents();
186 }
187 $top = $right = $bottom = $left = null;
188 switch (count($aValues)) {
189 case 1:
190 $top = $right = $bottom = $left = $aValues[0];
191 break;
192 case 2:
193 $top = $bottom = $aValues[0];
194 $left = $right = $aValues[1];
195 break;
196 case 3:
197 $top = $aValues[0];
198 $left = $right = $aValues[1];
199 $bottom = $aValues[2];
200 break;
201 case 4:
202 $top = $aValues[0];
203 $right = $aValues[1];
204 $bottom = $aValues[2];
205 $left = $aValues[3];
206 break;
207 }
208 foreach (array('top', 'right', 'bottom', 'left') as $sPosition) {
209 $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $this->iLineNo);
210 $oNewRule->setIsImportant($oRule->getIsImportant());
211 $oNewRule->addValue(${$sPosition});
212 $this->addRule($oNewRule);
213 }
214 $this->removeRule($sProperty);
215 }
216 }
217
218 /**
219 * Convert shorthand font declarations
220 * (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
221 * into their constituent parts.
222 * */
223 public function expandFontShorthand() {
224 $aRules = $this->getRulesAssoc();
225 if (!isset($aRules['font']))
226 return;
227 $oRule = $aRules['font'];
228 // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand
229 $aFontProperties = array(
230 'font-style' => 'normal',
231 'font-variant' => 'normal',
232 'font-weight' => 'normal',
233 'font-size' => 'normal',
234 'line-height' => 'normal'
235 );
236 $mRuleValue = $oRule->getValue();
237 $aValues = array();
238 if (!$mRuleValue instanceof RuleValueList) {
239 $aValues[] = $mRuleValue;
240 } else {
241 $aValues = $mRuleValue->getListComponents();
242 }
243 foreach ($aValues as $mValue) {
244 if (!$mValue instanceof Value) {
245 $mValue = mb_strtolower($mValue);
246 }
247 if (in_array($mValue, array('normal', 'inherit'))) {
248 foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) {
249 if (!isset($aFontProperties[$sProperty])) {
250 $aFontProperties[$sProperty] = $mValue;
251 }
252 }
253 } else if (in_array($mValue, array('italic', 'oblique'))) {
254 $aFontProperties['font-style'] = $mValue;
255 } else if ($mValue == 'small-caps') {
256 $aFontProperties['font-variant'] = $mValue;
257 } else if (
258 in_array($mValue, array('bold', 'bolder', 'lighter'))
259 || ($mValue instanceof Size
260 && in_array($mValue->getSize(), range(100, 900, 100)))
261 ) {
262 $aFontProperties['font-weight'] = $mValue;
263 } else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') {
264 list($oSize, $oHeight) = $mValue->getListComponents();
265 $aFontProperties['font-size'] = $oSize;
266 $aFontProperties['line-height'] = $oHeight;
267 } else if ($mValue instanceof Size && $mValue->getUnit() !== null) {
268 $aFontProperties['font-size'] = $mValue;
269 } else {
270 $aFontProperties['font-family'] = $mValue;
271 }
272 }
273 foreach ($aFontProperties as $sProperty => $mValue) {
274 $oNewRule = new Rule($sProperty, $this->iLineNo);
275 $oNewRule->addValue($mValue);
276 $oNewRule->setIsImportant($oRule->getIsImportant());
277 $this->addRule($oNewRule);
278 }
279 $this->removeRule('font');
280 }
281
282 /*
283 * Convert shorthand background declarations
284 * (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
285 * into their constituent parts.
286 * @see http://www.w3.org/TR/21/colors.html#propdef-background
287 * */
288
289 public function expandBackgroundShorthand() {
290 $aRules = $this->getRulesAssoc();
291 if (!isset($aRules['background']))
292 return;
293 $oRule = $aRules['background'];
294 $aBgProperties = array(
295 'background-color' => array('transparent'), 'background-image' => array('none'),
296 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'),
297 'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo))
298 );
299 $mRuleValue = $oRule->getValue();
300 $aValues = array();
301 if (!$mRuleValue instanceof RuleValueList) {
302 $aValues[] = $mRuleValue;
303 } else {
304 $aValues = $mRuleValue->getListComponents();
305 }
306 if (count($aValues) == 1 && $aValues[0] == 'inherit') {
307 foreach ($aBgProperties as $sProperty => $mValue) {
308 $oNewRule = new Rule($sProperty, $this->iLineNo);
309 $oNewRule->addValue('inherit');
310 $oNewRule->setIsImportant($oRule->getIsImportant());
311 $this->addRule($oNewRule);
312 }
313 $this->removeRule('background');
314 return;
315 }
316 $iNumBgPos = 0;
317 foreach ($aValues as $mValue) {
318 if (!$mValue instanceof Value) {
319 $mValue = mb_strtolower($mValue);
320 }
321 if ($mValue instanceof URL) {
322 $aBgProperties['background-image'] = $mValue;
323 } else if ($mValue instanceof Color) {
324 $aBgProperties['background-color'] = $mValue;
325 } else if (in_array($mValue, array('scroll', 'fixed'))) {
326 $aBgProperties['background-attachment'] = $mValue;
327 } else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) {
328 $aBgProperties['background-repeat'] = $mValue;
329 } else if (in_array($mValue, array('left', 'center', 'right', 'top', 'bottom'))
330 || $mValue instanceof Size
331 ) {
332 if ($iNumBgPos == 0) {
333 $aBgProperties['background-position'][0] = $mValue;
334 $aBgProperties['background-position'][1] = 'center';
335 } else {
336 $aBgProperties['background-position'][$iNumBgPos] = $mValue;
337 }
338 $iNumBgPos++;
339 }
340 }
341 foreach ($aBgProperties as $sProperty => $mValue) {
342 $oNewRule = new Rule($sProperty, $this->iLineNo);
343 $oNewRule->setIsImportant($oRule->getIsImportant());
344 $oNewRule->addValue($mValue);
345 $this->addRule($oNewRule);
346 }
347 $this->removeRule('background');
348 }
349
350 public function expandListStyleShorthand() {
351 $aListProperties = array(
352 'list-style-type' => 'disc',
353 'list-style-position' => 'outside',
354 'list-style-image' => 'none'
355 );
356 $aListStyleTypes = array(
357 'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal',
358 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin',
359 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic',
360 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana'
361 );
362 $aListStylePositions = array(
363 'inside', 'outside'
364 );
365 $aRules = $this->getRulesAssoc();
366 if (!isset($aRules['list-style']))
367 return;
368 $oRule = $aRules['list-style'];
369 $mRuleValue = $oRule->getValue();
370 $aValues = array();
371 if (!$mRuleValue instanceof RuleValueList) {
372 $aValues[] = $mRuleValue;
373 } else {
374 $aValues = $mRuleValue->getListComponents();
375 }
376 if (count($aValues) == 1 && $aValues[0] == 'inherit') {
377 foreach ($aListProperties as $sProperty => $mValue) {
378 $oNewRule = new Rule($sProperty, $this->iLineNo);
379 $oNewRule->addValue('inherit');
380 $oNewRule->setIsImportant($oRule->getIsImportant());
381 $this->addRule($oNewRule);
382 }
383 $this->removeRule('list-style');
384 return;
385 }
386 foreach ($aValues as $mValue) {
387 if (!$mValue instanceof Value) {
388 $mValue = mb_strtolower($mValue);
389 }
390 if ($mValue instanceof Url) {
391 $aListProperties['list-style-image'] = $mValue;
392 } else if (in_array($mValue, $aListStyleTypes)) {
393 $aListProperties['list-style-types'] = $mValue;
394 } else if (in_array($mValue, $aListStylePositions)) {
395 $aListProperties['list-style-position'] = $mValue;
396 }
397 }
398 foreach ($aListProperties as $sProperty => $mValue) {
399 $oNewRule = new Rule($sProperty, $this->iLineNo);
400 $oNewRule->setIsImportant($oRule->getIsImportant());
401 $oNewRule->addValue($mValue);
402 $this->addRule($oNewRule);
403 }
404 $this->removeRule('list-style');
405 }
406
407 public function createShorthandProperties(array $aProperties, $sShorthand) {
408 $aRules = $this->getRulesAssoc();
409 $aNewValues = array();
410 foreach ($aProperties as $sProperty) {
411 if (!isset($aRules[$sProperty]))
412 continue;
413 $oRule = $aRules[$sProperty];
414 if (!$oRule->getIsImportant()) {
415 $mRuleValue = $oRule->getValue();
416 $aValues = array();
417 if (!$mRuleValue instanceof RuleValueList) {
418 $aValues[] = $mRuleValue;
419 } else {
420 $aValues = $mRuleValue->getListComponents();
421 }
422 foreach ($aValues as $mValue) {
423 $aNewValues[] = $mValue;
424 }
425 $this->removeRule($sProperty);
426 }
427 }
428 if (count($aNewValues)) {
429 $oNewRule = new Rule($sShorthand, $this->iLineNo);
430 foreach ($aNewValues as $mValue) {
431 $oNewRule->addValue($mValue);
432 }
433 $this->addRule($oNewRule);
434 }
435 }
436
437 public function createBackgroundShorthand() {
438 $aProperties = array(
439 'background-color', 'background-image', 'background-repeat',
440 'background-position', 'background-attachment'
441 );
442 $this->createShorthandProperties($aProperties, 'background');
443 }
444
445 public function createListStyleShorthand() {
446 $aProperties = array(
447 'list-style-type', 'list-style-position', 'list-style-image'
448 );
449 $this->createShorthandProperties($aProperties, 'list-style');
450 }
451
452 /**
453 * Combine border-color, border-style and border-width into border
454 * Should be run after create_dimensions_shorthand!
455 * */
456 public function createBorderShorthand() {
457 $aProperties = array(
458 'border-width', 'border-style', 'border-color'
459 );
460 $this->createShorthandProperties($aProperties, 'border');
461 }
462
463 /*
464 * Looks for long format CSS dimensional properties
465 * (margin, padding, border-color, border-style and border-width)
466 * and converts them into shorthand CSS properties.
467 * */
468
469 public function createDimensionsShorthand() {
470 $aPositions = array('top', 'right', 'bottom', 'left');
471 $aExpansions = array(
472 'margin' => 'margin-%s',
473 'padding' => 'padding-%s',
474 'border-color' => 'border-%s-color',
475 'border-style' => 'border-%s-style',
476 'border-width' => 'border-%s-width'
477 );
478 $aRules = $this->getRulesAssoc();
479 foreach ($aExpansions as $sProperty => $sExpanded) {
480 $aFoldable = array();
481 foreach ($aRules as $sRuleName => $oRule) {
482 foreach ($aPositions as $sPosition) {
483 if ($sRuleName == sprintf($sExpanded, $sPosition)) {
484 $aFoldable[$sRuleName] = $oRule;
485 }
486 }
487 }
488 // All four dimensions must be present
489 if (count($aFoldable) == 4) {
490 $aValues = array();
491 foreach ($aPositions as $sPosition) {
492 $oRule = $aRules[sprintf($sExpanded, $sPosition)];
493 $mRuleValue = $oRule->getValue();
494 $aRuleValues = array();
495 if (!$mRuleValue instanceof RuleValueList) {
496 $aRuleValues[] = $mRuleValue;
497 } else {
498 $aRuleValues = $mRuleValue->getListComponents();
499 }
500 $aValues[$sPosition] = $aRuleValues;
501 }
502 $oNewRule = new Rule($sProperty, $this->iLineNo);
503 if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) {
504 if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) {
505 if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) {
506 // All 4 sides are equal
507 $oNewRule->addValue($aValues['top']);
508 } else {
509 // Top and bottom are equal, left and right are equal
510 $oNewRule->addValue($aValues['top']);
511 $oNewRule->addValue($aValues['left']);
512 }
513 } else {
514 // Only left and right are equal
515 $oNewRule->addValue($aValues['top']);
516 $oNewRule->addValue($aValues['left']);
517 $oNewRule->addValue($aValues['bottom']);
518 }
519 } else {
520 // No sides are equal
521 $oNewRule->addValue($aValues['top']);
522 $oNewRule->addValue($aValues['left']);
523 $oNewRule->addValue($aValues['bottom']);
524 $oNewRule->addValue($aValues['right']);
525 }
526 $this->addRule($oNewRule);
527 foreach ($aPositions as $sPosition) {
528 $this->removeRule(sprintf($sExpanded, $sPosition));
529 }
530 }
531 }
532 }
533
534 /**
535 * Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
536 * tries to convert them into a shorthand CSS <tt>font</tt> property.
537 * At least font-size AND font-family must be present in order to create a shorthand declaration.
538 * */
539 public function createFontShorthand() {
540 $aFontProperties = array(
541 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'
542 );
543 $aRules = $this->getRulesAssoc();
544 if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
545 return;
546 }
547 $oNewRule = new Rule('font', $this->iLineNo);
548 foreach (array('font-style', 'font-variant', 'font-weight') as $sProperty) {
549 if (isset($aRules[$sProperty])) {
550 $oRule = $aRules[$sProperty];
551 $mRuleValue = $oRule->getValue();
552 $aValues = array();
553 if (!$mRuleValue instanceof RuleValueList) {
554 $aValues[] = $mRuleValue;
555 } else {
556 $aValues = $mRuleValue->getListComponents();
557 }
558 if ($aValues[0] !== 'normal') {
559 $oNewRule->addValue($aValues[0]);
560 }
561 }
562 }
563 // Get the font-size value
564 $oRule = $aRules['font-size'];
565 $mRuleValue = $oRule->getValue();
566 $aFSValues = array();
567 if (!$mRuleValue instanceof RuleValueList) {
568 $aFSValues[] = $mRuleValue;
569 } else {
570 $aFSValues = $mRuleValue->getListComponents();
571 }
572 // But wait to know if we have line-height to add it
573 if (isset($aRules['line-height'])) {
574 $oRule = $aRules['line-height'];
575 $mRuleValue = $oRule->getValue();
576 $aLHValues = array();
577 if (!$mRuleValue instanceof RuleValueList) {
578 $aLHValues[] = $mRuleValue;
579 } else {
580 $aLHValues = $mRuleValue->getListComponents();
581 }
582 if ($aLHValues[0] !== 'normal') {
583 $val = new RuleValueList('/', $this->iLineNo);
584 $val->addListComponent($aFSValues[0]);
585 $val->addListComponent($aLHValues[0]);
586 $oNewRule->addValue($val);
587 }
588 } else {
589 $oNewRule->addValue($aFSValues[0]);
590 }
591 $oRule = $aRules['font-family'];
592 $mRuleValue = $oRule->getValue();
593 $aFFValues = array();
594 if (!$mRuleValue instanceof RuleValueList) {
595 $aFFValues[] = $mRuleValue;
596 } else {
597 $aFFValues = $mRuleValue->getListComponents();
598 }
599 $oFFValue = new RuleValueList(',', $this->iLineNo);
600 $oFFValue->setListComponents($aFFValues);
601 $oNewRule->addValue($oFFValue);
602
603 $this->addRule($oNewRule);
604 foreach ($aFontProperties as $sProperty) {
605 $this->removeRule($sProperty);
606 }
607 }
608
609 public function __toString() {
610 return $this->render(new \Sabberworm\CSS\OutputFormat());
611 }
612
613 public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
614 if(count($this->aSelectors) === 0) {
615 // If all the selectors have been removed, this declaration block becomes invalid
616 throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
617 }
376eb156
MM
618 $sResult = $oOutputFormat->sBeforeDeclarationBlock;
619 $sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
620 $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
621 $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
fbe18cc0
FM
622 $sResult .= parent::render($oOutputFormat);
623 $sResult .= '}';
376eb156 624 $sResult .= $oOutputFormat->sAfterDeclarationBlock;
fbe18cc0
FM
625 return $sResult;
626 }
627
628}