From bab4fcd31dc89f4f70abafce3debcfd366c4e51f Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Thu, 9 Jan 2025 13:58:04 +0900 Subject: [PATCH] Support nested var() and nested calc() Fixes #125. Closes #127. --- lib/CSSStyleDeclaration.js | 4 +- lib/parsers.js | 35 +++++++++++----- package-lock.json | 65 ++++++++++++++++++++++++----- package.json | 2 +- test/CSSStyleDeclaration.js | 81 +++++++++++++++++++++++++++++++++++++ test/parsers.js | 71 ++++++++++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 22 deletions(-) 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": { 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', () => {