From 970d0c4e9d6050737fa560723c2420c49be7b9d5 Mon Sep 17 00:00:00 2001 From: Krister Kari Date: Wed, 22 Feb 2023 17:50:08 -0300 Subject: [PATCH] operator-no-unspaced: add support for :has (#768) --- .../operator-no-unspaced/__tests__/index.js | 10 +++ .../__tests__/isInsideFunctionCall.test.js | 70 +++++++++++++++++++ src/utils/sassValueParser/index.js | 51 +++++++++++--- 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/utils/__tests__/isInsideFunctionCall.test.js diff --git a/src/rules/operator-no-unspaced/__tests__/index.js b/src/rules/operator-no-unspaced/__tests__/index.js index 2e8bf2df..8ebdcdae 100644 --- a/src/rules/operator-no-unspaced/__tests__/index.js +++ b/src/rules/operator-no-unspaced/__tests__/index.js @@ -1323,6 +1323,16 @@ testRule({ } `, description: "ignores @at-root" + }, + { + code: ` + .element { + @supports selector(:has(*)) { + opacity: 0; + } + } + `, + description: "issue #709" } ], diff --git a/src/utils/__tests__/isInsideFunctionCall.test.js b/src/utils/__tests__/isInsideFunctionCall.test.js new file mode 100644 index 00000000..c87ccc9a --- /dev/null +++ b/src/utils/__tests__/isInsideFunctionCall.test.js @@ -0,0 +1,70 @@ +import { isInsideFunctionCall } from "../sassValueParser"; + +describe("isInsideFunctionCall", () => { + it("should handle operators/signs inside url functions", () => { + const urlFunctions = [ + { + string: "url(../../img/build/svg/arrow-11-down-dark.svg)", + index: 13 + }, + { + string: "url(../../img/build/svg/arrow-#{$i + 2}-down-dark.svg)", + index: 13 + }, + { + string: "url(https://99-0a.x.y.rackcdn.com/z.jpg)", + index: 11 + }, + { + string: + "url(../../img/build/svg/arrow-11-down-dark.svg), url(../../img/build/svg/arrow-11-down-dark.svg)", + index: 13 + }, + { + string: + "url(https://99-0a.x.y.rackcdn.com/img/build/svg/arrow-#{$i / 2}-down-dark.svg)", + index: 14 + }, + { + string: "url(../../img/build/svg/arrow-#{$i /2}-down-dark.svg)", + index: 13 + }, + { + string: + "url(https://99-0a.x.y.rackcdn.com/img/build/svg/arrow-#{$i * 2}-down-dark.svg)", + index: 14 + }, + { + string: + "url(https://99-0a.x.y.rackcdn.com/img/build/svg/arrow-#{$i % 2}-down-dark.svg)", + index: 14 + }, + { + string: + "url(https://99-0a.x.y.rackcdn.com/img/build/svg/arrow-#{$i %2}-down-dark.svg)", + index: 59 + } + ]; + + urlFunctions.forEach(test => { + expect(isInsideFunctionCall(test.string, test.index).fn).toBe("url"); + }); + }); + + it("should handle operators/signs inside translate function", () => { + expect( + isInsideFunctionCall("translate(-50%, -$no-ui-slider-height)", 16).fn + ).toBe("translate"); + }); + + it("should handle operators/signs that are interpolated", () => { + expect(isInsideFunctionCall("#{math.acos(0.7-0.5)}", 15).fn).toBe("acos"); + expect( + isInsideFunctionCall("#{scale-color(#fff, $lightness: -75%)}", 32).fn + ).toBe("scale-color"); + }); + + it("should handle nested functions", () => { + expect(isInsideFunctionCall("selector(:has(*))", 14).fn).toBe(":has"); + }); +}); diff --git a/src/utils/sassValueParser/index.js b/src/utils/sassValueParser/index.js index 5508fd06..c517409b 100644 --- a/src/utils/sassValueParser/index.js +++ b/src/utils/sassValueParser/index.js @@ -158,7 +158,7 @@ export function mathOperatorCharType(string, index, isAfterColon) { // ---- Processing * character if (character === "*") { - return "op"; + return checkMultiplication(string, index); } // ---- Processing % character @@ -180,6 +180,34 @@ export function mathOperatorCharType(string, index, isAfterColon) { // Functions for checking particular characters (+, -, /) // -------------------------------------------------------------------------- +/** + * Checks the specified `*` char type: operator, sign (*), part of string + * + * @param {String} string - the source string + * @param {Number} index - the index of the character in string to check + * @return {String|false} + * • "op", if the character is a operator in a math/string operation + * • "sign" if it is a sign before a positive number, + * • "char" if it is a part of a string or identifier, + * • false - if it is none from above (most likely an error) + */ +function checkMultiplication(string, index) { + const insideFn = isInsideFunctionCall(string, index); + + if (insideFn.is && insideFn.fn) { + const fnArgsReg = new RegExp(insideFn.fn + "\\(([^)]+)\\)"); + const fnArgs = string.match(fnArgsReg); + const isSingleMultiplicationChar = + Array.isArray(fnArgs) && fnArgs[1] === "*"; + // e.g. selector(:has(*)) + if (isSingleMultiplicationChar) { + return "char"; + } + } + + return "op"; +} + /** * Checks the specified `+` char type: operator, sign (+ or -), part of string * @@ -350,6 +378,11 @@ function checkMinus(string, index) { // e.g. `#{10px -1}`, `#{math.acos(-0.5)}` if (isInsideInterpolation(string, index)) { + // e.g. `url(https://my-url.com/image-#{$i -2}-dark.svg)` + if (isInsideFunctionCall_.fn === "url") { + return "op"; + } + if ( isInsideFunctionCall_.is && ((isValueWithUnitAfter_.is && !isValueWithUnitAfter_.opsBetween) || @@ -518,7 +551,11 @@ function checkSlash(string, index, isAfterColon) { // e.g. `(1px/1)`, `fn(7 / 15)`, but not `url(8/11)` const isInsideFn = isInsideFunctionCall(string, index); - if (isInsideFn.is && isInsideFn.fn === "url" && isProtocolBefore(before)) { + if (isInsideFn.is && isInsideFn.fn === "url") { + // e.g. `url(https://my-url.com/image-#{$i /2}-dark.svg)` + if (isInsideInterpolation(string, index)) { + return "op"; + } return "char"; } @@ -693,11 +730,13 @@ function isInsideInterpolation(string, index) { * {Boolean} return.is - if inside a function arguments * {String} return.fn - function name */ -function isInsideFunctionCall(string, index) { +export function isInsideFunctionCall(string, index) { const result = { is: false, fn: null }; const before = string.substring(0, index).trim(); const after = string.substring(index + 1).trim(); - const beforeMatch = before.match(/([a-zA-Z_-][\w-]*)\([^(){}]+$/); + const beforeMatch = before.match( + /(?:[a-zA-Z_-][\w-]*\()?(:?[a-zA-Z_-][\w-]*)\(/ + ); if (beforeMatch && beforeMatch[0] && after.search(/^[^(,]+\)/) !== -1) { result.is = true; @@ -895,10 +934,6 @@ function isDotBefore(before) { return before.slice(-1) === "."; } -function isProtocolBefore(before) { - return before.search(/https?:/) !== -1; -} - function isFunctionBefore(before) { return before.trim().search(/[\w-]\(.*?\)\s*$/) !== -1; }