@@ -95,7 +95,19 @@ public function setSelectors($mSelector, $oList = null)
9595 if (is_array ($ mSelector )) {
9696 $ this ->aSelectors = $ mSelector ;
9797 } 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+ }
99111 }
100112 foreach ($ this ->aSelectors as $ iKey => $ mSelector ) {
101113 if (!($ mSelector instanceof Selector)) {
@@ -122,6 +134,56 @@ public function setSelectors($mSelector, $oList = null)
122134 }
123135 }
124136
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+
125187 /**
126188 * Remove one of the selectors of the block.
127189 *
0 commit comments