Repository: less/less.js (js)

Issue:
Dotted layer name in `@layer` rule throws an error in Less 4.4.0 (regression)
**To reproduce:**

👉️ [Playground link](https://lesscss.org/less-preview/#eyJjb2RlIjoiQGxheWVyIGZvby5iYXoge1xuICAgLmJhciB7XG4gICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gICB9XG59IiwiYWN0aXZlVmVyc2lvbiI6IjQuNC4wIiwibWF0aCI6InBhcmVucy1kaXZpc2lvbiIsInN0cmljdFVuaXRzIjpmYWxzZX0=) 👈️

1. Write a CSS `@layer` rule in a `.less` stylesheet addressing a nested layer using dotted notation:

     ```less
    @layer foo.baz {
        .bar {
            font-weight: bold;
        }
    }
    ```
  
2. Process it:

    ```
    npx less stylesheet.less
    ```

3. Bang:

    > `Error: [less] at-rule options not recognised`


**Current behavior:**

`lessc` throws an error on the dotted syntax.

**Expected behavior:**

The dotted syntax should pass through `lessc` the same way as a non-dotted rule.

**Environment information:**

<!-- Provide the `less` and `nodejs` packages versions -->

- `less` version: 4.4.0
- `nodejs` version: 22.17.0 
- `operating system`: Linux


<!-- feel free to add additional comments -->

### Other links

[MDN Link about nested CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer#nesting_layers)

[Playground example of a non-dotted layer name working great in 4.4.0](https://lesscss.org/less-preview/#eyJjb2RlIjoiQGxheWVyIGZvbyB7XG4gICAuYmFyIHtcbiAgICAgICBmb250LXdlaWdodDogYm9sZDtcbiAgIH1cbn0iLCJhY3RpdmVWZXJzaW9uIjoiNC40LjAiLCJtYXRoIjoicGFyZW5zLWRpdmlzaW9uIiwic3RyaWN0VW5pdHMiOmZhbHNlfQ==)

[Playground example of the dotted notation working in 4.3.0](https://lesscss.org/less-preview/#eyJjb2RlIjoiQGxheWVyIGZvby5iYXoge1xuICAgLmJhciB7XG4gICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gICB9XG59IiwiYWN0aXZlVmVyc2lvbiI6IjQuMy4wIiwibWF0aCI6InBhcmVucy1kaXZpc2lvbiIsInN0cmljdFVuaXRzIjpmYWxzZX0=)



Patch:
diff --git a/packages/less/src/less/parser/parser.js b/packages/less/src/less/parser/parser.js
index 3fe7a37f5..f1bc2d32a 100644
--- a/packages/less/src/less/parser/parser.js
+++ b/packages/less/src/less/parser/parser.js
@@ -2048,7 +2048,49 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
                     return null;
                 }
             },
-
+            atruleUnknown: function (value, name, hasBlock) {
+                value = this.permissiveValue(/^[{;]/);
+                hasBlock = (parserInput.currentChar() === '{');
+                if (!value) {
+                    if (!hasBlock && parserInput.currentChar() !== ';') {
+                        error(''.concat(name, ' rule is missing block or ending semi-colon'));
+                    }
+                }
+                else if (!value.value) {
+                    value = null;
+                }
+                return [value, hasBlock];
+            },
+            atruleBlock: function (rules, value, isRooted, isKeywordList) {
+                rules = this.blockRuleset();
+                parserInput.save();
+                if (!rules && !isRooted) {
+                    value = this.entity();
+                    rules = this.blockRuleset();
+                }
+                if (!rules && !isRooted) {
+                    parserInput.restore();
+                    var e = [];
+                    value = this.entity();
+                    while (parserInput.$char(',')) {
+                        e.push(value);
+                        value = this.entity();
+                    }
+                    if (value && e.length > 0) {
+                        e.push(value);
+                        value = e;
+                        isKeywordList = true;
+                    }
+                    else {
+                        rules = this.blockRuleset();
+                    }
+                }
+                else {
+                    parserInput.forget();
+                }
+                    
+                return [rules, value, isKeywordList];
+            },
             //
             // A CSS AtRule
             //
@@ -2127,48 +2169,29 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
                         error(`expected ${name} expression`);
                     }
                 } else if (hasUnknown) {
-                    value = this.permissiveValue(/^[{;]/);
-                    hasBlock = (parserInput.currentChar() === '{');
-                    if (!value) {
-                        if (!hasBlock && parserInput.currentChar() !== ';') {
-                            error(`${name} rule is missing block or ending semi-colon`);
-                        }
-                    }
-                    else if (!value.value) {
-                        value = null;
-                    }
+                    const unknownPackage = this.atruleUnknown(value, name, hasBlock);
+                    value = unknownPackage[0];
+                    hasBlock = unknownPackage[1];
                 }
-
+                    
                 if (hasBlock) {
-                    rules = this.blockRuleset();
-
-                    parserInput.save();
-
-                    if (!rules && !isRooted) {
-                        value = this.entity();
-                        rules = this.blockRuleset();
-                    }
+                    let blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
+                    rules = blockPackage[0];
+                    value = blockPackage[1];
+                    isKeywordList = blockPackage[2];
 
-                    if (!rules && !isRooted) {
+                    if (!rules && !hasUnknown) {
                         parserInput.restore();
-
-                        let e = [];
-                        value = this.entity();
-
-                        while (parserInput.$char(',')) {
-                            e.push(value);
-                            value = this.entity();
+                        name = parserInput.$re(/^@[a-z-]+/);
+                        const unknownPackage = this.atruleUnknown(value, name, hasBlock);
+                        value = unknownPackage[0];
+                        hasBlock = unknownPackage[1];
+                        if (hasBlock) {
+                            blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
+                            rules = blockPackage[0];
+                            value = blockPackage[1];
+                            isKeywordList = blockPackage[2];
                         }
-
-                        if (value && e.length > 0) {
-                            e.push(value);
-                            value = e;
-                            isKeywordList = true;
-                        } else {
-                            rules = this.blockRuleset();
-                        }
-                    } else {
-                        parserInput.forget();
                     }
                 }
 


Test patch:
diff --git a/packages/test-data/css/_main/layer.css b/packages/test-data/css/_main/layer.css
index 43218ad0a..7196325af 100644
--- a/packages/test-data/css/_main/layer.css
+++ b/packages/test-data/css/_main/layer.css
@@ -69,3 +69,25 @@
 .parent:hover {
   background: lightgray;
 }
+@layer foo.baz {
+  .bar {
+    font-weight: bold;
+  }
+}
+@layer framework {
+  @layer layout {
+    .container {
+      display: grid;
+      gap: 2rem;
+    }
+  }
+}
+@layer framework.layout {
+  main {
+    padding: 2rem;
+  }
+  p {
+    margin-block: 1rem;
+    color: #555;
+  }
+}
diff --git a/packages/test-data/less/_main/layer.less b/packages/test-data/less/_main/layer.less
index 8e93ec6b9..9340f83af 100644
--- a/packages/test-data/less/_main/layer.less
+++ b/packages/test-data/less/_main/layer.less
@@ -85,3 +85,28 @@
         background: lightgray;
     }
 }
+
+@layer foo.baz {
+   .bar {
+       font-weight: bold;
+   }
+}
+
+@layer framework {
+    @layer layout {
+        .container {
+            display: grid;
+            gap: 2rem;
+        }
+    }
+}
+
+@layer framework.layout {
+    main {
+        padding: 2rem;
+    }
+    p {
+        margin-block: 1rem;
+        color: #555;
+    }
+}