rtl-shield 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +24 -0
- package/dist/rules/prefer-logical-properties.d.ts +3 -0
- package/dist/rules/prefer-logical-properties.js +80 -0
- package/dist/rules/prefer-logical-tailwindcss.d.ts +3 -0
- package/dist/rules/prefer-logical-tailwindcss.js +156 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Berkin Duz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# 🛡️ RTL Shield
|
|
2
|
+
|
|
3
|
+
**Automated ESLint guardrails for robust RTL support in the GCC market.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/eslint-plugin-rtl-shield)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚠️ The Problem
|
|
11
|
+
|
|
12
|
+
In the Middle East and North Africa (MENA) region, Arabic and other RTL (Right-to-Left) languages represent a massive market opportunity. However, **many engineering teams still build with physical CSS properties** like `left`, `right`, `marginLeft`, and `paddingRight`—properties that fundamentally break bidirectional layouts.
|
|
13
|
+
|
|
14
|
+
### Why This Matters
|
|
15
|
+
|
|
16
|
+
- **Physical properties are direction-specific**: `left: 10px` means "move 10px from the left edge"—in RTL, this is visually incorrect.
|
|
17
|
+
- **Manual reviews are error-prone**: CSS property mapping is tedious and easy to overlook in code reviews.
|
|
18
|
+
- **Scale is impossible**: As codebases grow, maintaining RTL consistency becomes exponentially harder without automation.
|
|
19
|
+
- **Lost revenue**: Poorly localized experiences drive users away from your platform.
|
|
20
|
+
|
|
21
|
+
**RTL Shield solves this at the source**—by enforcing logical CSS properties during development, ensuring your application is RTL-ready before it ships.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ✨ Key Features
|
|
26
|
+
|
|
27
|
+
### 🎨 **CSS-in-JS & Style Objects**
|
|
28
|
+
|
|
29
|
+
Full support for React, Next.js, and JavaScript style objects (Emotion, Styled Components, inline styles, etc.)
|
|
30
|
+
|
|
31
|
+
- Automatically detects and fixes physical properties
|
|
32
|
+
- Works with object literals in JavaScript
|
|
33
|
+
- Smart autofix saves hundreds of manual hours
|
|
34
|
+
|
|
35
|
+
### 🌬️ **Tailwind CSS Support**
|
|
36
|
+
|
|
37
|
+
Seamlessly handles Tailwind utility classes in `className` and `class` attributes
|
|
38
|
+
|
|
39
|
+
- Converts `ml-4` → `ms-4`, `text-left` → `text-start`
|
|
40
|
+
- Processes multiple classes in a single string
|
|
41
|
+
- Fully compatible with Next.js and modern React projects
|
|
42
|
+
|
|
43
|
+
### 🔄 **Comprehensive Property Mapping**
|
|
44
|
+
|
|
45
|
+
Covers the full spectrum of directional CSS:
|
|
46
|
+
|
|
47
|
+
- **Margins & Padding**: `marginLeft` → `marginInlineStart`
|
|
48
|
+
- **Positioning**: `left` → `insetInlineStart`, `right` → `insetInlineEnd`
|
|
49
|
+
- **Borders**: All variants—`borderLeft`, `borderLeftWidth`, `borderLeftColor`, etc.
|
|
50
|
+
- **Border Radius**: Complex radius properties like `borderTopLeftRadius` → `borderStartStartRadius`
|
|
51
|
+
|
|
52
|
+
### ⚡ **Smart Autofix**
|
|
53
|
+
|
|
54
|
+
One command to fix your entire codebase:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
eslint --fix
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No manual intervention required—just commit and deploy.
|
|
61
|
+
|
|
62
|
+
### 🎯 **Zero Configuration**
|
|
63
|
+
|
|
64
|
+
The `recommended` preset works out-of-the-box with sensible defaults.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📦 Installation
|
|
69
|
+
|
|
70
|
+
### Step 1: Install the Plugin
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install --save-dev eslint-plugin-rtl-shield
|
|
74
|
+
# or
|
|
75
|
+
yarn add --dev eslint-plugin-rtl-shield
|
|
76
|
+
# or
|
|
77
|
+
pnpm add --save-dev eslint-plugin-rtl-shield
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Step 2: Configure ESLint
|
|
81
|
+
|
|
82
|
+
Add the plugin to your ESLint configuration file (`.eslintrc.json`, `.eslintrc.js`, or `eslint.config.js`):
|
|
83
|
+
|
|
84
|
+
#### Using the Recommended Config (Recommended âś…)
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"extends": ["plugin:rtl-shield/recommended"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This automatically enables both rules with error-level severity:
|
|
93
|
+
|
|
94
|
+
- `rtl-shield/prefer-logical-properties` — for CSS-in-JS
|
|
95
|
+
- `rtl-shield/prefer-logical-tailwindcss` — for Tailwind CSS
|
|
96
|
+
|
|
97
|
+
#### Manual Configuration
|
|
98
|
+
|
|
99
|
+
If you prefer granular control:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"plugins": ["rtl-shield"],
|
|
104
|
+
"rules": {
|
|
105
|
+
"rtl-shield/prefer-logical-properties": "error",
|
|
106
|
+
"rtl-shield/prefer-logical-tailwindcss": "error"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Step 3: Run ESLint
|
|
112
|
+
|
|
113
|
+
Lint your project:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
eslint .
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Auto-fix issues:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
eslint --fix .
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 📊 Showcase: Physical vs. Logical
|
|
128
|
+
|
|
129
|
+
### CSS-in-JS / Style Objects
|
|
130
|
+
|
|
131
|
+
| Physical (❌ Breaks RTL) | Logical (✅ RTL-Ready) | Use Case |
|
|
132
|
+
| ------------------------- | ---------------------------- | --------------- |
|
|
133
|
+
| `marginLeft: 16` | `marginInlineStart: 16` | Outer spacing |
|
|
134
|
+
| `paddingRight: 8` | `paddingInlineEnd: 8` | Inner spacing |
|
|
135
|
+
| `left: 0` | `insetInlineStart: 0` | Positioning |
|
|
136
|
+
| `right: 10` | `insetInlineEnd: 10` | Positioning |
|
|
137
|
+
| `borderLeftWidth: 2` | `borderInlineStartWidth: 2` | Border styling |
|
|
138
|
+
| `borderTopLeftRadius: 12` | `borderStartStartRadius: 12` | Rounded corners |
|
|
139
|
+
|
|
140
|
+
### Tailwind CSS
|
|
141
|
+
|
|
142
|
+
| Physical (❌ Breaks RTL) | Logical (✅ RTL-Ready) | Use Case |
|
|
143
|
+
| ------------------------ | ---------------------- | ---------------------------- |
|
|
144
|
+
| `ml-4` | `ms-4` | Margin start |
|
|
145
|
+
| `mr-8` | `me-8` | Margin end |
|
|
146
|
+
| `pl-2` | `ps-2` | Padding start |
|
|
147
|
+
| `pr-6` | `pe-6` | Padding end |
|
|
148
|
+
| `text-left` | `text-start` | Text alignment |
|
|
149
|
+
| `text-right` | `text-end` | Text alignment |
|
|
150
|
+
| `rounded-tl-lg` | `rounded-ss-lg` | Border radius (top-left) |
|
|
151
|
+
| `rounded-br-md` | `rounded-ee-md` | Border radius (bottom-right) |
|
|
152
|
+
| `left-0` | `start-0` | Positioning |
|
|
153
|
+
| `right-10` | `end-10` | Positioning |
|
|
154
|
+
|
|
155
|
+
### Before & After Example
|
|
156
|
+
|
|
157
|
+
**Before (Physical Properties):**
|
|
158
|
+
|
|
159
|
+
```jsx
|
|
160
|
+
// ❌ Breaks in Arabic/RTL
|
|
161
|
+
const Card = () => (
|
|
162
|
+
<div className="ml-4 p-4 rounded-tl-lg">
|
|
163
|
+
<h1 style={{ paddingLeft: 16, textAlign: "left" }}>Title</h1>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**After (Logical Properties):**
|
|
169
|
+
|
|
170
|
+
```jsx
|
|
171
|
+
// âś… Works perfectly in RTL
|
|
172
|
+
const Card = () => (
|
|
173
|
+
<div className="ms-4 p-4 rounded-ss-lg">
|
|
174
|
+
<h1 style={{ paddingInlineStart: 16, textAlign: "start" }}>Title</h1>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🌍 Why for the GCC Market?
|
|
182
|
+
|
|
183
|
+
The GCC region (United Arab Emirates, Saudi Arabia, Qatar, Kuwait, Bahrain, and Oman) represents one of the fastest-growing digital markets:
|
|
184
|
+
|
|
185
|
+
- **High-value market**: GCC users have the highest digital spending in the MENA region
|
|
186
|
+
- **Native RTL requirement**: Arabic is a constitutional requirement in government and enterprise apps
|
|
187
|
+
- **Business critical**: Improper RTL support directly impacts user trust and platform adoption
|
|
188
|
+
- **Regulatory compliance**: Many GCC-based companies must meet localization standards
|
|
189
|
+
- **Competitive advantage**: Teams that ship proper RTL support get better retention and market share
|
|
190
|
+
|
|
191
|
+
**RTL Shield enables your engineering team to:**
|
|
192
|
+
|
|
193
|
+
- Ship RTL-ready code from day one
|
|
194
|
+
- Reduce localization QA cycles
|
|
195
|
+
- Avoid expensive refactoring late in the project lifecycle
|
|
196
|
+
- Build products that resonate with users across the MENA region
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## đź“‹ Rules
|
|
201
|
+
|
|
202
|
+
### `prefer-logical-properties`
|
|
203
|
+
|
|
204
|
+
Enforces logical CSS properties in JavaScript style objects, preventing physical properties that break bidirectional layouts.
|
|
205
|
+
|
|
206
|
+
**Type:** Suggestion
|
|
207
|
+
**Fixable:** Yes âś…
|
|
208
|
+
|
|
209
|
+
**Example:**
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// ❌ Error
|
|
213
|
+
const styles = { marginLeft: 10, borderRightColor: "red" };
|
|
214
|
+
|
|
215
|
+
// âś… Correct
|
|
216
|
+
const styles = { marginInlineStart: 10, borderInlineEndColor: "red" };
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `prefer-logical-tailwindcss`
|
|
220
|
+
|
|
221
|
+
Enforces logical Tailwind CSS utility classes in JSX `className` and `class` attributes.
|
|
222
|
+
|
|
223
|
+
**Type:** Suggestion
|
|
224
|
+
**Fixable:** Yes âś…
|
|
225
|
+
|
|
226
|
+
**Example:**
|
|
227
|
+
|
|
228
|
+
```jsx
|
|
229
|
+
// ❌ Error
|
|
230
|
+
<div className="ml-4 text-left rounded-tl-lg" />
|
|
231
|
+
|
|
232
|
+
// âś… Correct
|
|
233
|
+
<div className="ms-4 text-start rounded-ss-lg" />
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 🚀 Quick Start
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# 1. Install
|
|
242
|
+
npm install --save-dev eslint-plugin-rtl-shield
|
|
243
|
+
|
|
244
|
+
# 2. Add to .eslintrc.json
|
|
245
|
+
echo '{
|
|
246
|
+
"extends": ["plugin:rtl-shield/recommended"]
|
|
247
|
+
}' > .eslintrc.json
|
|
248
|
+
|
|
249
|
+
# 3. Fix all files
|
|
250
|
+
npx eslint --fix .
|
|
251
|
+
|
|
252
|
+
# 4. Commit and deploy
|
|
253
|
+
git add .
|
|
254
|
+
git commit -m "chore: enforce RTL-safe logical properties"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 📚 Resources
|
|
260
|
+
|
|
261
|
+
- [MDN: CSS Logical Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties)
|
|
262
|
+
- [Tailwind CSS: Logical Properties](https://tailwindcss.com/docs/guides/rtl)
|
|
263
|
+
- [W3C: Structural Markup and Right-to-Left Text in HTML](https://www.w3.org/International/questions/qa-html-dir)
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 🤝 Contributing
|
|
268
|
+
|
|
269
|
+
We welcome contributions! Please feel free to open issues or submit pull requests on [GitHub](https://github.com/berkinduz/rtl-shield).
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## đź“„ License
|
|
274
|
+
|
|
275
|
+
MIT © 2025 [Berkin Duz](https://github.com/berkinduz)
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## đź’ˇ Support
|
|
280
|
+
|
|
281
|
+
Have questions or feedback? Open an issue on our [GitHub repository](https://github.com/berkinduz/rtl-shield/issues).
|
|
282
|
+
|
|
283
|
+
**Built with ❤️ for the GCC market and the broader MENA region.**
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
declare const _default: {
|
|
3
|
+
rules: Record<string, Rule.RuleModule>;
|
|
4
|
+
configs: {
|
|
5
|
+
recommended: {
|
|
6
|
+
plugins: string[];
|
|
7
|
+
rules: {
|
|
8
|
+
"rtl-shield/prefer-logical-properties": string;
|
|
9
|
+
"rtl-shield/prefer-logical-tailwindcss": string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const prefer_logical_properties_1 = __importDefault(require("./rules/prefer-logical-properties"));
|
|
7
|
+
const prefer_logical_tailwindcss_1 = __importDefault(require("./rules/prefer-logical-tailwindcss"));
|
|
8
|
+
const rules = {
|
|
9
|
+
"prefer-logical-properties": prefer_logical_properties_1.default,
|
|
10
|
+
"prefer-logical-tailwindcss": prefer_logical_tailwindcss_1.default,
|
|
11
|
+
};
|
|
12
|
+
const configs = {
|
|
13
|
+
recommended: {
|
|
14
|
+
plugins: ["rtl-shield"],
|
|
15
|
+
rules: {
|
|
16
|
+
"rtl-shield/prefer-logical-properties": "error",
|
|
17
|
+
"rtl-shield/prefer-logical-tailwindcss": "error",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
exports.default = {
|
|
22
|
+
rules,
|
|
23
|
+
configs,
|
|
24
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const PHYSICAL_TO_LOGICAL = {
|
|
4
|
+
marginLeft: "marginInlineStart",
|
|
5
|
+
marginRight: "marginInlineEnd",
|
|
6
|
+
paddingLeft: "paddingInlineStart",
|
|
7
|
+
paddingRight: "paddingInlineEnd",
|
|
8
|
+
left: "insetInlineStart",
|
|
9
|
+
right: "insetInlineEnd",
|
|
10
|
+
borderTopLeftRadius: "borderStartStartRadius",
|
|
11
|
+
borderBottomLeftRadius: "borderStartEndRadius",
|
|
12
|
+
borderTopRightRadius: "borderEndStartRadius",
|
|
13
|
+
borderBottomRightRadius: "borderEndEndRadius",
|
|
14
|
+
borderLeft: "borderInlineStart",
|
|
15
|
+
borderRight: "borderInlineEnd",
|
|
16
|
+
borderLeftWidth: "borderInlineStartWidth",
|
|
17
|
+
borderRightWidth: "borderInlineEndWidth",
|
|
18
|
+
borderLeftStyle: "borderInlineStartStyle",
|
|
19
|
+
borderRightStyle: "borderInlineEndStyle",
|
|
20
|
+
borderLeftColor: "borderInlineStartColor",
|
|
21
|
+
borderRightColor: "borderInlineEndColor",
|
|
22
|
+
};
|
|
23
|
+
const TEXT_ALIGN_MAPPING = {
|
|
24
|
+
left: "start",
|
|
25
|
+
right: "end",
|
|
26
|
+
};
|
|
27
|
+
const rule = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: "suggestion",
|
|
30
|
+
docs: {
|
|
31
|
+
description: "Enforce CSS Logical Properties instead of physical properties for better RTL support",
|
|
32
|
+
recommended: true,
|
|
33
|
+
},
|
|
34
|
+
fixable: "code",
|
|
35
|
+
schema: [],
|
|
36
|
+
},
|
|
37
|
+
create(context) {
|
|
38
|
+
return {
|
|
39
|
+
Property(node) {
|
|
40
|
+
// Check if this is a property with an Identifier or Literal key
|
|
41
|
+
const key = node.key;
|
|
42
|
+
let propName = "";
|
|
43
|
+
if (key.type === "Identifier") {
|
|
44
|
+
propName = key.name;
|
|
45
|
+
}
|
|
46
|
+
else if (key.type === "Literal" && typeof key.value === "string") {
|
|
47
|
+
propName = key.value;
|
|
48
|
+
}
|
|
49
|
+
// Check for physical properties that should be logical
|
|
50
|
+
if (propName in PHYSICAL_TO_LOGICAL) {
|
|
51
|
+
const logicalProperty = PHYSICAL_TO_LOGICAL[propName];
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
message: `Use logical property '${logicalProperty}' instead of '${propName}' for better RTL support`,
|
|
55
|
+
fix(fixer) {
|
|
56
|
+
return fixer.replaceText(key, logicalProperty);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Check for textAlign property
|
|
61
|
+
if (propName === "textAlign") {
|
|
62
|
+
const value = node.value;
|
|
63
|
+
if (value.type === "Literal" &&
|
|
64
|
+
typeof value.value === "string" &&
|
|
65
|
+
value.value in TEXT_ALIGN_MAPPING) {
|
|
66
|
+
const logicalValue = TEXT_ALIGN_MAPPING[value.value];
|
|
67
|
+
context.report({
|
|
68
|
+
node: value,
|
|
69
|
+
message: `Use textAlign: '${logicalValue}' instead of '${value.value}' for better RTL support`,
|
|
70
|
+
fix(fixer) {
|
|
71
|
+
return fixer.replaceText(value, `'${logicalValue}'`);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
exports.default = rule;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Mapping of physical Tailwind classes to their logical counterparts
|
|
4
|
+
const TAILWIND_PHYSICAL_TO_LOGICAL = {
|
|
5
|
+
"ml-": "ms-",
|
|
6
|
+
"mr-": "me-",
|
|
7
|
+
"pl-": "ps-",
|
|
8
|
+
"pr-": "pe-",
|
|
9
|
+
"text-left": "text-start",
|
|
10
|
+
"text-right": "text-end",
|
|
11
|
+
"rounded-tl-": "rounded-ss-",
|
|
12
|
+
"rounded-tr-": "rounded-se-",
|
|
13
|
+
"rounded-bl-": "rounded-es-",
|
|
14
|
+
"rounded-br-": "rounded-ee-",
|
|
15
|
+
"left-": "start-",
|
|
16
|
+
"right-": "end-",
|
|
17
|
+
};
|
|
18
|
+
const EXACT_REPLACEMENTS = {
|
|
19
|
+
"text-left": "text-start",
|
|
20
|
+
"text-right": "text-end",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Replace physical Tailwind classes with their logical equivalents
|
|
24
|
+
*/
|
|
25
|
+
function replaceClassesInString(str) {
|
|
26
|
+
let result = str;
|
|
27
|
+
// First, handle exact replacements (e.g., text-left -> text-start)
|
|
28
|
+
for (const [physical, logical] of Object.entries(EXACT_REPLACEMENTS)) {
|
|
29
|
+
result = result.replace(new RegExp(`\\b${physical}\\b`, "g"), logical);
|
|
30
|
+
}
|
|
31
|
+
// Then handle prefix replacements (e.g., ml- -> ms-)
|
|
32
|
+
// We need to be careful to match the full class name
|
|
33
|
+
for (const [physical, logical] of Object.entries(TAILWIND_PHYSICAL_TO_LOGICAL)) {
|
|
34
|
+
// Skip exact replacements as they're already handled
|
|
35
|
+
if (physical in EXACT_REPLACEMENTS)
|
|
36
|
+
continue;
|
|
37
|
+
// Create a regex that matches the physical prefix followed by any characters until whitespace or end
|
|
38
|
+
const regex = new RegExp(`\\b${physical.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([a-zA-Z0-9-]*)\\b`, "g");
|
|
39
|
+
result = result.replace(regex, `${logical}$1`);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if a string contains any physical Tailwind classes that need to be replaced
|
|
45
|
+
*/
|
|
46
|
+
function hasPhysicalClasses(str) {
|
|
47
|
+
// Check for exact matches first
|
|
48
|
+
for (const physical of Object.keys(EXACT_REPLACEMENTS)) {
|
|
49
|
+
if (new RegExp(`\\b${physical}\\b`).test(str)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Check for prefix matches
|
|
54
|
+
for (const physical of Object.keys(TAILWIND_PHYSICAL_TO_LOGICAL)) {
|
|
55
|
+
if (physical in EXACT_REPLACEMENTS)
|
|
56
|
+
continue;
|
|
57
|
+
if (new RegExp(`\\b${physical.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(str)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const rule = {
|
|
64
|
+
meta: {
|
|
65
|
+
type: "suggestion",
|
|
66
|
+
docs: {
|
|
67
|
+
description: "Enforce logical Tailwind CSS classes instead of physical ones for better RTL support",
|
|
68
|
+
recommended: true,
|
|
69
|
+
},
|
|
70
|
+
fixable: "code",
|
|
71
|
+
schema: [],
|
|
72
|
+
},
|
|
73
|
+
create(context) {
|
|
74
|
+
return {
|
|
75
|
+
Literal(node) {
|
|
76
|
+
// Check if this is a className or class attribute value
|
|
77
|
+
if (typeof node.value !== "string") {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const value = node.value;
|
|
81
|
+
// Only process if this looks like a className/class value
|
|
82
|
+
if (!hasPhysicalClasses(value)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Check if the parent is a JSXAttribute with name "className" or "class"
|
|
86
|
+
const parent = node.parent;
|
|
87
|
+
if (parent && parent.type === "JSXExpressionContainer") {
|
|
88
|
+
// Parent is within a JSX expression container
|
|
89
|
+
const grandParent = parent.parent;
|
|
90
|
+
if (grandParent &&
|
|
91
|
+
grandParent.type === "JSXAttribute" &&
|
|
92
|
+
(grandParent.name.name === "className" ||
|
|
93
|
+
grandParent.name.name === "class")) {
|
|
94
|
+
const logicalValue = replaceClassesInString(value);
|
|
95
|
+
if (logicalValue !== value) {
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
message: "Use logical Tailwind CSS classes instead of physical ones for better RTL support",
|
|
99
|
+
fix(fixer) {
|
|
100
|
+
return fixer.replaceText(node, `"${logicalValue}"`);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (parent && parent.type === "JSXAttribute") {
|
|
107
|
+
// Direct attribute value
|
|
108
|
+
if (parent.name.name === "className" ||
|
|
109
|
+
parent.name.name === "class") {
|
|
110
|
+
const logicalValue = replaceClassesInString(value);
|
|
111
|
+
if (logicalValue !== value) {
|
|
112
|
+
context.report({
|
|
113
|
+
node,
|
|
114
|
+
message: "Use logical Tailwind CSS classes instead of physical ones for better RTL support",
|
|
115
|
+
fix(fixer) {
|
|
116
|
+
return fixer.replaceText(node, `"${logicalValue}"`);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
TemplateElement(node) {
|
|
124
|
+
// Handle template literals in className attributes
|
|
125
|
+
const value = node.value.raw;
|
|
126
|
+
if (!hasPhysicalClasses(value)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Check if this is part of a template literal in className
|
|
130
|
+
const parent = node.parent;
|
|
131
|
+
if (parent && parent.type === "TemplateLiteral") {
|
|
132
|
+
const grandParent = parent.parent;
|
|
133
|
+
if (grandParent &&
|
|
134
|
+
grandParent.type === "JSXExpressionContainer" &&
|
|
135
|
+
grandParent.parent &&
|
|
136
|
+
grandParent.parent.type === "JSXAttribute") {
|
|
137
|
+
const attr = grandParent.parent;
|
|
138
|
+
if (attr.name.name === "className" || attr.name.name === "class") {
|
|
139
|
+
const logicalValue = replaceClassesInString(value);
|
|
140
|
+
if (logicalValue !== value) {
|
|
141
|
+
context.report({
|
|
142
|
+
node,
|
|
143
|
+
message: "Use logical Tailwind CSS classes instead of physical ones for better RTL support",
|
|
144
|
+
fix(fixer) {
|
|
145
|
+
return fixer.replaceText(node, logicalValue);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
exports.default = rule;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rtl-shield",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin to enforce CSS Logical Properties for robust RTL support",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:watch": "vitest --watch",
|
|
16
|
+
"prepublishOnly": "npm run build && npm run test -- --run"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"eslint",
|
|
20
|
+
"eslint-plugin",
|
|
21
|
+
"rtl",
|
|
22
|
+
"css-logical-properties",
|
|
23
|
+
"i18n"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/berkinduz/rtl-shield.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/berkinduz/rtl-shield/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/berkinduz/rtl-shield#readme",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"eslint": "^8.0.0 || ^9.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@typescript-eslint/utils": "^6.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@babel/eslint-parser": "^7.28.5",
|
|
43
|
+
"@babel/preset-react": "^7.28.5",
|
|
44
|
+
"@types/eslint": "^9.6.1",
|
|
45
|
+
"@types/node": "^20.0.0",
|
|
46
|
+
"eslint": "^8.0.0",
|
|
47
|
+
"espree": "^11.0.0",
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"vitest": "^1.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|