Skip to content

Commit fa139f6

Browse files
committed
Fix parsing CSS selectors which contain commas
1 parent 8be357d commit fa139f6

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,19 @@ public function setSelectors($mSelector) {
2828
if (is_array($mSelector)) {
2929
$this->aSelectors = $mSelector;
3030
} else {
31-
$this->aSelectors = explode(',', $mSelector);
31+
list( $sSelectors, $aPlaceholders ) = $this->addSelectorExpressionPlaceholders( $mSelector );
32+
if ( empty( $aPlaceholders ) ) {
33+
$this->aSelectors = explode(',', $sSelectors);
34+
} else {
35+
$aSearches = array_keys( $aPlaceholders );
36+
$aReplaces = array_values( $aPlaceholders );
37+
$this->aSelectors = array_map(
38+
function( $sSelector ) use ( $aSearches, $aReplaces ) {
39+
return str_replace( $aSearches, $aReplaces, $sSelector );
40+
},
41+
explode(',', $sSelectors)
42+
);
43+
}
3244
}
3345
foreach ($this->aSelectors as $iKey => $mSelector) {
3446
if (!($mSelector instanceof Selector)) {
@@ -37,6 +49,52 @@ public function setSelectors($mSelector) {
3749
}
3850
}
3951

52+
/**
53+
* Add placeholders for parenthetical/bracketed expressions in selectors which may contain commas that break exploding.
54+
*
55+
* This prevents a single selector like `.widget:not(.foo, .bar)` from erroneously getting parsed in setSelectors as
56+
* two selectors `.widget:not(.foo` and `.bar)`.
57+
*
58+
* @param string $sSelectors Selectors.
59+
* @return array First array value is the selectors with placeholders, and second value is the array of placeholders mapped to the original expressions.
60+
*/
61+
private function addSelectorExpressionPlaceholders( $sSelectors ) {
62+
$iOffset = 0;
63+
$aPlaceholders = array();
64+
65+
while ( preg_match( '/\(|\[/', $sSelectors, $aMatches, PREG_OFFSET_CAPTURE, $iOffset ) ) {
66+
$sMatchString = $aMatches[0][0];
67+
$iMatchOffset = $aMatches[0][1];
68+
$iStyleLength = strlen( $sSelectors );
69+
$iOpenParens = 1;
70+
$iStartOffset = $iMatchOffset + strlen( $sMatchString );
71+
$iFinalOffset = $iStartOffset;
72+
for ( ; $iFinalOffset < $iStyleLength; $iFinalOffset++ ) {
73+
if ( '(' === $sSelectors[ $iFinalOffset ] || '[' === $sSelectors[ $iFinalOffset ] ) {
74+
$iOpenParens++;
75+
} elseif ( ')' === $sSelectors[ $iFinalOffset ] || ']' === $sSelectors[ $iFinalOffset ] ) {
76+
$iOpenParens--;
77+
}
78+
79+
// Found the end of the expression, so replace it with a placeholder.
80+
if ( 0 === $iOpenParens ) {
81+
$sMatchedExpr = substr( $sSelectors, $iMatchOffset, $iFinalOffset - $iMatchOffset + 1 );
82+
$sPlaceholder = sprintf( '{placeholder:%d}', count( $aPlaceholders ) + 1 );
83+
$aPlaceholders[ $sPlaceholder ] = $sMatchedExpr;
84+
85+
// Update the CSS to replace the matched calc() with the placeholder function.
86+
$sSelectors = substr( $sSelectors, 0, $iMatchOffset ) . $sPlaceholder . substr( $sSelectors, $iFinalOffset + 1 );
87+
// Update offset based on difference of length of placeholder vs original matched calc().
88+
$iFinalOffset += strlen( $sPlaceholder ) - strlen( $sMatchedExpr );
89+
break;
90+
}
91+
}
92+
// Start matching at the next byte after the match.
93+
$iOffset = $iFinalOffset + 1;
94+
}
95+
return array( $sSelectors, $aPlaceholders );
96+
}
97+
4098
// remove one of the selector of the block
4199
public function removeSelector($mSelector) {
42100
if($mSelector instanceof Selector) {

tests/Sabberworm/CSS/ParserTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ function testSpecificity() {
148148
case "li.green":
149149
$this->assertSame(11, $oSelector->getSpecificity());
150150
break;
151+
case "div:not(.foo[title=\"a,b\"], .bar)":
152+
$this->assertSame(31, $oSelector->getSpecificity());
153+
break;
154+
case "div[title=\"a,b\"]":
155+
$this->assertSame(11, $oSelector->getSpecificity());
156+
break;
151157
default:
152158
$this->fail("specificity: untested selector " . $oSelector->getSelector());
153159
}

tests/files/specificity.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#file,
33
.help:hover,
44
li.green,
5-
ol li::before {
5+
ol li::before,
6+
div:not(.foo[title="a,b"], .bar),
7+
div[title="a,b"] {
68
font-family: Helvetica;
79
}

0 commit comments

Comments
 (0)