Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

operator-no-unspaced: add support for :has #768

Merged
merged 1 commit into from Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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