From 905c344e83e718cfdd1025c484acc83afd00b3c1 Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Mon, 7 Apr 2025 18:06:21 -0400 Subject: [PATCH 1/3] fix(issue#4242): add support for layer at-rule * Add support for layer at-rule. * Add tests for layer at-rule. --- packages/less/src/less/parser/parser.js | 35 +++++++- packages/less/src/less/tree/atrule.js | 13 ++- packages/test-data/css/_main/layer.css | 66 +++++++++++++++ .../less/_main/import/layer-import.less | 5 ++ packages/test-data/less/_main/layer.less | 81 +++++++++++++++++++ 5 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 packages/test-data/css/_main/layer.css create mode 100644 packages/test-data/less/_main/import/layer-import.less create mode 100644 packages/test-data/less/_main/layer.less diff --git a/packages/less/src/less/parser/parser.js b/packages/less/src/less/parser/parser.js index 6af533ff3..53f8a5832 100644 --- a/packages/less/src/less/parser/parser.js +++ b/packages/less/src/less/parser/parser.js @@ -2065,6 +2065,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { let hasUnknown; let hasBlock = true; let isRooted = true; + let isKeywordList = false; if (parserInput.currentChar() !== '@') { return; } @@ -2105,6 +2106,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { case '@starting-style': isRooted = false; break; + case '@layer': + isRooted = false; + break; default: hasUnknown = true; break; @@ -2137,9 +2141,38 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { if (hasBlock) { rules = this.blockRuleset(); + + parserInput.save(); + + if (!rules && !isRooted) { + value = this.entity(); + rules = this.blockRuleset(); + } + + if (!rules && !isRooted) { + parserInput.restore(); + + let 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(); + } } - if (rules || (!hasBlock && value && parserInput.$char(';'))) { + if (rules || isKeywordList || (!hasBlock && value && parserInput.$char(';'))) { parserInput.forget(); return new(tree.AtRule)(name, value, rules, index + currentIndex, fileInfo, context.dumpLineNumbers ? getDebugInfo(index) : null, diff --git a/packages/less/src/less/tree/atrule.js b/packages/less/src/less/tree/atrule.js index 5cdac997d..df36a5d87 100644 --- a/packages/less/src/less/tree/atrule.js +++ b/packages/less/src/less/tree/atrule.js @@ -77,6 +77,14 @@ AtRule.prototype = Object.assign(new Node(), { } }, + keywordList(rules) { + if (!Array.isArray(rules)) { + return false; + } else { + return rules.filter(function (node) { return (node.type === 'Keyword' || node.type === 'Comment'); }).length === rules.length; + } + }, + accept(visitor) { const value = this.value, rules = this.rules, declarations = this.declarations; @@ -127,6 +135,9 @@ AtRule.prototype = Object.assign(new Node(), { if (value) { value = value.eval(context); + if (value.value && this.keywordList(value.value)) { + value = new Anonymous(value.value.map(keyword => keyword.value).join(', '), this.getIndex(), this.fileInfo()); + } } if (rules) { @@ -143,7 +154,7 @@ AtRule.prototype = Object.assign(new Node(), { } if (this.simpleBlock && rules) { rules[0].functionRegistry = context.frames[0].functionRegistry.inherit(); - rules= rules.map(function (rule) { return rule.eval(context); }); + rules = rules.map(function (rule) { return rule.eval(context); }); } // restore media bubbling information diff --git a/packages/test-data/css/_main/layer.css b/packages/test-data/css/_main/layer.css new file mode 100644 index 000000000..97235d750 --- /dev/null +++ b/packages/test-data/css/_main/layer.css @@ -0,0 +1,66 @@ +@layer { + .main::before { + color: #f00; + } +} +@layer legacy { + .sub-rule ul { + color: white; + } +} +@layer primevue { + .test { + foo: bar; + } +} +@layer reset, base, components, utilities; +@layer reset { + *, + *::before, + *::after { + box-sizing: border-box; + } +} +@layer base { + body { + margin: 0; + font-family: system-ui, sans-serif; + } + body header { + background-color: #f0f0f0; + padding: 1rem; + } +} +@layer components { + .button { + display: inline-block; + padding: 0.5rem 1rem; + background-color: blue; + color: white; + } + .button:hover { + background-color: darkblue; + } +} +@layer utilities { + .text-center { + text-align: center; + } + .responsive { + width: 100%; + } + @media (min-width: 768px) { + .responsive { + width: 50%; + } + } +} +.parent { + color: black; +} +.parent .child { + color: red; +} +.parent:hover { + background: lightgray; +} diff --git a/packages/test-data/less/_main/import/layer-import.less b/packages/test-data/less/_main/import/layer-import.less new file mode 100644 index 000000000..f30c9561f --- /dev/null +++ b/packages/test-data/less/_main/import/layer-import.less @@ -0,0 +1,5 @@ +.sub-rule { + ul { + color: white; + } +} diff --git a/packages/test-data/less/_main/layer.less b/packages/test-data/less/_main/layer.less new file mode 100644 index 000000000..00d0aae11 --- /dev/null +++ b/packages/test-data/less/_main/layer.less @@ -0,0 +1,81 @@ +.main { + @layer { + &::before { + color: #f00; + } + } +} + +@layer legacy { + @import "./import/layer-import.less"; +} + +@layer-name: primevue; + +@layer @layer-name { + .test { + foo: bar; + } +} + +@layer reset, base, components, utilities; + +@layer reset { + + *, + *::before, + *::after { + box-sizing: border-box; + } +} + +@layer base { + body { + margin: 0; + font-family: system-ui, sans-serif; + + header { + background-color: #f0f0f0; + padding: 1rem; + } + } +} + +@layer components { + .button { + display: inline-block; + padding: 0.5rem 1rem; + background-color: blue; + color: white; + + &:hover { + background-color: darkblue; + } + } +} + +@layer utilities { + .text-center { + text-align: center; + } + + .responsive { + width: 100%; + + @media (min-width: 768px) { + width: 50%; + } + } +} + +.parent { + color: black; + + .child { + color: red; + } + + &:hover { + background: lightgray; + } +} From 8468fa4bd0b5e45c9d4de2066cd62252b6804706 Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Tue, 15 Apr 2025 19:42:44 -0400 Subject: [PATCH 2/3] feat: add support for layer import syntax * Add support for layer import at-rule syntax. See: https://developer.mozilla.org/en-US/docs/Web/CSS/@import --- packages/less/src/less/tree/expression.js | 2 + packages/less/src/less/tree/import.js | 57 ++++++++++++++++++- packages/test-data/css/_main/layer.css | 4 ++ .../less/_main/import/layer-import-2.css | 5 ++ .../less/_main/import/layer-import-3.css | 0 .../less/_main/import/layer-import-4.css | 5 ++ .../less/_main/import/layer-import-5.css | 5 ++ packages/test-data/less/_main/layer.less | 5 ++ 8 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/test-data/less/_main/import/layer-import-2.css create mode 100644 packages/test-data/less/_main/import/layer-import-3.css create mode 100644 packages/test-data/less/_main/import/layer-import-4.css create mode 100644 packages/test-data/less/_main/import/layer-import-5.css diff --git a/packages/less/src/less/tree/expression.js b/packages/less/src/less/tree/expression.js index 03b40a1c8..c72f55b5b 100644 --- a/packages/less/src/less/tree/expression.js +++ b/packages/less/src/less/tree/expression.js @@ -20,6 +20,7 @@ Expression.prototype = Object.assign(new Node(), { }, eval(context) { + const noSpacing = this.noSpacing; let returnValue; const mathOn = context.isMathOn(); const inParenthesis = this.parens; @@ -50,6 +51,7 @@ Expression.prototype = Object.assign(new Node(), { && (!(returnValue instanceof Dimension))) { returnValue = new Paren(returnValue); } + returnValue.noSpacing = returnValue.noSpacing || noSpacing; return returnValue; }, diff --git a/packages/less/src/less/tree/import.js b/packages/less/src/less/tree/import.js index abafa3d9f..48af5559c 100644 --- a/packages/less/src/less/tree/import.js +++ b/packages/less/src/less/tree/import.js @@ -6,6 +6,7 @@ import Ruleset from './ruleset'; import Anonymous from './anonymous'; import * as utils from '../utils'; import LessError from '../less-error'; +import Expression from './expression'; // // CSS @import node @@ -157,6 +158,20 @@ Import.prototype = Object.assign(new Node(), { return []; } } + if (this.features) { + let featureValue = this.features.value; + if (Array.isArray(featureValue) && featureValue.length === 1) { + const expr = featureValue[0]; + if (expr.type === 'Expression' && Array.isArray(expr.value) && expr.value.length >= 2) { + featureValue = expr.value; + const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer' + && featureValue[1].type === 'Paren'; + if (isLayer) { + this.css = false; + } + } + } + } if (this.options.inline) { const contents = new Anonymous(this.root, 0, { @@ -165,18 +180,58 @@ Import.prototype = Object.assign(new Node(), { }, true, true); return this.features ? new Media([contents], this.features.value) : [contents]; - } else if (this.css) { + } else if (this.css || this.layerCss) { const newImport = new Import(this.evalPath(context), features, this.options, this._index); + if (this.layerCss) { + newImport.css = this.layerCss; + newImport.path._fileInfo = this._fileInfo; + } if (!newImport.css && this.error) { throw this.error; } return newImport; } else if (this.root) { + if (this.features) { + let featureValue = this.features.value; + if (Array.isArray(featureValue) && featureValue.length === 1) { + const expr = featureValue[0]; + if (expr.type === 'Expression' && Array.isArray(expr.value) && expr.value.length >= 2) { + featureValue = expr.value; + const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer' + && featureValue[1].type === 'Paren'; + if (isLayer) { + this.layerCss = true; + // expr.noSpacing = true; + featureValue[0] = new Expression(featureValue.slice(0, 2)); + featureValue.splice(1, 1); + featureValue[0].noSpacing = true; + return this; + } + } + } + } ruleset = new Ruleset(null, utils.copyArray(this.root.rules)); ruleset.evalImports(context); return this.features ? new Media(ruleset.rules, this.features.value) : ruleset.rules; } else { + if (this.features) { + let featureValue = this.features.value; + if (Array.isArray(featureValue) && featureValue.length >= 1) { + featureValue = featureValue[0].value; + if (Array.isArray(featureValue) && featureValue.length >= 2) { + const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer' + && featureValue[1].type === 'Paren'; + if (isLayer) { + this.css = true; + featureValue[0] = new Expression(featureValue.slice(0, 2)); + featureValue.splice(1, 1); + featureValue[0].noSpacing = true; + return this; + } + } + } + } return []; } } diff --git a/packages/test-data/css/_main/layer.css b/packages/test-data/css/_main/layer.css index 97235d750..1b342e996 100644 --- a/packages/test-data/css/_main/layer.css +++ b/packages/test-data/css/_main/layer.css @@ -1,3 +1,7 @@ +@import url("/import/layer-import-2.css") layer(foo); +@import url("/import/layer-import-3.css") layer(responsive) supports(display: flex) screen and (max-width: 768px); +@import url("/import/layer-import-4.css") layer(print) print; +@import url("/import/layer-import-5.css") layer(features) supports(display: grid); @layer { .main::before { color: #f00; diff --git a/packages/test-data/less/_main/import/layer-import-2.css b/packages/test-data/less/_main/import/layer-import-2.css new file mode 100644 index 000000000..f30c9561f --- /dev/null +++ b/packages/test-data/less/_main/import/layer-import-2.css @@ -0,0 +1,5 @@ +.sub-rule { + ul { + color: white; + } +} diff --git a/packages/test-data/less/_main/import/layer-import-3.css b/packages/test-data/less/_main/import/layer-import-3.css new file mode 100644 index 000000000..e69de29bb diff --git a/packages/test-data/less/_main/import/layer-import-4.css b/packages/test-data/less/_main/import/layer-import-4.css new file mode 100644 index 000000000..f30c9561f --- /dev/null +++ b/packages/test-data/less/_main/import/layer-import-4.css @@ -0,0 +1,5 @@ +.sub-rule { + ul { + color: white; + } +} diff --git a/packages/test-data/less/_main/import/layer-import-5.css b/packages/test-data/less/_main/import/layer-import-5.css new file mode 100644 index 000000000..f30c9561f --- /dev/null +++ b/packages/test-data/less/_main/import/layer-import-5.css @@ -0,0 +1,5 @@ +.sub-rule { + ul { + color: white; + } +} diff --git a/packages/test-data/less/_main/layer.less b/packages/test-data/less/_main/layer.less index 00d0aae11..7943da048 100644 --- a/packages/test-data/less/_main/layer.less +++ b/packages/test-data/less/_main/layer.less @@ -10,6 +10,11 @@ @import "./import/layer-import.less"; } +@import url("/import/layer-import-2.css") layer(foo); +@import url("/import/layer-import-3.css") layer(responsive) supports(display: flex) screen and (max-width: 768px); +@import url("/import/layer-import-4.css") layer(print) print; +@import url("/import/layer-import-5.css") layer(features) supports(display: grid); + @layer-name: primevue; @layer @layer-name { From 69e892d27bf35278726c45e468e2164215abaec8 Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Wed, 16 Apr 2025 17:02:45 -0400 Subject: [PATCH 3/3] chore: cleanup layer import solution * Cleanup layer import at-rule solution before merge. --- packages/less/src/less/tree/import.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/less/src/less/tree/import.js b/packages/less/src/less/tree/import.js index 48af5559c..8c377df87 100644 --- a/packages/less/src/less/tree/import.js +++ b/packages/less/src/less/tree/import.js @@ -201,7 +201,6 @@ Import.prototype = Object.assign(new Node(), { && featureValue[1].type === 'Paren'; if (isLayer) { this.layerCss = true; - // expr.noSpacing = true; featureValue[0] = new Expression(featureValue.slice(0, 2)); featureValue.splice(1, 1); featureValue[0].noSpacing = true;