3 namespace Sabberworm\CSS\RuleSet;
5 use Sabberworm\CSS\Parsing\ParserState;
6 use Sabberworm\CSS\Parsing\OutputException;
7 use Sabberworm\CSS\Property\Selector;
8 use Sabberworm\CSS\Rule\Rule;
9 use Sabberworm\CSS\Value\RuleValueList;
10 use Sabberworm\CSS\Value\Value;
11 use Sabberworm\CSS\Value\Size;
12 use Sabberworm\CSS\Value\Color;
13 use Sabberworm\CSS\Value\URL;
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).
19 class DeclarationBlock extends RuleSet {
23 public function __construct($iLineNo = 0) {
24 parent::__construct($iLineNo);
25 $this->aSelectors = array();
28 public static function parse(ParserState $oParserState) {
30 $oResult = new DeclarationBlock($oParserState->currentLine());
31 $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments));
32 $oResult->setComments($aComments);
33 RuleSet::parseRuleSet($oParserState, $oResult);
38 public function setSelectors($mSelector) {
39 if (is_array($mSelector)) {
40 $this->aSelectors = $mSelector;
42 $this->aSelectors = explode(',', $mSelector);
44 foreach ($this->aSelectors as $iKey => $mSelector) {
45 if (!($mSelector instanceof Selector)) {
46 $this->aSelectors[$iKey] = new Selector($mSelector);
51 // remove one of the selector of the block
52 public function removeSelector($mSelector) {
53 if($mSelector instanceof Selector) {
54 $mSelector = $mSelector->getSelector();
56 foreach($this->aSelectors as $iKey => $oSelector) {
57 if($oSelector->getSelector() === $mSelector) {
58 unset($this->aSelectors[$iKey]);
66 * @deprecated use getSelectors()
68 public function getSelector() {
69 return $this->getSelectors();
73 * @deprecated use setSelectors()
75 public function setSelector($mSelector) {
76 $this->setSelectors($mSelector);
82 * @return Selector[] Selectors.
84 public function getSelectors() {
85 return $this->aSelectors;
89 * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
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();
101 * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
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();
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
117 public function expandBorderShorthand() {
118 $aBorderRules = array(
119 'border', 'border-left', 'border-right', 'border-top', 'border-bottom'
121 $aBorderSizes = array(
122 'thin', 'medium', 'thick'
124 $aRules = $this->getRulesAssoc();
125 foreach ($aBorderRules as $sBorderRule) {
126 if (!isset($aRules[$sBorderRule]))
128 $oRule = $aRules[$sBorderRule];
129 $mRuleValue = $oRule->getValue();
131 if (!$mRuleValue instanceof RuleValueList) {
132 $aValues[] = $mRuleValue;
134 $aValues = $mRuleValue->getListComponents();
136 foreach ($aValues as $mValue) {
137 if ($mValue instanceof Value) {
138 $mNewValue = clone $mValue;
140 $mNewValue = $mValue;
142 if ($mValue instanceof Size) {
143 $sNewRuleName = $sBorderRule . "-width";
144 } else if ($mValue instanceof Color) {
145 $sNewRuleName = $sBorderRule . "-color";
147 if (in_array($mValue, $aBorderSizes)) {
148 $sNewRuleName = $sBorderRule . "-width";
149 } else/* if(in_array($mValue, $aBorderStyles)) */ {
150 $sNewRuleName = $sBorderRule . "-style";
153 $oNewRule = new Rule($sNewRuleName, $this->iLineNo);
154 $oNewRule->setIsImportant($oRule->getIsImportant());
155 $oNewRule->addValue(array($mNewValue));
156 $this->addRule($oNewRule);
158 $this->removeRule($sBorderRule);
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.
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'
175 $aRules = $this->getRulesAssoc();
176 foreach ($aExpansions as $sProperty => $sExpanded) {
177 if (!isset($aRules[$sProperty]))
179 $oRule = $aRules[$sProperty];
180 $mRuleValue = $oRule->getValue();
182 if (!$mRuleValue instanceof RuleValueList) {
183 $aValues[] = $mRuleValue;
185 $aValues = $mRuleValue->getListComponents();
187 $top = $right = $bottom = $left = null;
188 switch (count($aValues)) {
190 $top = $right = $bottom = $left = $aValues[0];
193 $top = $bottom = $aValues[0];
194 $left = $right = $aValues[1];
198 $left = $right = $aValues[1];
199 $bottom = $aValues[2];
203 $right = $aValues[1];
204 $bottom = $aValues[2];
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);
214 $this->removeRule($sProperty);
219 * Convert shorthand font declarations
220 * (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
221 * into their constituent parts.
223 public function expandFontShorthand() {
224 $aRules = $this->getRulesAssoc();
225 if (!isset($aRules['font']))
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'
236 $mRuleValue = $oRule->getValue();
238 if (!$mRuleValue instanceof RuleValueList) {
239 $aValues[] = $mRuleValue;
241 $aValues = $mRuleValue->getListComponents();
243 foreach ($aValues as $mValue) {
244 if (!$mValue instanceof Value) {
245 $mValue = mb_strtolower($mValue);
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;
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;
258 in_array($mValue, array('bold', 'bolder', 'lighter'))
259 || ($mValue instanceof Size
260 && in_array($mValue->getSize(), range(100, 900, 100)))
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;
270 $aFontProperties['font-family'] = $mValue;
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);
279 $this->removeRule('font');
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
289 public function expandBackgroundShorthand() {
290 $aRules = $this->getRulesAssoc();
291 if (!isset($aRules['background']))
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))
299 $mRuleValue = $oRule->getValue();
301 if (!$mRuleValue instanceof RuleValueList) {
302 $aValues[] = $mRuleValue;
304 $aValues = $mRuleValue->getListComponents();
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);
313 $this->removeRule('background');
317 foreach ($aValues as $mValue) {
318 if (!$mValue instanceof Value) {
319 $mValue = mb_strtolower($mValue);
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
332 if ($iNumBgPos == 0) {
333 $aBgProperties['background-position'][0] = $mValue;
334 $aBgProperties['background-position'][1] = 'center';
336 $aBgProperties['background-position'][$iNumBgPos] = $mValue;
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);
347 $this->removeRule('background');
350 public function expandListStyleShorthand() {
351 $aListProperties = array(
352 'list-style-type' => 'disc',
353 'list-style-position' => 'outside',
354 'list-style-image' => 'none'
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'
362 $aListStylePositions = array(
365 $aRules = $this->getRulesAssoc();
366 if (!isset($aRules['list-style']))
368 $oRule = $aRules['list-style'];
369 $mRuleValue = $oRule->getValue();
371 if (!$mRuleValue instanceof RuleValueList) {
372 $aValues[] = $mRuleValue;
374 $aValues = $mRuleValue->getListComponents();
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);
383 $this->removeRule('list-style');
386 foreach ($aValues as $mValue) {
387 if (!$mValue instanceof Value) {
388 $mValue = mb_strtolower($mValue);
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;
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);
404 $this->removeRule('list-style');
407 public function createShorthandProperties(array $aProperties, $sShorthand) {
408 $aRules = $this->getRulesAssoc();
409 $aNewValues = array();
410 foreach ($aProperties as $sProperty) {
411 if (!isset($aRules[$sProperty]))
413 $oRule = $aRules[$sProperty];
414 if (!$oRule->getIsImportant()) {
415 $mRuleValue = $oRule->getValue();
417 if (!$mRuleValue instanceof RuleValueList) {
418 $aValues[] = $mRuleValue;
420 $aValues = $mRuleValue->getListComponents();
422 foreach ($aValues as $mValue) {
423 $aNewValues[] = $mValue;
425 $this->removeRule($sProperty);
428 if (count($aNewValues)) {
429 $oNewRule = new Rule($sShorthand, $this->iLineNo);
430 foreach ($aNewValues as $mValue) {
431 $oNewRule->addValue($mValue);
433 $this->addRule($oNewRule);
437 public function createBackgroundShorthand() {
438 $aProperties = array(
439 'background-color', 'background-image', 'background-repeat',
440 'background-position', 'background-attachment'
442 $this->createShorthandProperties($aProperties, 'background');
445 public function createListStyleShorthand() {
446 $aProperties = array(
447 'list-style-type', 'list-style-position', 'list-style-image'
449 $this->createShorthandProperties($aProperties, 'list-style');
453 * Combine border-color, border-style and border-width into border
454 * Should be run after create_dimensions_shorthand!
456 public function createBorderShorthand() {
457 $aProperties = array(
458 'border-width', 'border-style', 'border-color'
460 $this->createShorthandProperties($aProperties, 'border');
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.
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'
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;
488 // All four dimensions must be present
489 if (count($aFoldable) == 4) {
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;
498 $aRuleValues = $mRuleValue->getListComponents();
500 $aValues[$sPosition] = $aRuleValues;
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']);
509 // Top and bottom are equal, left and right are equal
510 $oNewRule->addValue($aValues['top']);
511 $oNewRule->addValue($aValues['left']);
514 // Only left and right are equal
515 $oNewRule->addValue($aValues['top']);
516 $oNewRule->addValue($aValues['left']);
517 $oNewRule->addValue($aValues['bottom']);
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']);
526 $this->addRule($oNewRule);
527 foreach ($aPositions as $sPosition) {
528 $this->removeRule(sprintf($sExpanded, $sPosition));
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.
539 public function createFontShorthand() {
540 $aFontProperties = array(
541 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'
543 $aRules = $this->getRulesAssoc();
544 if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
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();
553 if (!$mRuleValue instanceof RuleValueList) {
554 $aValues[] = $mRuleValue;
556 $aValues = $mRuleValue->getListComponents();
558 if ($aValues[0] !== 'normal') {
559 $oNewRule->addValue($aValues[0]);
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;
570 $aFSValues = $mRuleValue->getListComponents();
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;
580 $aLHValues = $mRuleValue->getListComponents();
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);
589 $oNewRule->addValue($aFSValues[0]);
591 $oRule = $aRules['font-family'];
592 $mRuleValue = $oRule->getValue();
593 $aFFValues = array();
594 if (!$mRuleValue instanceof RuleValueList) {
595 $aFFValues[] = $mRuleValue;
597 $aFFValues = $mRuleValue->getListComponents();
599 $oFFValue = new RuleValueList(',', $this->iLineNo);
600 $oFFValue->setListComponents($aFFValues);
601 $oNewRule->addValue($oFFValue);
603 $this->addRule($oNewRule);
604 foreach ($aFontProperties as $sProperty) {
605 $this->removeRule($sProperty);
609 public function __toString() {
610 return $this->render(new \Sabberworm\CSS\OutputFormat());
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);
618 $sResult = $oOutputFormat->sBeforeDeclarationBlock;
619 $sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
620 $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
621 $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
622 $sResult .= parent::render($oOutputFormat);
624 $sResult .= $oOutputFormat->sAfterDeclarationBlock;