From fe7ab14eb8fe17c9fb3cea1cab4c9e380726ee75 Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Sat, 7 Dec 2024 12:11:06 -0500 Subject: [PATCH 1/3] fix(issue:4224, issue:3777) CSS custom property * Fixes CSS custom property handling to address issue #4224 and issue #3777. * Added new CSS custom property tests. --- .../less/src/less/functions/math-helper.js | 9 ++++ packages/less/src/less/parser/parser.js | 16 ++++++- .../less/src/less/tree/custom-property.js | 18 +++++++ packages/less/src/less/tree/operation.js | 47 ++++++++++++++++++- packages/less/src/less/tree/ruleset.js | 2 +- .../test-data/css/_main/custom-property.css | 16 +++++++ .../test-data/less/_main/custom-property.less | 19 ++++++++ 7 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 packages/less/src/less/tree/custom-property.js create mode 100644 packages/test-data/css/_main/custom-property.css create mode 100644 packages/test-data/less/_main/custom-property.less diff --git a/packages/less/src/less/functions/math-helper.js b/packages/less/src/less/functions/math-helper.js index b557875c5..4cca18668 100644 --- a/packages/less/src/less/functions/math-helper.js +++ b/packages/less/src/less/functions/math-helper.js @@ -1,6 +1,15 @@ +import Call from '../tree/call'; +import CustomProperty from '../tree/custom-property'; import Dimension from '../tree/dimension'; const MathHelper = (fn, unit, n) => { + if (n instanceof Call && n.name === 'var') { + if (n.args && n.args.length === 1) { + return new Call(fn.name, [new CustomProperty(n.args[0].toCSS(), n._index, n._fileInfo)], n._index, n._fileInfo); + } else { + throw { type: 'Argument', message: 'var must contain one expression' }; + } + } if (!(n instanceof Dimension)) { throw { type: 'Argument', message: 'argument must be a number' }; } diff --git a/packages/less/src/less/parser/parser.js b/packages/less/src/less/parser/parser.js index 59aa2eef4..8b2ad6e42 100644 --- a/packages/less/src/less/parser/parser.js +++ b/packages/less/src/less/parser/parser.js @@ -794,6 +794,18 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*:/))) { return name[1]; } }, + // + // The custom property part of a variable definition. + // + // --fink: + // + customProperty: function () { + var name; + if (parserInput.currentChar() === '-' && (name = parserInput.$re(/^(--[\w-]+)\s*:/))) { + return name[1]; + } + }, + // // Call a variable value to retrieve a detached ruleset // or a value from a detached ruleset's rules. @@ -1558,7 +1570,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { parserInput.save(); - name = this.variable() || this.ruleProperty(); + name = this.variable() || this.customProperty() || this.ruleProperty(); if (name) { isVariable = typeof name === 'string'; @@ -1577,7 +1589,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { merge = !isVariable && name.length > 1 && name.pop().value; // Custom property values get permissive parsing - if (name[0].value && name[0].value.slice(0, 2) === '--') { + if (isVariable && name.startsWith('--')) { value = this.permissiveValue(/[;}]/); } // Try to store values as anonymous diff --git a/packages/less/src/less/tree/custom-property.js b/packages/less/src/less/tree/custom-property.js new file mode 100644 index 000000000..3eaa284b9 --- /dev/null +++ b/packages/less/src/less/tree/custom-property.js @@ -0,0 +1,18 @@ +/* eslint-disable no-prototype-builtins */ +import Node from './node'; + +const CustomProperty = function (name, index, currentFileInfo) { + this.name = name; + this._index = index; + this._fileInfo = currentFileInfo; +}; + +CustomProperty.prototype = Object.assign(new Node(), { + type: 'CustomProperty', + + genCSS: function (context, output) { + output.add('var(' + this.name + ')'); + } +}); + +export default CustomProperty; diff --git a/packages/less/src/less/tree/operation.js b/packages/less/src/less/tree/operation.js index 2805326be..982d571d5 100644 --- a/packages/less/src/less/tree/operation.js +++ b/packages/less/src/less/tree/operation.js @@ -2,6 +2,7 @@ import Node from './node'; import Color from './color'; import Dimension from './dimension'; import * as Constants from '../constants'; +import Call from './call'; const MATH = Constants.Math; @@ -19,7 +20,15 @@ Operation.prototype = Object.assign(new Node(), { }, eval(context) { - let a = this.operands[0].eval(context), b = this.operands[1].eval(context), op; + var a = this.evalVariable(context, this.operands[0]) + if (!a) { + a = this.operands[0].eval(context) + } + var b = this.evalVariable(context, this.operands[1]); + if (!b) { + b = this.operands[1].eval(context); + } + var op; if (context.isMathOn(this.op)) { op = this.op === './' ? '/' : this.op; @@ -56,7 +65,41 @@ Operation.prototype = Object.assign(new Node(), { output.add(' '); } this.operands[1].genCSS(context, output); - } + }, + + find: function (obj, fun) { + for (var i_2 = 0, r = void 0; i_2 < obj.length; i_2++) { + r = fun.call(obj, obj[i_2]); + if (r) { + return r; + } + } + return null; + }, + + evalVariable: function (context, operand) { + if (operand.name === 'var' && operand.args.length === 1) { + var varName = operand.args[0].toCSS(); + var variable = this.find(context.frames, function (frame) { + var v = frame.variable(varName); + if (v) { + if (v.important) { + var importantScope = context.importantScope[context.importantScope.length - 1]; + importantScope.important = v.important; + } + // If in calc, wrap vars in a function call to cascade evaluate args first + if (context.inCalc) { + return (new Call('_SELF', [v.value])).eval(context); + } + else { + return v.value.eval(context); + } + } + }); + + return variable; + } + }, }); export default Operation; diff --git a/packages/less/src/less/tree/ruleset.js b/packages/less/src/less/tree/ruleset.js index a3324cf07..878ac3f80 100644 --- a/packages/less/src/less/tree/ruleset.js +++ b/packages/less/src/less/tree/ruleset.js @@ -286,7 +286,7 @@ Ruleset.prototype = Object.assign(new Node(), { variables() { if (!this._variables) { this._variables = !this.rules ? {} : this.rules.reduce(function (hash, r) { - if (r instanceof Declaration && r.variable === true) { + if (r instanceof Declaration && (r.variable === true || (typeof r.name ==='string' && r.name.startsWith('--')))) { hash[r.name] = r; } // when evaluating variables in an import statement, imports have not been eval'd diff --git a/packages/test-data/css/_main/custom-property.css b/packages/test-data/css/_main/custom-property.css new file mode 100644 index 000000000..243947443 --- /dev/null +++ b/packages/test-data/css/_main/custom-property.css @@ -0,0 +1,16 @@ +.test { + --basic-deg: 20deg; + --basic-deg-tan: tan(var(--basic-deg)); +} +.test2 { + --some-var: 5%; + prop: min(95%, 10px); +} +.test3 { + --some-var: 55%; + prop: min(60%, 15px); +} +.test4 { + color: red; + --other-color: green; +} diff --git a/packages/test-data/less/_main/custom-property.less b/packages/test-data/less/_main/custom-property.less new file mode 100644 index 000000000..960267dcd --- /dev/null +++ b/packages/test-data/less/_main/custom-property.less @@ -0,0 +1,19 @@ +.test { + --basic-deg: 20deg; + --basic-deg-tan: tan(var(--basic-deg)); +} + +.test2 { + --some-var: 5%; + prop: min(100% - var(--some-var), 10px); +} + +.test3 { + --some-var: 55%; + prop: min(var(--some-var) + 5%, 15px); +} + +.test4 { + color: red; + --other-color: green; +} From 5ed7bfc61467db48e5b83a7605dee4907a72a4ba Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Sat, 7 Dec 2024 13:19:52 -0500 Subject: [PATCH 2/3] fix(issue:3777) improve custom property initial * Improves CSS custom property initial value support, including nesting scenarios to better address issue #3777. --- .../less/src/less/functions/math-helper.js | 4 +-- packages/less/src/less/functions/number.js | 5 ++++ packages/less/src/less/tree/call.js | 26 ++++++++++++++++++- .../less/src/less/tree/custom-property.js | 5 ++-- packages/less/src/less/tree/operation.js | 12 +++++++-- .../test-data/css/_main/custom-property.css | 12 +++++++-- .../test-data/less/_main/custom-property.less | 11 ++++++++ 7 files changed, 66 insertions(+), 9 deletions(-) diff --git a/packages/less/src/less/functions/math-helper.js b/packages/less/src/less/functions/math-helper.js index 4cca18668..2b5c73d7e 100644 --- a/packages/less/src/less/functions/math-helper.js +++ b/packages/less/src/less/functions/math-helper.js @@ -4,8 +4,8 @@ import Dimension from '../tree/dimension'; const MathHelper = (fn, unit, n) => { if (n instanceof Call && n.name === 'var') { - if (n.args && n.args.length === 1) { - return new Call(fn.name, [new CustomProperty(n.args[0].toCSS(), n._index, n._fileInfo)], n._index, n._fileInfo); + if (n.args && n.args.length >= 1) { + return new Call(fn.name, [new CustomProperty(n.args[0].toCSS(), n.args[1] ? n.args[1].toCSS() : null, n._index, n._fileInfo)], n._index, n._fileInfo); } else { throw { type: 'Argument', message: 'var must contain one expression' }; } diff --git a/packages/less/src/less/functions/number.js b/packages/less/src/less/functions/number.js index ccb97afef..5b5cac91e 100644 --- a/packages/less/src/less/functions/number.js +++ b/packages/less/src/less/functions/number.js @@ -1,6 +1,7 @@ import Dimension from '../tree/dimension'; import Anonymous from '../tree/anonymous'; import mathHelper from './math-helper.js'; +import Call from '../tree/call'; const minMax = function (isMin, args) { args = Array.prototype.slice.call(args); @@ -78,6 +79,10 @@ export default { return new Dimension(a.value % b.value, a.unit); }, pow: function(x, y) { + if (x instanceof Call || y instanceof Call) { + // Must return Node + return new Anonymous(x.toCSS() + ', ' + y.toCSS()); + } if (typeof x === 'number' && typeof y === 'number') { x = new Dimension(x); y = new Dimension(y); diff --git a/packages/less/src/less/tree/call.js b/packages/less/src/less/tree/call.js index 15e98eb80..1bdfd1791 100644 --- a/packages/less/src/less/tree/call.js +++ b/packages/less/src/less/tree/call.js @@ -1,6 +1,7 @@ import Node from './node'; import Anonymous from './anonymous'; import FunctionCaller from '../functions/function-caller'; +import CustomProperty from './custom-property'; // // A function call node. @@ -98,11 +99,34 @@ Call.prototype = Object.assign(new Node(), { genCSS(context, output) { output.add(`${this.name}(`, this.fileInfo(), this.getIndex()); + let isCustomProperty = false; + let customExpressionCount = 0; for (let i = 0; i < this.args.length; i++) { this.args[i].genCSS(context, output); + + if (this.args[i] instanceof CustomProperty + || ((i + 2 < this.args.length && this.args[i + 2] instanceof CustomProperty))) { + if (isCustomProperty) { + isCustomProperty = false; + } else { + isCustomProperty = true; + customExpressionCount = 1; + } + } + + if (customExpressionCount === 3) { + isCustomProperty = false; + customExpressionCount = 0; + } + if (i + 1 < this.args.length) { - output.add(', '); + if (!isCustomProperty) { + output.add(', '); + } else { + output.add(' '); + customExpressionCount++; + } } } diff --git a/packages/less/src/less/tree/custom-property.js b/packages/less/src/less/tree/custom-property.js index 3eaa284b9..993cf5ade 100644 --- a/packages/less/src/less/tree/custom-property.js +++ b/packages/less/src/less/tree/custom-property.js @@ -1,8 +1,9 @@ /* eslint-disable no-prototype-builtins */ import Node from './node'; -const CustomProperty = function (name, index, currentFileInfo) { +const CustomProperty = function (name, initialValue, index, currentFileInfo) { this.name = name; + this.initialValue = initialValue; this._index = index; this._fileInfo = currentFileInfo; }; @@ -11,7 +12,7 @@ CustomProperty.prototype = Object.assign(new Node(), { type: 'CustomProperty', genCSS: function (context, output) { - output.add('var(' + this.name + ')'); + output.add('var(' + this.name + (this.initialValue ? ', ' + this.initialValue : '') + ')'); } }); diff --git a/packages/less/src/less/tree/operation.js b/packages/less/src/less/tree/operation.js index 982d571d5..241da7ff7 100644 --- a/packages/less/src/less/tree/operation.js +++ b/packages/less/src/less/tree/operation.js @@ -3,6 +3,8 @@ import Color from './color'; import Dimension from './dimension'; import * as Constants from '../constants'; import Call from './call'; +import CustomProperty from './custom-property'; +import Anonymous from './anonymous'; const MATH = Constants.Math; @@ -38,6 +40,12 @@ Operation.prototype = Object.assign(new Node(), { if (b instanceof Dimension && a instanceof Color) { b = b.toColor(); } + if (a instanceof Dimension && b instanceof CustomProperty) { + return [a, new Anonymous(op), b]; + } + if (b instanceof Dimension && a instanceof CustomProperty) { + return [a, new Anonymous(op), b]; + } if (!a.operate || !b.operate) { if ( (a instanceof Operation || b instanceof Operation) @@ -78,7 +86,7 @@ Operation.prototype = Object.assign(new Node(), { }, evalVariable: function (context, operand) { - if (operand.name === 'var' && operand.args.length === 1) { + if (operand.name === 'var' && operand.args.length >= 1) { var varName = operand.args[0].toCSS(); var variable = this.find(context.frames, function (frame) { var v = frame.variable(varName); @@ -92,7 +100,7 @@ Operation.prototype = Object.assign(new Node(), { return (new Call('_SELF', [v.value])).eval(context); } else { - return v.value.eval(context); + return new CustomProperty(v.name, operand.args[1] ? operand.args[1].toCSS() : null, 0, {}); } } }); diff --git a/packages/test-data/css/_main/custom-property.css b/packages/test-data/css/_main/custom-property.css index 243947443..ad430c143 100644 --- a/packages/test-data/css/_main/custom-property.css +++ b/packages/test-data/css/_main/custom-property.css @@ -4,13 +4,21 @@ } .test2 { --some-var: 5%; - prop: min(95%, 10px); + prop: min(100% - var(--some-var), 10px); } .test3 { --some-var: 55%; - prop: min(60%, 15px); + prop: min(var(--some-var) + 5%, 15px); } .test4 { color: red; --other-color: green; } +.test5 { + --font-level: 5; + prop: var(--font-ratio-min), var(--font-level, 0); +} +.test6 { + --some-var: 5px; + prop: min(100% - var(--some-var, var(--somevar, 0)), 10px); +} diff --git a/packages/test-data/less/_main/custom-property.less b/packages/test-data/less/_main/custom-property.less index 960267dcd..b50bfcb58 100644 --- a/packages/test-data/less/_main/custom-property.less +++ b/packages/test-data/less/_main/custom-property.less @@ -17,3 +17,14 @@ color: red; --other-color: green; } + +.test5 { + --font-level: 5; + prop: pow(var(--font-ratio-min), var(--font-level, 0)) +} + + +.test6 { + --some-var: 5px; + prop: min(100% - var(--some-var, var(--somevar, 0)), 10px); +} From 0b04bcd928ee83f3e7c36a3b40473b38d0cee967 Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Sat, 7 Dec 2024 13:35:35 -0500 Subject: [PATCH 3/3] chore: add more CSS custom property tests * Add more CSS custom property tests. --- packages/test-data/css/_main/custom-property.css | 5 +++++ packages/test-data/less/_main/custom-property.less | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/test-data/css/_main/custom-property.css b/packages/test-data/css/_main/custom-property.css index ad430c143..b02be5a16 100644 --- a/packages/test-data/css/_main/custom-property.css +++ b/packages/test-data/css/_main/custom-property.css @@ -22,3 +22,8 @@ --some-var: 5px; prop: min(100% - var(--some-var, var(--somevar, 0)), 10px); } +.test7 { + --some-var: 6px; + --some-var-2: 7px; + prop: min(100% - var(--some-var, var(--somevar, var(--some-var-2, 8px))), 10px); +} diff --git a/packages/test-data/less/_main/custom-property.less b/packages/test-data/less/_main/custom-property.less index b50bfcb58..b2bcffc68 100644 --- a/packages/test-data/less/_main/custom-property.less +++ b/packages/test-data/less/_main/custom-property.less @@ -23,8 +23,13 @@ prop: pow(var(--font-ratio-min), var(--font-level, 0)) } - .test6 { --some-var: 5px; prop: min(100% - var(--some-var, var(--somevar, 0)), 10px); } + +.test7 { + --some-var: 6px; + --some-var-2: 7px; + prop: min(100% - var(--some-var, var(--somevar, var(--some-var-2, 8px))), 10px); +}