Skip to content

Commit

Permalink
Option to allow physical inset with absolute/fixed center (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
AhmedBaset authored Sep 27, 2024
2 parents c3fd0e8 + f7fa50a commit 34e3580
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 85 deletions.
7 changes: 7 additions & 0 deletions .changeset/young-ties-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"eslint-plugin-rtl-friendly": minor
---

Add option `allowPhysicalInsetWithAbsolute` to allow the use of `left-1/2` with `fixed -translate-x-1/2`

Add option `debug`
8 changes: 7 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import js from "@eslint/js";
import eslintPlugin from "eslint-plugin-eslint-plugin";
import { config, configs } from "typescript-eslint";

import rtlFriendly from "./dist/index.js";
import rtlFriendly, { ruleSettings } from "./dist/index.js";

export default config(
{
Expand All @@ -25,5 +25,11 @@ export default config(
{
files: ["**/*.{tsx,jsx}"],
...rtlFriendly.configs.recommended,
rules: {
"rtl-friendly/no-physical-properties": [
"warn",
ruleSettings({ allowPhysicalInsetWithAbsolute: true }),
],
},
}
);
7 changes: 7 additions & 0 deletions src/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Act as default options for rules
*/
export const FLAGS = {
allowPhysicalInsetWithAbsolute: false,
debug: false,
};
19 changes: 13 additions & 6 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import rtlFriendly from "./index.js";
import rtlFriendly, { ruleSettings } from "./index.js";
import { name, version } from "../package.json";
import type { RuleModule } from "@typescript-eslint/utils/ts-eslint";
import type { MessageId } from "./rules/no-phyisical-properties/rule.js";
import type { Rule } from "./rules/no-phyisical-properties/rule.js";

// useless tests generated by copilot :)

Expand All @@ -14,9 +13,9 @@ describe("rtlFriendly", () => {

it("should have the no-physical-properties rule", () => {
expect(rtlFriendly.rules).toHaveProperty("no-physical-properties");
expectTypeOf(rtlFriendly.rules["no-physical-properties"]).toMatchTypeOf<
RuleModule<MessageId>
>();
expectTypeOf(
rtlFriendly.rules["no-physical-properties"]
).toMatchTypeOf<Rule>();
});

describe("configs", () => {
Expand Down Expand Up @@ -46,3 +45,11 @@ describe("rtlFriendly", () => {
});
});
});

describe("ruleSettings", () => {
it("should return the options", () => {
expect(ruleSettings({ allowPhysicalInsetWithAbsolute: true })).toEqual({
allowPhysicalInsetWithAbsolute: true,
});
});
});
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
import { name, version } from "../package.json";
import { noPhysicalProperties } from "./rules/no-phyisical-properties/rule.js";
import {
noPhysicalProperties,
ruleSettings,
} from "./rules/no-phyisical-properties/rule.js";

const rtlFriendly = {
meta: { name, version },
Expand Down Expand Up @@ -32,3 +35,5 @@ const configs = {
Object.assign(rtlFriendly.configs, configs);

export default rtlFriendly;

export { ruleSettings, rtlFriendly };
29 changes: 20 additions & 9 deletions src/rules/no-phyisical-properties/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function extractTokensFromNode(
// node: TSESTree.JSXAttribute,
node: TSESTree.Node,
ctx: Context,
runner: "checker" | "fixer"
{ debug }: { debug: boolean }
): Token[] {
if (node.type === "JSXAttribute") {
// value: Literal | JSXExpressionContainer | JSXElement | JSXFragment | null
Expand All @@ -40,7 +40,7 @@ export function extractTokensFromNode(

if (!expression || expression?.type === "JSXEmptyExpression") return [];

return extractTokensFromExpression(expression, ctx, runner);
return extractTokensFromExpression(expression, ctx, { debug });
}

// if (value.type === "JSXElement" || value.type === "JSXSpreadChild") {
Expand All @@ -66,12 +66,15 @@ type Exp = TSESTree.Expression | TSESTree.TemplateElement;
function extractTokensFromExpression(
exp: Exp,
ctx: Context,
runner: "checker" | "fixer",
{ isIdentifier = false }: { isIdentifier?: boolean } = {}
{
isIdentifier = false,
debug,
}: { isIdentifier?: boolean; debug?: boolean } = {}
): Token[] {
const rerun = (expression: Exp, referenceIsIdentifier?: boolean) => {
return extractTokensFromExpression(expression, ctx, runner, {
return extractTokensFromExpression(expression, ctx, {
isIdentifier: referenceIsIdentifier || isIdentifier,
debug,
});
};

Expand Down Expand Up @@ -175,9 +178,10 @@ function extractTokensFromExpression(
return writes.flatMap((n) => rerun(n, true));
}

// if (is(exp, "MemberExpression") && is(exp.property, "Identifier")) {
// return [];
// }
if (is(exp, "MemberExpression")) {
// Unsupported
return [];
}

/*
if (is(exp, "ArrowFunctionExpression")) {
Expand Down Expand Up @@ -225,7 +229,14 @@ function extractTokensFromExpression(
// }

if (!unimplemented.has(exp.type)) {
console.log("Unimplemented: ", exp.type, exp);
if (debug) {
console.log(
"rtl-friendly plugin detected that you are writing your writing your tailwind classes in a way that is not yet supported by this plugin.\n" +
"Kindly open an issue on GitHub so we can add support for this case. Thanks!\n" +
`https://github.com/AhmedBaset/eslint-plugin-rtl-friendly/issues/new?title=Unimplemented+Node%3A+%60${exp.type}%60\n`,
"You can disable this warning by setteng the `debug` option to `false` the rule options."
);
}
unimplemented.add(exp.type);
}

Expand Down
54 changes: 40 additions & 14 deletions src/rules/no-phyisical-properties/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from "@typescript-eslint/utils/ts-eslint";
import { type Token, extractTokensFromNode } from "./ast.js";
import { parseForPhysicalClasses } from "./tailwind.js";
import { FLAGS } from "../../flags.js";

// const cache = new Map<
// /** invalid */ string,
Expand All @@ -15,15 +16,20 @@ import { parseForPhysicalClasses } from "./tailwind.js";
// return `https://github.com/AhmedBaset/eslint-plugin-rtl-friendly/blob/main/src/rules/${ruleName}/README.md`;
// });

// Since the rule is no longer for physical properties specifically,
// we consider renaming it e.g. `rtl-friendly/tailwind`
export const RULE_NAME = "no-physical-properties";

export const NO_PHYSICAL_CLASSESS = "NO_PHYSICAL_CLASSESS";
export const IDENTIFIER_USED = "IDENTIFIER_USED";
export type MessageId = "NO_PHYSICAL_CLASSESS" | "IDENTIFIER_USED";
export type Rule = RuleModule<
MessageId,
[{ allowPhysicalInsetWithAbsolute?: boolean; debug?: boolean }]
>;

export const noPhysicalProperties: RuleModule<MessageId> = {
export const noPhysicalProperties: Rule = {
// name: RULE_NAME,
defaultOptions: [],
meta: {
type: "suggestion",
docs: {
Expand All @@ -35,8 +41,24 @@ export const noPhysicalProperties: RuleModule<MessageId> = {
[NO_PHYSICAL_CLASSESS]: `Avoid using physical properties such as "{{ invalid }}". Instead, use logical properties like "{{ valid }}" for better RTL support.`,
[IDENTIFIER_USED]: `This text is used later as a class name but contains physical properties such as "{{ invalid }}". It's better to use logical properties like "{{ valid }}" for improved RTL support.`,
},
schema: [],
schema: [
{
type: "object",
properties: {
allowPhysicalInsetWithAbsolute: {
type: "boolean",
default: false,
},
debug: {
type: "boolean",
default: false,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [FLAGS],
create: (ctx) => {
return {
JSXAttribute: (node) => {
Expand All @@ -46,12 +68,6 @@ export const noPhysicalProperties: RuleModule<MessageId> = {
const isClassAttribute = ["className", "class"].includes(attr);
if (!isClassAttribute) return;

// let result = extractFromNode(node);
// if (!result) return;

// result = result.filter((c) => typeof c === "string");
// if (!result.length) return;

// const classesAsString = result.join(" ");
// const cachedValid = cache.get(classesAsString);
// if (cachedValid) {
Expand All @@ -60,14 +76,20 @@ export const noPhysicalProperties: RuleModule<MessageId> = {
// return;
// }

const tokens = extractTokensFromNode(node, ctx, "checker");
const allowPhysicalInsetWithAbsolute =
ctx.options[0]?.allowPhysicalInsetWithAbsolute ??
FLAGS.allowPhysicalInsetWithAbsolute;
const debug = ctx.options[0]?.debug ?? FLAGS.debug;

const tokens = extractTokensFromNode(node, ctx, { debug });
tokens?.forEach((token) => {
const classValue = token?.getValue();
if (!classValue) return;

const classes = classValue.split(" ");

const parsed = parseForPhysicalClasses(classes);
const parsed = parseForPhysicalClasses(
classValue,
allowPhysicalInsetWithAbsolute
);

const isInvalid = parsed.some((p) => p.isInvalid);
if (!isInvalid) return;
Expand All @@ -86,8 +108,12 @@ export const noPhysicalProperties: RuleModule<MessageId> = {
},
};

export function ruleSettings(options: Partial<Rule["defaultOptions"][number]>) {
return options;
}

export type Context = Readonly<
RuleContext<"NO_PHYSICAL_CLASSESS" | "IDENTIFIER_USED", []>
RuleContext<keyof Rule["meta"]["messages"], Rule["defaultOptions"]>
>;

function report({
Expand Down
51 changes: 44 additions & 7 deletions src/rules/no-phyisical-properties/tailwind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export const twLogicalClasses = [
{ physical: "mr-", /* */ logical: "me-" },
{ physical: "pl-", /* */ logical: "ps-" },
{ physical: "pr-", /* */ logical: "pe-" },
{ physical: "left-", /* */ logical: "start-" },
{ physical: "right-", /* */ logical: "end-" },
{ physical: "left-", /* */ logical: "start-", if: isNotAbsoluteCenterd },
{ physical: "right-", /* */ logical: "end-", if: isNotAbsoluteCenterd },
{ physical: "text-left", /* */ logical: "text-start" },
{ physical: "text-right", /* */ logical: "text-end" },
{ physical: "border-l-", /* */ logical: "border-s-" },
Expand All @@ -19,7 +19,11 @@ export const twLogicalClasses = [
{ physical: "scroll-mr-", /* */ logical: "scroll-me-" },
{ physical: "scroll-pl-", /* */ logical: "scroll-ps-" },
{ physical: "scroll-pr-", /* */ logical: "scroll-pe-" },
] satisfies { physical: string; logical: string }[];
] satisfies {
physical: string;
logical: string;
if?: (className: string) => boolean;
}[];

export function tailwindClassCases(cls: string) {
return [
Expand All @@ -33,11 +37,35 @@ export function tailwindClassCases(cls: string) {
];
}

const allCases = twLogicalClasses.flatMap(({ physical, logical }) =>
tailwindClassCases(physical).map((regex) => ({ regex, physical, logical }))
);
function getAllCases(
className: string,
allowPhysicalInsetWithAbsolute: boolean
) {
return twLogicalClasses.flatMap((cls) => {
const shouldValidate = allowPhysicalInsetWithAbsolute
? cls.if?.(className) ?? true
: true;
if (!shouldValidate) return [];

const { physical, logical } = cls;
return tailwindClassCases(physical).map((regex) => {
return {
regex,
physical,
logical,
};
});
});
}

export function parseForPhysicalClasses(
className: string,
allowPhysicalInsetWithAbsolute: boolean
) {
const allCases = getAllCases(className, allowPhysicalInsetWithAbsolute);

const classes = className.split(" ");

export function parseForPhysicalClasses(classes: string[]) {
return classes.map((cls) => {
const isInvalid = allCases.some(({ regex }) => regex.test(cls));
const valid = allCases.reduce(
Expand All @@ -64,3 +92,12 @@ export function parseForPhysicalClasses(classes: string[]) {
// });
// });
}

function isNotAbsoluteCenterd(className: string) {
return !["absolute", "fixed", "sticky"].some((c) => {
// We match absolute-CENTERED not every absolute position
// We encourage the usage of logical properties with positioning except for valid
// cases like center with fixed/absolute
return className.includes(c) && className.includes("translate-x");
});
}
Loading

0 comments on commit 34e3580

Please sign in to comment.