Skip to content

Commit

Permalink
operator-no-unspaced: add support for :has (#768)
Browse files Browse the repository at this point in the history
  • Loading branch information
kristerkari committed Feb 22, 2023
1 parent 26ed911 commit 970d0c4
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 8 deletions.
10 changes: 10 additions & 0 deletions src/rules/operator-no-unspaced/__tests__/index.js
Expand Up @@ -1323,6 +1323,16 @@ testRule({
}
`,
description: "ignores @at-root"
},
{
code: `
.element {
@supports selector(:has(*)) {
opacity: 0;
}
}
`,
description: "issue #709"
}
],

Expand Down
70 changes: 70 additions & 0 deletions 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");
});
});
51 changes: 43 additions & 8 deletions src/utils/sassValueParser/index.js
Expand Up @@ -158,7 +158,7 @@ export function mathOperatorCharType(string, index, isAfterColon) {

// ---- Processing * character
if (character === "*") {
return "op";
return checkMultiplication(string, index);
}

// ---- Processing % character
Expand All @@ -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
*
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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";
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 970d0c4

Please sign in to comment.