Repository: jsdom/cssstyle (js)

Issue:
allow var functions in style.color values
Hi,

I've seen that when I try to insert a value to style.color a function called parseColor not allow to use values with var function like var(--customVar).

One solution is add another regEx in parser.valueType to return a color type but maybe there is a more transversal solution wich is being implementig to support this kind of values in all properties.

Thank you for your help



Pull request description:
Support nested var() and nested calc()
Fixes #125, closes #127

Based on #127. Many thanks.


Patch:
diff --git a/lib/CSSStyleDeclaration.js b/lib/CSSStyleDeclaration.js
index 0a180f6..d7a7566 100644
--- a/lib/CSSStyleDeclaration.js
+++ b/lib/CSSStyleDeclaration.js
@@ -53,7 +53,9 @@ CSSStyleDeclaration.prototype = {
       this.removeProperty(name);
       return;
     }
-    var isCustomProperty = name.indexOf('--') === 0;
+    var isCustomProperty =
+      name.indexOf('--') === 0 ||
+      (typeof value === 'string' && /^var\(--[-\w]+,?.*\)$/.test(value));
     if (isCustomProperty) {
       this._setProperty(name, value, priority);
       return;
diff --git a/lib/parsers.js b/lib/parsers.js
index 3478658..d5ae9c3 100644
--- a/lib/parsers.js
+++ b/lib/parsers.js
@@ -4,7 +4,7 @@
  ********************************************************************/
 'use strict';
 
-const { isColor, resolve } = require('@asamuzakjp/css-color');
+const { cssCalc, isColor, resolve } = require('@asamuzakjp/css-color');
 
 exports.TYPES = {
   INTEGER: 1,
@@ -18,17 +18,24 @@ exports.TYPES = {
   KEYWORD: 9,
   NULL_OR_EMPTY_STR: 10,
   CALC: 11,
+  VAR: 12,
 };
 
-// rough regular expressions
-var integerRegEx = /^[-+]?[0-9]+$/;
-var numberRegEx = /^[-+]?[0-9]*\.?[0-9]+$/;
-var lengthRegEx = /^(0|[-+]?[0-9]*\.?[0-9]+(in|cm|em|mm|pt|pc|px|ex|rem|vh|vw|ch))$/;
-var percentRegEx = /^[-+]?[0-9]*\.?[0-9]+%$/;
+// regular expressions
+var DIGIT = '(?:0|[1-9]\\d*)';
+var NUMBER = `[+-]?(?:${DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${DIGIT})?`;
+var integerRegEx = new RegExp(`^[+-]?${DIGIT}$`);
+var numberRegEx = new RegExp(`^${NUMBER}$`);
+var lengthRegEx = new RegExp(
+  `^${NUMBER}(?:[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic))$`
+);
+var percentRegEx = new RegExp(`^${NUMBER}%$`);
+var angleRegEx = new RegExp(`^${NUMBER}(?:deg|g?rad|turn)$`);
 var urlRegEx = /^url\(\s*([^)]*)\s*\)$/;
 var stringRegEx = /^("[^"]*"|'[^']*')$/;
-var calcRegEx = /^calc\(([^)]*)\)$/;
-var angleRegEx = /^([-+]?[0-9]*\.?[0-9]+)(deg|grad|rad)$/;
+var varRegEx = /^var\(|(?<=[*/\s(])var\(/;
+var calcRegEx =
+  /^(?:a?(?:cos|sin|tan)|abs|atan2|calc|clamp|exp|hypot|log|max|min|mod|pow|rem|round|sign|sqrt)\(/;
 
 // This will return one of the above types based on the passed in string
 exports.valueType = function valueType(val) {
@@ -38,11 +45,9 @@ exports.valueType = function valueType(val) {
   if (typeof val === 'number') {
     val = val.toString();
   }
-
   if (typeof val !== 'string') {
     return undefined;
   }
-
   if (integerRegEx.test(val)) {
     return exports.TYPES.INTEGER;
   }
@@ -58,6 +63,9 @@ exports.valueType = function valueType(val) {
   if (urlRegEx.test(val)) {
     return exports.TYPES.URL;
   }
+  if (varRegEx.test(val)) {
+    return exports.TYPES.VAR;
+  }
   if (calcRegEx.test(val)) {
     return exports.TYPES.CALC;
   }
@@ -160,9 +168,14 @@ exports.parsePercent = function parsePercent(val) {
 // either a length or a percent
 exports.parseMeasurement = function parseMeasurement(val) {
   var type = exports.valueType(val);
-  if (type === exports.TYPES.CALC) {
+  if (type === exports.TYPES.VAR) {
     return val;
   }
+  if (type === exports.TYPES.CALC) {
+    return cssCalc(val, {
+      format: 'specifiedValue',
+    });
+  }
 
   var length = exports.parseLength(val);
   if (length !== undefined) {
diff --git a/package-lock.json b/package-lock.json
index 454e249..3e5cc20 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
       "version": "4.1.0",
       "license": "MIT",
       "dependencies": {
-        "@asamuzakjp/css-color": "^2.5.0",
+        "@asamuzakjp/css-color": "^2.8.2",
         "rrweb-cssom": "^0.7.1"
       },
       "devDependencies": {
@@ -30,12 +30,14 @@
       }
     },
     "node_modules/@asamuzakjp/css-color": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.5.0.tgz",
-      "integrity": "sha512-LuUSDuz8MPTAPOb9Zvd6b/D2nUyG7NZaI8OGZhu45xgQTqLQOC29HLfadPMrs5C7JY784WRtLMvLmQhTpIY0Pw==",
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.2.tgz",
+      "integrity": "sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==",
       "license": "MIT",
       "dependencies": {
-        "@csstools/css-calc": "^2.1.0",
+        "@csstools/css-calc": "^2.1.1",
+        "@csstools/css-color-parser": "^3.0.7",
+        "@csstools/css-parser-algorithms": "^3.0.4",
         "@csstools/css-tokenizer": "^3.0.3",
         "lru-cache": "^11.0.2"
       }
@@ -181,10 +183,29 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@csstools/color-helpers": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz",
+      "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/@csstools/css-calc": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.0.tgz",
-      "integrity": "sha512-X69PmFOrjTZfN5ijxtI8hZ9kRADFSLrmmQ6hgDJ272Il049WGKpDY64KhrFm/7rbWve0z81QepawzjkKlqkNGw==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz",
+      "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==",
       "funding": [
         {
           "type": "github",
@@ -204,6 +225,33 @@
         "@csstools/css-tokenizer": "^3.0.3"
       }
     },
+    "node_modules/@csstools/css-color-parser": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz",
+      "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/color-helpers": "^5.0.1",
+        "@csstools/css-calc": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^3.0.4",
+        "@csstools/css-tokenizer": "^3.0.3"
+      }
+    },
     "node_modules/@csstools/css-parser-algorithms": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
@@ -219,7 +267,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=18"
       },
diff --git a/package.json b/package.json
index 0f8e70c..b1517e3 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
   ],
   "main": "./lib/CSSStyleDeclaration.js",
   "dependencies": {
-    "@asamuzakjp/css-color": "^2.5.0",
+    "@asamuzakjp/css-color": "^2.8.2",
     "rrweb-cssom": "^0.7.1"
   },
   "devDependencies": {


Test patch:
diff --git a/test/CSSStyleDeclaration.js b/test/CSSStyleDeclaration.js
index 62f35d0..0f48f6d 100644
--- a/test/CSSStyleDeclaration.js
+++ b/test/CSSStyleDeclaration.js
@@ -748,4 +748,85 @@ describe('CSSStyleDeclaration', () => {
       assert.strictEqual(style.getPropertyValue(property), 'calc(100% - 100px)');
     });
   }
+
+  it('supports nested calc', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'calc(100% - calc(200px - 100px))');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(100% - 100px)');
+  });
+
+  it('supports nested calc', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'calc(100% * calc(2 / 3))');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(66.6667%)');
+  });
+
+  it('supports var', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'var(--foo)');
+    assert.strictEqual(style.getPropertyValue('width'), 'var(--foo)');
+  });
+
+  it('supports var with fallback', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'var(--foo, 100px)');
+    assert.strictEqual(style.getPropertyValue('width'), 'var(--foo, 100px)');
+  });
+
+  it('supports var with var fallback', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'var(--foo, var(--bar))');
+    assert.strictEqual(style.getPropertyValue('width'), 'var(--foo, var(--bar))');
+  });
+
+  it('supports calc with var inside', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'calc(100% - var(--foo))');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(100% - var(--foo))');
+  });
+
+  it('supports var with calc inside', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'var(--foo, calc(var(--bar) + 3px))');
+    assert.strictEqual(style.getPropertyValue('width'), 'var(--foo, calc(var(--bar) + 3px))');
+  });
+
+  it('supports color var', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('color', 'var(--foo)');
+    assert.strictEqual(style.getPropertyValue('color'), 'var(--foo)');
+  });
+
+  it('should not normalize if var() is included', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'calc( /* comment */ 100% - calc(var(--foo) *2 ))');
+    assert.strictEqual(
+      style.getPropertyValue('width'),
+      'calc( /* comment */ 100% - calc(var(--foo) *2 ))'
+    );
+  });
+
+  it('supports abs', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'abs(1 + 2 + 3)');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(6)');
+  });
+
+  it('supports abs inside calc', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'calc(abs(1) + abs(2))');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(3)');
+  });
+
+  it('supports sign', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'sign(.1)');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(1)');
+  });
+
+  it('supports sign inside calc', () => {
+    const style = new CSSStyleDeclaration();
+    style.setProperty('width', 'calc(sign(.1) + sign(.2))');
+    assert.strictEqual(style.getPropertyValue('width'), 'calc(2)');
+  });
 });
diff --git a/test/parsers.js b/test/parsers.js
index 4f0fa97..a06ef7f 100644
--- a/test/parsers.js
+++ b/test/parsers.js
@@ -68,13 +68,49 @@ describe('valueType', () => {
     assert.strictEqual(output, parsers.TYPES.LENGTH);
   });
 
+  it('returns var from calc(100px *  var(--foo))', () => {
+    let input = 'calc(100px *  var(--foo))';
+    let output = parsers.valueType(input);
+
+    assert.strictEqual(output, parsers.TYPES.VAR);
+  });
+
+  it('returns var from var(--foo)', () => {
+    let input = 'var(--foo)';
+    let output = parsers.valueType(input);
+
+    assert.strictEqual(output, parsers.TYPES.VAR);
+  });
+
+  it('returns var from var(--foo, var(--bar))', () => {
+    let input = 'var(--foo, var(--bar))';
+    let output = parsers.valueType(input);
+
+    assert.strictEqual(output, parsers.TYPES.VAR);
+  });
+
+  it('returns var from var(--foo, calc(var(--bar) * 2))', () => {
+    let input = 'var(--foo, calc(var(--bar) * 2))';
+    let output = parsers.valueType(input);
+
+    assert.strictEqual(output, parsers.TYPES.VAR);
+  });
+
   it('returns calc from calc(100px * 2)', () => {
     let input = 'calc(100px * 2)';
     let output = parsers.valueType(input);
 
     assert.strictEqual(output, parsers.TYPES.CALC);
   });
+
+  it('returns calc from calc(100px *  calc(2 * 1))', () => {
+    let input = 'calc(100px * calc(2 * 1))';
+    let output = parsers.valueType(input);
+
+    assert.strictEqual(output, parsers.TYPES.CALC);
+  });
 });
+
 describe('parseInteger', () => {
   it.todo('test');
 });
@@ -88,6 +124,41 @@ describe('parsePercent', () => {
   it.todo('test');
 });
 describe('parseMeasurement', () => {
+  it('should return value with em unit', () => {
+    let input = '1em';
+    let output = parsers.parseMeasurement(input);
+
+    assert.strictEqual(output, '1em');
+  });
+
+  it('should return value with percent', () => {
+    let input = '100%';
+    let output = parsers.parseMeasurement(input);
+
+    assert.strictEqual(output, '100%');
+  });
+
+  it('should return value as is', () => {
+    let input = 'var(/* comment */ --foo)';
+    let output = parsers.parseMeasurement(input);
+
+    assert.strictEqual(output, 'var(/* comment */ --foo)');
+  });
+
+  it('should return calculated value', () => {
+    let input = 'calc(2em / 3)';
+    let output = parsers.parseMeasurement(input);
+
+    assert.strictEqual(output, 'calc(0.666667em)');
+  });
+
+  it('should return calculated value', () => {
+    let input = 'calc(100% / 3)';
+    let output = parsers.parseMeasurement(input);
+
+    assert.strictEqual(output, 'calc(33.3333%)');
+  });
+
   it.todo('test');
 });
 describe('parseUrl', () => {