@youthfulhps/prettier-plugin-tailwindcss-normalizer 0.4.1 → 0.5.2

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.
Files changed (3) hide show
  1. package/README.md +92 -137
  2. package/dist/normalizer.js +142 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,31 +5,26 @@
5
5
 
6
6
  A Prettier plugin that automatically normalizes Tailwind CSS arbitrary values into standard utility classes, helping maintain consistent and optimized CSS across your project.
7
7
 
8
- > **⚠️ Important**: This plugin requires **Prettier v3.0.0 or higher**. It does not support Prettier v2.
8
+ **Requires Prettier v3.0.0 or higher** (Prettier v2 is not supported)
9
9
 
10
- ## Features
10
+ ## Features
11
11
 
12
- - **Automatic Normalization**: Converts arbitrary values like `p-[4px]` to standard classes like `p-1`
13
- - **Multi-Framework Support**: Works with HTML, React/JSX, Vue.js, and Angular
14
- - **Safe Transformation**: Only transforms class attributes, leaving comments, text, and other attributes untouched
15
- - **Comprehensive Mapping**: Supports padding, margin, sizing, typography, borders, shadows, and more
16
- - **Prettier v3+ Only**: Built specifically for Prettier v3.0.0 and above (v2 not supported)
12
+ - Converts arbitrary values like `p-[4px]` to standard classes like `p-1`
13
+ - Works with HTML, React/JSX, Vue.js, and Angular
14
+ - Only transforms class attributes, leaving comments, text, and other attributes untouched
15
+ - Supports padding, margin, sizing, typography, borders, shadows, and more
16
+ - Supports all Tailwind CSS variants (responsive, state, dark mode, group, peer, etc.)
17
17
 
18
- ## 🚀 Installation
19
-
20
- **Prerequisites**: Make sure you have Prettier v3.0.0 or higher installed.
18
+ ## Installation
21
19
 
22
20
  ```bash
23
- # Install Prettier v3+ (if not already installed)
24
21
  npm install --save-dev prettier@^3.0.0
25
-
26
- # Install the plugin
27
22
  npm install --save-dev @youthfulhps/prettier-plugin-tailwindcss-normalizer
28
23
  ```
29
24
 
30
- ## 📖 Usage
25
+ ## Usage
31
26
 
32
- ### 1. Configure Prettier
27
+ ### Basic Configuration
33
28
 
34
29
  Add the plugin to your Prettier configuration:
35
30
 
@@ -49,7 +44,7 @@ module.exports = {
49
44
  };
50
45
  ```
51
46
 
52
- ### 2. Run Prettier
47
+ ### Running Prettier
53
48
 
54
49
  ```bash
55
50
  # Format all files
@@ -59,35 +54,29 @@ npx prettier --write .
59
54
  npx prettier --write src/**/*.{tsx,jsx,html,vue}
60
55
  ```
61
56
 
62
- ## 🔗 Using with Other Prettier Tailwind Plugins
63
-
64
- To use this plugin alongside other prettier tailwind plugins, you need to install and configure `prettier-plugin-merge`.
57
+ ## Using with Other Prettier Tailwind Plugins
65
58
 
66
- ### Installation
59
+ To use this plugin alongside other prettier tailwind plugins (like `prettier-plugin-tailwindcss`), install and configure `prettier-plugin-merge`:
67
60
 
68
61
  ```bash
69
62
  npm install --save-dev prettier-plugin-merge
70
63
  ```
71
64
 
72
- ### Configuration
73
-
74
- In your `.prettierrc.js` file, add `prettier-plugin-merge` as the last item in the plugins array:
65
+ **`.prettierrc.js`**
75
66
 
76
67
  ```javascript
77
68
  module.exports = {
78
69
  plugins: [
79
70
  "@youthfulhps/prettier-plugin-tailwindcss-normalizer",
80
71
  "prettier-plugin-tailwindcss",
81
- "prettier-plugin-merge",
72
+ "prettier-plugin-merge", // Must be last
82
73
  ],
83
74
  };
84
75
  ```
85
76
 
86
- > **Note**: `prettier-plugin-merge` is a plugin that enables multiple prettier plugins to work together. When using with other tailwind-related plugins, it should always be placed last in the plugins array.
77
+ ## Examples
87
78
 
88
- ## 🎯 Examples
89
-
90
- ### Before
79
+ **Before:**
91
80
 
92
81
  ```jsx
93
82
  <div className="p-[16px] m-[8px] bg-blue-500">
@@ -95,7 +84,7 @@ module.exports = {
95
84
  </div>
96
85
  ```
97
86
 
98
- ### After
87
+ **After:**
99
88
 
100
89
  ```jsx
101
90
  <div className="p-4 m-2 bg-blue-500">
@@ -103,65 +92,58 @@ module.exports = {
103
92
  </div>
104
93
  ```
105
94
 
106
- ## 🛡️ Safety Features
107
-
108
- The plugin is designed to be safe and only transforms class-related attributes:
109
-
110
- ### ✅ What Gets Transformed
111
-
112
- - `className` attributes in JSX/TSX
113
- - `class` attributes in HTML
114
- - `:class` and `v-bind:class` in Vue
115
- - `[class]` in Angular
116
- - String literals in template literals
117
- - Function calls like `clsx()`, `classnames()`, `cn()`
95
+ **With Variants:**
118
96
 
119
- ### ❌ What Stays Untouched
97
+ ```jsx
98
+ // Before
99
+ <div className="md:p-[16px] hover:m-[8px] dark:bg-[#1f2937]">
100
+ Content
101
+ </div>
120
102
 
121
- - Comments and documentation
122
- - Regular text content
123
- - Other HTML attributes (`title`, `data-*`, etc.)
124
- - JavaScript strings and variables
125
- - CSS in `<style>` tags
103
+ // After
104
+ <div className="md:p-4 hover:m-2 dark:bg-[#1f2937]">
105
+ Content
106
+ </div>
107
+ ```
126
108
 
127
- ## 📋 Supported Mappings
109
+ ## Supported Mappings
128
110
 
129
111
  ### Spacing
130
112
 
131
- - **Padding**: `p-[4px]` → `p-1`, `px-[16px]` → `px-4`
132
- - **Margin**: `m-[8px]` → `m-2`, `my-[12px]` → `my-3`
133
- - **Gap**: `gap-[8px]` → `gap-2`, `gap-x-[16px]` → `gap-x-4`
113
+ - Padding: `p-[4px]` → `p-1`, `px-[16px]` → `px-4`
114
+ - Margin: `m-[8px]` → `m-2`, `my-[12px]` → `my-3`
115
+ - Gap: `gap-[8px]` → `gap-2`, `gap-x-[16px]` → `gap-x-4`
134
116
 
135
117
  ### Sizing
136
118
 
137
- - **Width**: `w-[100px]` → `w-25`, `w-[200px]` → `w-50`
138
- - **Height**: `h-[50px]` → `h-12.5`, `h-[100px]` → `h-25`
119
+ - Width: `w-[100px]` → `w-25`, `w-[200px]` → `w-50`
120
+ - Height: `h-[50px]` → `h-12.5`, `h-[100px]` → `h-25`
139
121
 
140
122
  ### Typography
141
123
 
142
- - **Font Size**: `text-[14px]` → `text-sm`, `text-[18px]` → `text-lg`
124
+ - Font Size: `text-[14px]` → `text-sm`, `text-[18px]` → `text-lg`
125
+ - Letter Spacing: `tracking-[-0.05em]` → `tracking-tighter`
143
126
 
144
127
  ### Layout
145
128
 
146
- - **Border Radius**: `rounded-[4px]` → `rounded`, `rounded-[6px]` → `rounded-md`
147
- - **Border Width**: `border-[1px]` → `border`, `border-[2px]` → `border-2`
129
+ - Border Radius: `rounded-[4px]` → `rounded`, `rounded-[6px]` → `rounded-md`
130
+ - Border Width: `border-[1px]` → `border`, `border-[2px]` → `border-2`
148
131
 
149
132
  ### Effects
150
133
 
151
- - **Box Shadow**: `shadow-[0_1px_3px_rgba(0,0,0,0.1)]` → `shadow-sm`
152
- - **Opacity**: `opacity-[0.5]` → `opacity-50`
134
+ - Box Shadow: `shadow-[0_1px_3px_rgba(0,0,0,0.1)]` → `shadow-sm`
135
+ - Opacity: `opacity-[0.5]` → `opacity-50`
153
136
 
154
- ## 🔧 Configuration
137
+ ### Transforms
155
138
 
156
- ### Basic Configuration
139
+ - Rotate: `rotate-[-180deg]` → `-rotate-180`
140
+ - Translate: `translate-x-[-100%]` → `-translate-x-full`
157
141
 
158
- The plugin works out of the box with default settings. No additional configuration is required.
142
+ ## Configuration
159
143
 
160
144
  ### Custom Spacing Unit
161
145
 
162
- If you've customized your Tailwind CSS spacing scale, you can configure the plugin to match your custom spacing unit.
163
-
164
- By default, Tailwind uses **4px** as the base spacing unit (e.g., `p-1` = 4px, `p-2` = 8px). If you've changed this in your Tailwind configuration, you should update the `customSpacingUnit` option.
146
+ By default, Tailwind uses **4px** as the base spacing unit (e.g., `p-1` = 4px, `p-2` = 8px). If you've customized your Tailwind spacing scale, configure the `customSpacingUnit` option:
165
147
 
166
148
  **`.prettierrc.js`**
167
149
 
@@ -172,83 +154,69 @@ module.exports = {
172
154
  };
173
155
  ```
174
156
 
175
- **`tailwind.config.js` (Example with 8px base unit)**
176
-
177
- ```javascript
178
- module.exports = {
179
- theme: {
180
- extend: {
181
- spacing: {
182
- 1: "8px", // 8px * 1
183
- 2: "16px", // 8px * 2
184
- 3: "24px", // 8px * 3
185
- 4: "32px", // 8px * 4
186
- // ... etc
187
- },
188
- },
189
- },
190
- };
191
- ```
192
-
193
- **Tailwind CSS v4 (`global.css` or similar)**
194
-
195
- ```css
196
- @theme {
197
- --spacing: 1px;
198
- /* ... etc */
199
- }
200
- ```
201
-
202
- **Example with `customSpacingUnit: 8`**
157
+ **Example with `customSpacingUnit: 8`:**
203
158
 
204
159
  ```jsx
205
160
  // Before
206
161
  <div className="p-[8px] m-[16px] gap-[24px]">Content</div>
207
162
 
208
- // After (with customSpacingUnit: 8)
163
+ // After
209
164
  <div className="p-1 m-2 gap-3">Content</div>
210
165
  ```
211
166
 
212
- **Example with default `customSpacingUnit: 4`**
167
+ See the [examples/custom-spacing](./examples/custom-spacing) directory for a complete working example.
213
168
 
214
- ```jsx
215
- // Before
216
- <div className="p-[4px] m-[8px] gap-[12px]">Content</div>
169
+ ## Supported Variants
217
170
 
218
- // After (with customSpacingUnit: 4)
219
- <div className="p-1 m-2 gap-3">Content</div>
220
- ```
171
+ The plugin supports all Tailwind CSS variants:
221
172
 
222
- See the [examples/custom-spacing](./examples/custom-spacing) directory for a complete working example.
173
+ - **Responsive**: `sm:`, `md:`, `lg:`, `xl:`, `2xl:`
174
+ - **State**: `hover:`, `focus:`, `active:`, `disabled:`, etc.
175
+ - **Dark Mode**: `dark:`
176
+ - **Group/Peer**: `group-hover:`, `peer-checked:`, etc.
177
+ - **Pseudo-elements**: `before:`, `after:`, `placeholder:`, etc.
178
+ - **ARIA**: `aria-checked:`, `aria-disabled:`, etc.
179
+ - **Data Attributes**: `data-[status=active]:`
180
+ - **Arbitrary**: `[&:nth-child(3)]:`, `has-[input:focus]:`, etc.
181
+
182
+ ## Safety Features
183
+
184
+ The plugin only transforms class-related attributes:
185
+
186
+ **Transformed:**
187
+
188
+ - `className` attributes in JSX/TSX
189
+ - `class` attributes in HTML
190
+ - `:class` and `v-bind:class` in Vue
191
+ - `[class]` in Angular
192
+ - String literals in template literals
193
+ - Function calls like `clsx()`, `classnames()`, `cn()`
223
194
 
224
- > **Note**: This plugin is optimized for Prettier v3+ and takes advantage of the new plugin architecture. If you're using Prettier v2, please upgrade to v3 or use an alternative solution.
195
+ **Untouched:**
225
196
 
226
- ## 🧪 Testing
197
+ - Comments and documentation
198
+ - Regular text content
199
+ - Other HTML attributes
200
+ - JavaScript strings and variables
201
+ - CSS in `<style>` tags
227
202
 
228
- The plugin includes comprehensive tests covering:
203
+ ## File Support
229
204
 
230
- - Various file formats (HTML, JSX, TSX, Vue)
231
- - Edge cases and complex scenarios
232
- - Safety features and non-transformation cases
233
- - Different Tailwind CSS patterns
205
+ | Format | Extension |
206
+ | --------- | ----------------- |
207
+ | HTML | `.html` |
208
+ | React JSX | `.jsx` |
209
+ | React TSX | `.tsx` |
210
+ | Vue | `.vue` |
211
+ | Angular | `.component.html` |
234
212
 
235
- Run tests:
213
+ ## Testing
236
214
 
237
215
  ```bash
238
216
  npm test
239
217
  ```
240
218
 
241
- ## 📁 File Support
242
-
243
- | Format | Extension | Support |
244
- | --------- | ----------------- | ------- |
245
- | HTML | `.html` | ✅ |
246
- | React JSX | `.jsx` | ✅ |
247
- | React TSX | `.tsx` | ✅ |
248
- | Vue | `.vue` | ✅ |
249
- | Angular | `.component.html` | ✅ |
250
-
251
- ## 🤝 Contributing
219
+ ## Contributing
252
220
 
253
221
  Contributions are welcome! Please feel free to submit a Pull Request.
254
222
 
@@ -258,17 +226,11 @@ Contributions are welcome! Please feel free to submit a Pull Request.
258
226
  4. Push to the branch (`git push origin feature/amazing-feature`)
259
227
  5. Open a Pull Request
260
228
 
261
- ## 📄 License
229
+ ## License
262
230
 
263
231
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
264
232
 
265
- ## 🙏 Acknowledgments
266
-
267
- - Built for the Prettier ecosystem
268
- - Inspired by Tailwind CSS best practices
269
- - Thanks to all contributors and users
270
-
271
- ## 📞 Support
233
+ ## Support
272
234
 
273
235
  If you encounter any issues or have questions:
274
236
 
@@ -278,15 +240,8 @@ If you encounter any issues or have questions:
278
240
 
279
241
  ### Common Issues
280
242
 
281
- **Q: The plugin doesn't work with Prettier v2**
243
+ **Q: The plugin doesn't work with Prettier v2**
282
244
  A: This plugin requires Prettier v3.0.0 or higher. Please upgrade your Prettier version.
283
245
 
284
- **Q: How do I check my Prettier version?**
246
+ **Q: How do I check my Prettier version?**
285
247
  A: Run `npx prettier --version` to check your current Prettier version.
286
-
287
- **Q: Can I use this with Prettier v2?**
288
- A: No, this plugin is built specifically for Prettier v3+ and will not work with v2.
289
-
290
- ---
291
-
292
- **Made with ❤️ for the developer community**
@@ -77,18 +77,90 @@ function normalizeClassNames(classNames) {
77
77
  return classes.map((className) => normalizeClassName(className)).join(" ");
78
78
  }
79
79
  function normalizeClassName(className) {
80
+ let variantPrefix = "";
81
+ let pos = 0;
82
+ while (pos < className.length) {
83
+ if (className[pos] === "[") {
84
+ const bracketEnd = className.indexOf("]:", pos);
85
+ if (bracketEnd === -1)
86
+ break;
87
+ variantPrefix += className.substring(pos, bracketEnd + 2);
88
+ pos = bracketEnd + 2;
89
+ }
90
+ else {
91
+ const colonPos = className.indexOf(":", pos);
92
+ if (colonPos === -1)
93
+ break;
94
+ const beforeColon = className.substring(pos, colonPos);
95
+ if (beforeColon.includes("[")) {
96
+ const bracketEnd = className.indexOf("]:", pos);
97
+ if (bracketEnd === -1)
98
+ break;
99
+ variantPrefix += className.substring(pos, bracketEnd + 2);
100
+ pos = bracketEnd + 2;
101
+ }
102
+ else {
103
+ variantPrefix += className.substring(pos, colonPos + 1);
104
+ pos = colonPos + 1;
105
+ }
106
+ }
107
+ if (pos >= className.length)
108
+ break;
109
+ if (className[pos] === " ")
110
+ break;
111
+ const nextChar = className[pos];
112
+ if (!/[a-z0-9_\-\[\]]/.test(nextChar)) {
113
+ break;
114
+ }
115
+ }
116
+ const classWithoutVariant = className.substring(variantPrefix.length);
117
+ if (!variantPrefix) {
118
+ return normalizeClassNameWithoutVariant(className);
119
+ }
120
+ const normalizedClass = normalizeClassNameWithoutVariant(classWithoutVariant);
121
+ if (normalizedClass === classWithoutVariant) {
122
+ return className;
123
+ }
124
+ return variantPrefix + normalizedClass;
125
+ }
126
+ function normalizeClassNameWithoutVariant(className) {
80
127
  const negativeArbitraryValueRegex = /^-([a-z:-]+)\[([^\]]+)\]$/;
81
128
  const negativeArbitraryMatch = className.match(negativeArbitraryValueRegex);
82
129
  if (negativeArbitraryMatch) {
83
130
  const [, prefixWithDash, value] = negativeArbitraryMatch;
84
131
  const prefix = prefixWithDash.replace(/-$/, "");
132
+ const negativeAllowedPrefixes = [
133
+ "m",
134
+ "mx",
135
+ "my",
136
+ "mt",
137
+ "mr",
138
+ "mb",
139
+ "ml",
140
+ "ms",
141
+ "me",
142
+ "top",
143
+ "right",
144
+ "bottom",
145
+ "left",
146
+ "inset",
147
+ "inset-x",
148
+ "inset-y",
149
+ "gap",
150
+ "gap-x",
151
+ "gap-y",
152
+ "space-x",
153
+ "space-y",
154
+ ];
155
+ if (!negativeAllowedPrefixes.includes(prefix)) {
156
+ return className;
157
+ }
85
158
  if (value === "0px" || value === "0") {
86
159
  const mapping = findStandardMapping(prefix, value);
87
- return mapping || className;
160
+ return mapping ? `-${mapping}` : className;
88
161
  }
89
- const negativeValue = `-${value}`;
90
- const mapping = findStandardMapping(prefix, negativeValue);
91
- return mapping || className;
162
+ const mapping = findStandardMapping(prefix, value);
163
+ return mapping ? `-${mapping}` : className;
92
164
  }
93
165
  const arbitraryValueRegex = /^([a-z:-]+)\[([^\]]+)\]$/;
94
166
  const arbitraryMatch = className.match(arbitraryValueRegex);
@@ -101,8 +173,42 @@ function normalizeClassName(className) {
101
173
  const mapping = findStandardMapping(prefix, positiveValue);
102
174
  return mapping || className;
103
175
  }
104
- const mapping = findStandardMapping(prefix, value);
105
- return mapping || className;
176
+ const negativeMapping = findStandardMapping(prefix, value);
177
+ if (negativeMapping) {
178
+ return negativeMapping;
179
+ }
180
+ const negativeAllowedPrefixes = [
181
+ "m",
182
+ "mx",
183
+ "my",
184
+ "mt",
185
+ "mr",
186
+ "mb",
187
+ "ml",
188
+ "ms",
189
+ "me",
190
+ "top",
191
+ "right",
192
+ "bottom",
193
+ "left",
194
+ "inset",
195
+ "inset-x",
196
+ "inset-y",
197
+ "gap",
198
+ "gap-x",
199
+ "gap-y",
200
+ "space-x",
201
+ "space-y",
202
+ "rotate",
203
+ "translate-x",
204
+ "translate-y",
205
+ ];
206
+ if (negativeAllowedPrefixes.includes(prefix)) {
207
+ const positiveValue = value.substring(1);
208
+ const mapping = findStandardMapping(prefix, positiveValue);
209
+ return mapping ? `-${mapping}` : className;
210
+ }
211
+ return className;
106
212
  }
107
213
  const mapping = findStandardMapping(prefix, value);
108
214
  return mapping || className;
@@ -111,13 +217,39 @@ function normalizeClassName(className) {
111
217
  const negativePxMatch = className.match(negativePxSuffixRegex);
112
218
  if (negativePxMatch) {
113
219
  const [, prefix, numValue] = negativePxMatch;
220
+ const negativeAllowedPrefixes = [
221
+ "m",
222
+ "mx",
223
+ "my",
224
+ "mt",
225
+ "mr",
226
+ "mb",
227
+ "ml",
228
+ "ms",
229
+ "me",
230
+ "top",
231
+ "right",
232
+ "bottom",
233
+ "left",
234
+ "inset",
235
+ "inset-x",
236
+ "inset-y",
237
+ "gap",
238
+ "gap-x",
239
+ "gap-y",
240
+ "space-x",
241
+ "space-y",
242
+ ];
243
+ if (!negativeAllowedPrefixes.includes(prefix)) {
244
+ return className;
245
+ }
114
246
  if (numValue === "0") {
115
247
  const mapping = findStandardMapping(prefix, "0px");
116
- return mapping || className;
248
+ return mapping ? `-${mapping}` : className;
117
249
  }
118
- const negativePixelValue = `-${numValue}px`;
119
- const mapping = findStandardMapping(prefix, negativePixelValue);
120
- return mapping || className;
250
+ const pixelValue = `${numValue}px`;
251
+ const mapping = findStandardMapping(prefix, pixelValue);
252
+ return mapping ? `-${mapping}` : className;
121
253
  }
122
254
  const pxSuffixRegex = /^([a-z:-]+)-(\d+)px$/;
123
255
  const pxMatch = className.match(pxSuffixRegex);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youthfulhps/prettier-plugin-tailwindcss-normalizer",
3
- "version": "0.4.1",
3
+ "version": "0.5.2",
4
4
  "description": "A Prettier plugin that normalizes Tailwind CSS arbitrary values into standard utility classes.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",