From fac57db3a3261709d27a00ea53e1489c3b8c57b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20San=20Miguel=20Garc=C3=ADa?= Date: Tue, 3 Mar 2026 11:47:11 -0600 Subject: [PATCH] Less_Parser: Support comma-separated selector lists in pseudo-classes Pseudo-classes like :is(), :not(), :where(), and :has() accept comma-separated selector lists per CSS Selectors Level 4. The parser's parenthesized-selector fallback in parseElement() previously called parseSelector() which stops at commas, causing a parse error for inputs like :is(.a, .b). Add a comma loop in the paren fallback to collect multiple selectors, wrapping them in an Expression node inside the Paren node. This mirrors the upstream Less.js 4.2 fix (less.js#4290). Bug: https://github.com/wikimedia/less.php/issues/136 Co-Authored-By: Claude Opus 4.6 --- lib/Less/Parser.php | 25 ++++++++++++++++--- .../less.php/css/pseudo-selector-list.css | 21 ++++++++++++++++ .../less.php/less/pseudo-selector-list.less | 22 ++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 test/Fixtures/less.php/css/pseudo-selector-list.css create mode 100644 test/Fixtures/less.php/less/pseudo-selector-list.less diff --git a/lib/Less/Parser.php b/lib/Less/Parser.php index c4273f67..ba066d04 100644 --- a/lib/Less/Parser.php +++ b/lib/Less/Parser.php @@ -2187,9 +2187,28 @@ private function parseElement() { $this->save(); if ( $this->matchChar( '(' ) ) { $v = $this->parseSelector(); - if ( $v && $this->matchChar( ')' ) ) { - $e = new Less_Tree_Paren( $v ); - $this->forget(); + if ( $v ) { + // Support comma-separated selector lists inside parentheses, + // for pseudo-classes like :is(), :not(), :where(), :has(). + // Backported from Less.js 4.2 (less.js#4290). + $selectors = []; + while ( $this->matchChar( ',' ) ) { + $selectors[] = $v; + $selectors[] = new Less_Tree_Anonymous( ',' ); + $v = $this->parseSelector(); + } + + if ( $v && $this->matchChar( ')' ) ) { + if ( $selectors ) { + $selectors[] = $v; + $e = new Less_Tree_Paren( new Less_Tree_Expression( $selectors, true ) ); + } else { + $e = new Less_Tree_Paren( $v ); + } + $this->forget(); + } else { + $this->restore(); + } } else { $this->restore(); } diff --git a/test/Fixtures/less.php/css/pseudo-selector-list.css b/test/Fixtures/less.php/css/pseudo-selector-list.css new file mode 100644 index 00000000..081c4de4 --- /dev/null +++ b/test/Fixtures/less.php/css/pseudo-selector-list.css @@ -0,0 +1,21 @@ +:not(.active) { + display: none; +} +:not(.a, .b) { + display: block; +} +.form-small:is(.framework-input, .framework-search, select:not([multiple]):not([size])) { + height: 42px; +} +a:is(.b, :is(.c), :has(div)) { + color: red; +} +:where(.x, .y) { + margin: 0; +} +:has(div > .child, span + .sibling) { + border: 1px solid; +} +:is([type="text"], .input-field) { + padding: 4px; +} diff --git a/test/Fixtures/less.php/less/pseudo-selector-list.less b/test/Fixtures/less.php/less/pseudo-selector-list.less new file mode 100644 index 00000000..7393bc3c --- /dev/null +++ b/test/Fixtures/less.php/less/pseudo-selector-list.less @@ -0,0 +1,22 @@ +// https://github.com/wikimedia/less.php/issues/136 +:not(.active) { + display: none; +} +:not(.a, .b) { + display: block; +} +.form-small:is(.framework-input, .framework-search, select:not([multiple]):not([size])) { + height: 42px; +} +a:is(.b, :is(.c), :has(div)) { + color: red; +} +:where(.x, .y) { + margin: 0; +} +:has(div > .child, span + .sibling) { + border: 1px solid; +} +:is([type="text"], .input-field) { + padding: 4px; +}