Skip to content

Commit c00f54d

Browse files
committed
Add support for a wider variety of at rules
Unknown at-rules stil get to be `RuleSet`s but block rules like document/supports/region-style/font-feature-values are now supported via the AtRuleBlockList class. Also, all at rules now implement the Sabberworm\CSS\Property\AtRule interface. MediaQuery has been removed as its functionality is included fully in the AtRuleBlockList class. Closes MyIntervals#53 as fixed
1 parent 518269d commit c00f54d

File tree

12 files changed

+184
-60
lines changed

12 files changed

+184
-60
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The resulting data structure consists mainly of five basic types: `CSSList`, `Ru
5151

5252
`RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist:
5353

54-
* `AtRule` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face.
54+
* `AtRuleSet` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face.
5555
* `DeclarationBlock` – a RuleSet constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements.
5656

5757
Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`) while a `RuleSet` can only contain `Rule`s.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\CSSList;
4+
5+
use Sabberworm\CSS\Property\AtRule;
6+
7+
/**
8+
* A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRule objects.
9+
*/
10+
class AtRuleBlockList extends CSSBlockList implements AtRule {
11+
12+
private $sType;
13+
private $sArgs;
14+
15+
public function __construct($sType, $sArgs = '') {
16+
parent::__construct();
17+
$this->sType = $sType;
18+
$this->sArgs = $sArgs;
19+
}
20+
21+
public function atRuleName() {
22+
return $this->sType;
23+
}
24+
25+
public function atRuleArgs() {
26+
return $this->sArgs;
27+
}
28+
29+
public function __toString() {
30+
$sResult = "@{$this->sType} {$this->sArgs}{";
31+
$sResult .= parent::__toString();
32+
$sResult .= '}';
33+
return $sResult;
34+
}
35+
36+
}

lib/Sabberworm/CSS/CSSList/KeyFrame.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
namespace Sabberworm\CSS\CSSList;
44

5+
use Sabberworm\CSS\Property\AtRule;
56

6-
class KeyFrame extends CSSList {
7+
class KeyFrame extends CSSList implements AtRule {
78

89
private $vendorKeyFrame;
910
private $animationName;
@@ -37,4 +38,11 @@ public function __toString() {
3738
return $sResult;
3839
}
3940

41+
public function atRuleName() {
42+
return $this->vendorKeyFrame;
43+
}
44+
45+
public function atRuleArgs() {
46+
return $this->animationName;
47+
}
4048
}

lib/Sabberworm/CSS/CSSList/MediaQuery.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

lib/Sabberworm/CSS/Parser.php

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
use Sabberworm\CSS\CSSList\CSSList;
66
use Sabberworm\CSS\CSSList\Document;
7-
use Sabberworm\CSS\CSSList\MediaQuery;
87
use Sabberworm\CSS\CSSList\KeyFrame;
8+
use Sabberworm\CSS\Property\AtRule;
99
use Sabberworm\CSS\Property\Import;
1010
use Sabberworm\CSS\Property\Charset;
1111
use Sabberworm\CSS\Property\CSSNamespace;
12-
use Sabberworm\CSS\RuleSet\AtRule;
12+
use Sabberworm\CSS\RuleSet\AtRuleSet;
13+
use Sabberworm\CSS\CSSList\AtRuleBlockList;
1314
use Sabberworm\CSS\RuleSet\DeclarationBlock;
1415
use Sabberworm\CSS\Value\CSSFunction;
1516
use Sabberworm\CSS\Value\RuleValueList;
@@ -86,14 +87,7 @@ private function parseAtRule() {
8687
$this->consume('@');
8788
$sIdentifier = $this->parseIdentifier();
8889
$this->consumeWhiteSpace();
89-
if ($sIdentifier === 'media') {
90-
$oResult = new MediaQuery();
91-
$oResult->setQuery(trim($this->consumeUntil('{')));
92-
$this->consume('{');
93-
$this->consumeWhiteSpace();
94-
$this->parseList($oResult);
95-
return $oResult;
96-
} else if ($sIdentifier === 'import') {
90+
if ($sIdentifier === 'import') {
9791
$oLocation = $this->parseURLValue();
9892
$this->consumeWhiteSpace();
9993
$sMediaQuery = null;
@@ -108,7 +102,7 @@ private function parseAtRule() {
108102
$this->consume(';');
109103
$this->setCharset($sCharset->getString());
110104
return new Charset($sCharset);
111-
} else if (preg_match('/^(-\\w+-)?keyframes$/', $sIdentifier) === 1) {
105+
} else if (self::identifierIs($sIdentifier, 'keyframes')) {
112106
$oResult = new KeyFrame();
113107
$oResult->setVendorKeyFrame($sIdentifier);
114108
$oResult->setAnimationName(trim($this->consumeUntil('{')));
@@ -133,10 +127,23 @@ private function parseAtRule() {
133127
return new CSSNamespace($mUrl, $sPrefix);
134128
} else {
135129
//Unknown other at rule (font-face or such)
130+
$sArgs = $this->consumeUntil('{');
136131
$this->consume('{');
137132
$this->consumeWhiteSpace();
138-
$oAtRule = new AtRule($sIdentifier);
139-
$this->parseRuleSet($oAtRule);
133+
$bUseRuleSet = true;
134+
foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
135+
if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
136+
$bUseRuleSet = false;
137+
break;
138+
}
139+
}
140+
if($bUseRuleSet) {
141+
$oAtRule = new AtRuleSet($sIdentifier, $sArgs);
142+
$this->parseRuleSet($oAtRule);
143+
} else {
144+
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs);
145+
$this->parseList($oAtRule);
146+
}
140147
return $oAtRule;
141148
}
142149
}
@@ -449,6 +456,13 @@ private function parseURLValue() {
449456
}
450457
return $oResult;
451458
}
459+
460+
/**
461+
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
462+
*/
463+
private static function identifierIs($sIdentifier, $sMatch) {
464+
return preg_match("/^(-\\w+-)?$sMatch$/", $sIdentifier) === 1;
465+
}
452466

453467
private function comes($sString, $iOffset = 0) {
454468
if ($this->isEnd()) {
@@ -561,4 +575,3 @@ private function strlen($text) {
561575
}
562576

563577
}
564-
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\Property;
4+
5+
interface AtRule {
6+
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
7+
// Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule.
8+
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; //…and more font-specific ones (to be used inside font-feature-values)
9+
10+
public function atRuleName();
11+
public function atRuleArgs();
12+
public function __toString();
13+
}

lib/Sabberworm/CSS/Property/CSSNamespace.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* CSSNamespace represents an @namespace rule.
77
*/
8-
class CSSNamespace {
8+
class CSSNamespace implements AtRule {
99
private $mUrl;
1010
private $sPrefix;
1111

@@ -34,4 +34,15 @@ public function setPrefix($sPrefix) {
3434
$this->sPrefix = $sPrefix;
3535
}
3636

37+
public function atRuleName() {
38+
return 'namespace';
39+
}
40+
41+
public function atRuleArgs() {
42+
$aResult = array($this->mUrl);
43+
if($this->sPrefix) {
44+
array_unshift($aResult, $this->sPrefix);
45+
}
46+
return $aResult;
47+
}
3748
}

lib/Sabberworm/CSS/Property/Charset.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* • May only appear at the very top of a Document’s contents.
1010
* • Must not appear more than once.
1111
*/
12-
class Charset {
12+
class Charset implements AtRule {
1313

1414
private $sCharset;
1515

@@ -29,4 +29,11 @@ public function __toString() {
2929
return "@charset {$this->sCharset->__toString()};";
3030
}
3131

32+
public function atRuleName() {
33+
return 'charset';
34+
}
35+
36+
public function atRuleArgs() {
37+
return $this->sCharset;
38+
}
3239
}

lib/Sabberworm/CSS/Property/Import.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* Class representing an @import rule.
99
*/
10-
class Import {
10+
class Import implements AtRule {
1111
private $oLocation;
1212
private $sMediaQuery;
1313

@@ -27,4 +27,16 @@ public function getLocation() {
2727
public function __toString() {
2828
return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';';
2929
}
30+
31+
public function atRuleName() {
32+
return 'import';
33+
}
34+
35+
public function atRuleArgs() {
36+
$aResult = array($this->oLocation);
37+
if($this->sMediaQuery) {
38+
array_push($aResult, $this->sMediaQuery);
39+
}
40+
return $aResult;
41+
}
3042
}

lib/Sabberworm/CSS/RuleSet/AtRule.php renamed to lib/Sabberworm/CSS/RuleSet/AtRuleSet.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,32 @@
22

33
namespace Sabberworm\CSS\RuleSet;
44

5+
use Sabberworm\CSS\Property\AtRule;
6+
57
/**
68
* A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRule objects.
79
*/
8-
class AtRule extends RuleSet {
10+
class AtRuleSet extends RuleSet implements AtRule {
911

1012
private $sType;
13+
private $sArgs;
1114

12-
public function __construct($sType) {
15+
public function __construct($sType, $sArgs = '') {
1316
parent::__construct();
1417
$this->sType = $sType;
18+
$this->sArgs = $sArgs;
1519
}
1620

17-
public function getType() {
21+
public function atRuleName() {
1822
return $this->sType;
1923
}
2024

25+
public function atRuleArgs() {
26+
return $this->sArgs;
27+
}
28+
2129
public function __toString() {
22-
$sResult = "@{$this->sType} {";
30+
$sResult = "@{$this->sType} {$this->sArgs}{";
2331
$sResult .= parent::__toString();
2432
$sResult .= '}';
2533
return $sResult;

tests/Sabberworm/CSS/ParserTest.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44

55
use Sabberworm\CSS\Value\Size;
66
use Sabberworm\CSS\Property\Selector;
7-
use Sabberworm\CSS\RuleSet\AtRule;
8-
use Sabberworm\CSS\CSSList\KeyFrame;
7+
use Sabberworm\CSS\Property\AtRule;
98

109
class ParserTest extends \PHPUnit_Framework_TestCase {
1110

1211
function testFiles() {
13-
12+
1413
$sDirectory = dirname(__FILE__) . '/../../files';
1514
if ($rHandle = opendir($sDirectory)) {
1615
/* This is the correct way to loop over the directory. */
@@ -155,6 +154,12 @@ function testManipulation() {
155154
to {top: 200px;}
156155
}@-moz-keyframes some-move {from {top: 0px;}
157156
to {top: 200px;}
157+
}@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {body {font-family: "Helvetica";}
158+
}@page :pseudo-class {margin: 2in;}@-moz-document url(http://www.w3.org/),
159+
url-prefix(http://www.w3.org/Style/),
160+
domain(mozilla.org),
161+
regexp("https:.*") {body {color: purple;background: yellow;}
162+
}@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}@region-style #intro {p {color: blue;}
158163
}', $oDoc->__toString());
159164
foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
160165
foreach ($oBlock->getSelectors() as $oSelector) {
@@ -167,6 +172,12 @@ function testManipulation() {
167172
to {top: 200px;}
168173
}@-moz-keyframes some-move {from {top: 0px;}
169174
to {top: 200px;}
175+
}@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {#my_id body {font-family: "Helvetica";}
176+
}@page :pseudo-class {margin: 2in;}@-moz-document url(http://www.w3.org/),
177+
url-prefix(http://www.w3.org/Style/),
178+
domain(mozilla.org),
179+
regexp("https:.*") {#my_id body {color: purple;background: yellow;}
180+
}@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}@region-style #intro {#my_id p {color: blue;}
170181
}', $oDoc->__toString());
171182

172183
$oDoc = $this->parsedStructureForFile('values');
@@ -300,12 +311,12 @@ function testPrefixedGradient() {
300311
function testListValueRemoval() {
301312
$oDoc = $this->parsedStructureForFile('atrules');
302313
foreach ($oDoc->getContents() as $oItem) {
303-
if ($oItem instanceof AtRule || $oItem instanceof KeyFrame) {
314+
if ($oItem instanceof AtRule) {
304315
$oDoc->remove($oItem);
305316
continue;
306317
}
307318
}
308-
$this->assertSame('@charset "utf-8";html, body {font-size: 1.6em;}' . "\n", $oDoc->__toString());
319+
$this->assertSame('html, body {font-size: 1.6em;}' . "\n", $oDoc->__toString());
309320

310321
$oDoc = $this->parsedStructureForFile('nested');
311322
foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {

0 commit comments

Comments
 (0)