@@ -95,7 +95,19 @@ public function setSelectors($mSelector, $oList = null)
95
95
if (is_array ($ mSelector )) {
96
96
$ this ->aSelectors = $ mSelector ;
97
97
} else {
98
- $ this ->aSelectors = explode (', ' , $ mSelector );
98
+ list ($ sSelectors , $ aPlaceholders ) = $ this ->addSelectorExpressionPlaceholders ($ mSelector );
99
+ if (empty ($ aPlaceholders )) {
100
+ $ this ->aSelectors = explode (', ' , $ sSelectors );
101
+ } else {
102
+ $ aSearches = array_keys ($ aPlaceholders );
103
+ $ aReplaces = array_values ($ aPlaceholders );
104
+ $ this ->aSelectors = array_map (
105
+ static function ($ sSelector ) use ($ aSearches , $ aReplaces ) {
106
+ return str_replace ($ aSearches , $ aReplaces , $ sSelector );
107
+ },
108
+ explode (', ' , $ sSelectors )
109
+ );
110
+ }
99
111
}
100
112
foreach ($ this ->aSelectors as $ iKey => $ mSelector ) {
101
113
if (!($ mSelector instanceof Selector)) {
@@ -122,6 +134,56 @@ public function setSelectors($mSelector, $oList = null)
122
134
}
123
135
}
124
136
137
+ /**
138
+ * Add placeholders for parenthetical expressions in selectors which may contain commas that break exploding.
139
+ *
140
+ * This prevents a single selector like `.widget:not(.foo, .bar)` from erroneously getting parsed in setSelectors as
141
+ * two selectors `.widget:not(.foo` and `.bar)`.
142
+ *
143
+ * @param string $sSelectors Selectors.
144
+ * @return array First array value is the selectors with placeholders, and second value is the array of placeholders
145
+ * mapped to the original expressions.
146
+ */
147
+ private function addSelectorExpressionPlaceholders ($ sSelectors )
148
+ {
149
+ $ iOffset = 0 ;
150
+ $ aPlaceholders = [];
151
+
152
+ while (preg_match ('/\(|\[/ ' , $ sSelectors , $ aMatches , PREG_OFFSET_CAPTURE , $ iOffset )) {
153
+ $ sMatchString = $ aMatches [0 ][0 ];
154
+ $ iMatchOffset = $ aMatches [0 ][1 ];
155
+ $ iStyleLength = strlen ($ sSelectors );
156
+ $ iOpenParens = 1 ;
157
+ $ iStartOffset = $ iMatchOffset + strlen ($ sMatchString );
158
+ $ iFinalOffset = $ iStartOffset ;
159
+ for (; $ iFinalOffset < $ iStyleLength ; $ iFinalOffset ++) {
160
+ if ('( ' === $ sSelectors [ $ iFinalOffset ] || '[ ' === $ sSelectors [ $ iFinalOffset ]) {
161
+ $ iOpenParens ++;
162
+ } elseif (') ' === $ sSelectors [ $ iFinalOffset ] || '] ' === $ sSelectors [ $ iFinalOffset ]) {
163
+ $ iOpenParens --;
164
+ }
165
+
166
+ // Found the end of the expression, so replace it with a placeholder.
167
+ if (0 === $ iOpenParens ) {
168
+ $ sMatchedExpr = substr ($ sSelectors , $ iMatchOffset , $ iFinalOffset - $ iMatchOffset + 1 );
169
+ $ sPlaceholder = sprintf ('{placeholder:%d} ' , count ($ aPlaceholders ) + 1 );
170
+ $ aPlaceholders [ $ sPlaceholder ] = $ sMatchedExpr ;
171
+
172
+ // Update the CSS to replace the matched calc() with the placeholder function.
173
+ $ sSelectors = substr ($ sSelectors , 0 , $ iMatchOffset )
174
+ . $ sPlaceholder
175
+ . substr ($ sSelectors , $ iFinalOffset + 1 );
176
+ // Update offset based on difference of length of placeholder vs original matched calc().
177
+ $ iFinalOffset += strlen ($ sPlaceholder ) - strlen ($ sMatchedExpr );
178
+ break ;
179
+ }
180
+ }
181
+ // Start matching at the next byte after the match.
182
+ $ iOffset = $ iFinalOffset + 1 ;
183
+ }
184
+ return [ $ sSelectors , $ aPlaceholders ];
185
+ }
186
+
125
187
/**
126
188
* Remove one of the selectors of the block.
127
189
*
0 commit comments