prettier-plugin-bootstrap 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 +104 -0
- package/dist/class-order.d.ts +6 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +413 -0
- package/dist/sorting.d.ts +1 -0
- package/dist/traversal.d.ts +2 -0
- package/dist/types.d.ts +6 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pier Luigi Lenoci
|
|
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,104 @@
|
|
|
1
|
+
# prettier-plugin-bootstrap
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pierluigilenoci/prettier-plugin-bootstrap/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/prettier-plugin-bootstrap)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A [Prettier](https://prettier.io/) plugin that automatically sorts Bootstrap CSS classes following the framework's recommended order.
|
|
8
|
+
|
|
9
|
+
Works with **HTML**, **JSX/TSX**, **Vue**, **Angular**, and **Astro** templates.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -D prettier-plugin-bootstrap
|
|
15
|
+
# or
|
|
16
|
+
pnpm add -D prettier-plugin-bootstrap
|
|
17
|
+
# or
|
|
18
|
+
yarn add -D prettier-plugin-bootstrap
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Add the plugin to your Prettier configuration:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"plugins": ["prettier-plugin-bootstrap"]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it! Your Bootstrap classes will now be automatically sorted on format.
|
|
32
|
+
|
|
33
|
+
### Before
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<div class="text-center p-3 container bg-primary text-white mb-4 rounded"></div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### After
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div class="container bg-primary text-center text-white mb-4 p-3 rounded"></div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Sorting Order
|
|
46
|
+
|
|
47
|
+
Classes are sorted following Bootstrap's architecture:
|
|
48
|
+
|
|
49
|
+
1. **Layout** — containers, grid, columns
|
|
50
|
+
2. **Reboot / Typography** — headings, lead, display
|
|
51
|
+
3. **Images** — img-fluid, figures
|
|
52
|
+
4. **Tables** — table variants
|
|
53
|
+
5. **Forms** — form controls, selects, checks
|
|
54
|
+
6. **Buttons** — btn variants
|
|
55
|
+
7. **Components** — dropdowns, navs, cards, modals, etc. (alphabetical)
|
|
56
|
+
8. **Helpers** — clearfix, stacks, visually-hidden
|
|
57
|
+
9. **Utilities** — following the order in `scss/_utilities.scss`
|
|
58
|
+
|
|
59
|
+
Within each group, responsive variants (`sm`, `md`, `lg`, `xl`, `xxl`) sort after the base class.
|
|
60
|
+
|
|
61
|
+
Unknown classes are preserved in their original relative order and placed after all known Bootstrap classes.
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
| Option | Type | Default | Description |
|
|
66
|
+
|--------|------|---------|-------------|
|
|
67
|
+
| `bootstrapAttributes` | `string[]` | `[]` | Additional HTML attributes to sort (beyond `class` and `className`) |
|
|
68
|
+
| `bootstrapFunctions` | `string[]` | `[]` | Function names whose arguments are class lists (e.g. `clsx`, `classNames`) |
|
|
69
|
+
|
|
70
|
+
### Example
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"plugins": ["prettier-plugin-bootstrap"],
|
|
75
|
+
"bootstrapAttributes": ["ngClass", "v-bind:class"]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Supported Parsers
|
|
80
|
+
|
|
81
|
+
- `html` — HTML files
|
|
82
|
+
- `vue` — Vue single-file components
|
|
83
|
+
- `angular` — Angular templates
|
|
84
|
+
- `babel` / `babel-ts` / `typescript` — JSX/TSX files
|
|
85
|
+
- `acorn` / `meriyah` — Alternative JS parsers
|
|
86
|
+
- `astro` — Astro components (requires `prettier-plugin-astro`)
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pnpm install
|
|
92
|
+
pnpm run build
|
|
93
|
+
pnpm run test
|
|
94
|
+
pnpm run typecheck
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Compatibility
|
|
98
|
+
|
|
99
|
+
- Prettier >= 3.0.0
|
|
100
|
+
- Node.js >= 20
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SortKey } from './types';
|
|
2
|
+
export declare const BREAKPOINTS: readonly ["sm", "md", "lg", "xl", "xxl"];
|
|
3
|
+
export declare const CLASS_ORDER: readonly string[];
|
|
4
|
+
export declare const ORDER_MAP: Map<string, number>;
|
|
5
|
+
export declare function classKey(className: string): SortKey;
|
|
6
|
+
export declare function sortClasses(classes: string[]): string[];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Parser } from 'prettier';
|
|
2
|
+
export declare const options: {
|
|
3
|
+
bootstrapAttributes: {
|
|
4
|
+
type: "string";
|
|
5
|
+
array: boolean;
|
|
6
|
+
default: {
|
|
7
|
+
value: never[];
|
|
8
|
+
}[];
|
|
9
|
+
category: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
bootstrapFunctions: {
|
|
13
|
+
type: "string";
|
|
14
|
+
array: boolean;
|
|
15
|
+
default: {
|
|
16
|
+
value: never[];
|
|
17
|
+
}[];
|
|
18
|
+
category: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare const parsers: Record<string, Partial<Parser>>;
|
|
23
|
+
export type { BootstrapPluginOptions } from './types';
|
|
24
|
+
export { sortClasses, classKey, CLASS_ORDER, BREAKPOINTS } from './class-order';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
//#region src/class-order.ts
|
|
2
|
+
const BREAKPOINTS = [
|
|
3
|
+
"sm",
|
|
4
|
+
"md",
|
|
5
|
+
"lg",
|
|
6
|
+
"xl",
|
|
7
|
+
"xxl"
|
|
8
|
+
];
|
|
9
|
+
const RESPONSIVE_RE = new RegExp(`^(.+?)-(${BREAKPOINTS.join("|")})-(.+)$`);
|
|
10
|
+
const CLASS_ORDER = [
|
|
11
|
+
"container-fluid",
|
|
12
|
+
"container-sm",
|
|
13
|
+
"container-md",
|
|
14
|
+
"container-lg",
|
|
15
|
+
"container-xl",
|
|
16
|
+
"container-xxl",
|
|
17
|
+
"container",
|
|
18
|
+
"row",
|
|
19
|
+
"row-cols-",
|
|
20
|
+
"col-auto",
|
|
21
|
+
"col-1",
|
|
22
|
+
"col-2",
|
|
23
|
+
"col-3",
|
|
24
|
+
"col-4",
|
|
25
|
+
"col-5",
|
|
26
|
+
"col-6",
|
|
27
|
+
"col-7",
|
|
28
|
+
"col-8",
|
|
29
|
+
"col-9",
|
|
30
|
+
"col-10",
|
|
31
|
+
"col-11",
|
|
32
|
+
"col-12",
|
|
33
|
+
"col",
|
|
34
|
+
"offset-",
|
|
35
|
+
"g-",
|
|
36
|
+
"gx-",
|
|
37
|
+
"gy-",
|
|
38
|
+
"h1",
|
|
39
|
+
"h2",
|
|
40
|
+
"h3",
|
|
41
|
+
"h4",
|
|
42
|
+
"h5",
|
|
43
|
+
"h6",
|
|
44
|
+
"lead",
|
|
45
|
+
"display-",
|
|
46
|
+
"list-unstyled",
|
|
47
|
+
"list-inline",
|
|
48
|
+
"list-inline-item",
|
|
49
|
+
"initialism",
|
|
50
|
+
"blockquote",
|
|
51
|
+
"blockquote-footer",
|
|
52
|
+
"img-fluid",
|
|
53
|
+
"img-thumbnail",
|
|
54
|
+
"figure",
|
|
55
|
+
"figure-img",
|
|
56
|
+
"figure-caption",
|
|
57
|
+
"table",
|
|
58
|
+
"table-",
|
|
59
|
+
"caption-top",
|
|
60
|
+
"form-label",
|
|
61
|
+
"col-form-label",
|
|
62
|
+
"form-text",
|
|
63
|
+
"form-control",
|
|
64
|
+
"form-control-",
|
|
65
|
+
"form-select",
|
|
66
|
+
"form-select-",
|
|
67
|
+
"form-check",
|
|
68
|
+
"form-check-",
|
|
69
|
+
"form-switch",
|
|
70
|
+
"form-floating",
|
|
71
|
+
"form-range",
|
|
72
|
+
"input-group",
|
|
73
|
+
"input-group-",
|
|
74
|
+
"valid-feedback",
|
|
75
|
+
"valid-tooltip",
|
|
76
|
+
"invalid-feedback",
|
|
77
|
+
"invalid-tooltip",
|
|
78
|
+
"was-validated",
|
|
79
|
+
"btn",
|
|
80
|
+
"btn-",
|
|
81
|
+
"btn-close",
|
|
82
|
+
"btn-close-",
|
|
83
|
+
"fade",
|
|
84
|
+
"collapse",
|
|
85
|
+
"collapsing",
|
|
86
|
+
"show",
|
|
87
|
+
"dropdown",
|
|
88
|
+
"dropdown-",
|
|
89
|
+
"dropup",
|
|
90
|
+
"dropend",
|
|
91
|
+
"dropstart",
|
|
92
|
+
"btn-group",
|
|
93
|
+
"btn-group-",
|
|
94
|
+
"btn-toolbar",
|
|
95
|
+
"nav",
|
|
96
|
+
"nav-",
|
|
97
|
+
"tab-content",
|
|
98
|
+
"tab-pane",
|
|
99
|
+
"navbar",
|
|
100
|
+
"navbar-",
|
|
101
|
+
"card",
|
|
102
|
+
"card-",
|
|
103
|
+
"accordion",
|
|
104
|
+
"accordion-",
|
|
105
|
+
"breadcrumb",
|
|
106
|
+
"breadcrumb-item",
|
|
107
|
+
"pagination",
|
|
108
|
+
"pagination-",
|
|
109
|
+
"page-item",
|
|
110
|
+
"page-link",
|
|
111
|
+
"badge",
|
|
112
|
+
"alert",
|
|
113
|
+
"alert-",
|
|
114
|
+
"progress",
|
|
115
|
+
"progress-",
|
|
116
|
+
"progress-bar",
|
|
117
|
+
"progress-bar-",
|
|
118
|
+
"list-group",
|
|
119
|
+
"list-group-",
|
|
120
|
+
"toast",
|
|
121
|
+
"toast-",
|
|
122
|
+
"modal",
|
|
123
|
+
"modal-",
|
|
124
|
+
"tooltip",
|
|
125
|
+
"tooltip-",
|
|
126
|
+
"popover",
|
|
127
|
+
"popover-",
|
|
128
|
+
"carousel",
|
|
129
|
+
"carousel-",
|
|
130
|
+
"spinner-border",
|
|
131
|
+
"spinner-border-",
|
|
132
|
+
"spinner-grow",
|
|
133
|
+
"spinner-grow-",
|
|
134
|
+
"offcanvas",
|
|
135
|
+
"offcanvas-",
|
|
136
|
+
"placeholder",
|
|
137
|
+
"placeholder-",
|
|
138
|
+
"clearfix",
|
|
139
|
+
"link-",
|
|
140
|
+
"icon-link",
|
|
141
|
+
"icon-link-",
|
|
142
|
+
"ratio",
|
|
143
|
+
"ratio-",
|
|
144
|
+
"fixed-top",
|
|
145
|
+
"fixed-bottom",
|
|
146
|
+
"sticky-top",
|
|
147
|
+
"sticky-bottom",
|
|
148
|
+
"hstack",
|
|
149
|
+
"vstack",
|
|
150
|
+
"stretched-link",
|
|
151
|
+
"text-truncate",
|
|
152
|
+
"vr",
|
|
153
|
+
"visually-hidden",
|
|
154
|
+
"visually-hidden-focusable",
|
|
155
|
+
"align-",
|
|
156
|
+
"float-",
|
|
157
|
+
"object-fit-",
|
|
158
|
+
"opacity-",
|
|
159
|
+
"overflow-",
|
|
160
|
+
"d-",
|
|
161
|
+
"shadow",
|
|
162
|
+
"shadow-",
|
|
163
|
+
"focus-ring",
|
|
164
|
+
"focus-ring-",
|
|
165
|
+
"position-",
|
|
166
|
+
"top-",
|
|
167
|
+
"bottom-",
|
|
168
|
+
"start-",
|
|
169
|
+
"end-",
|
|
170
|
+
"translate-middle",
|
|
171
|
+
"translate-middle-",
|
|
172
|
+
"border",
|
|
173
|
+
"border-",
|
|
174
|
+
"w-",
|
|
175
|
+
"mw-",
|
|
176
|
+
"vw-",
|
|
177
|
+
"min-vw-",
|
|
178
|
+
"h-",
|
|
179
|
+
"mh-",
|
|
180
|
+
"vh-",
|
|
181
|
+
"min-vh-",
|
|
182
|
+
"flex-",
|
|
183
|
+
"justify-content-",
|
|
184
|
+
"align-items-",
|
|
185
|
+
"align-content-",
|
|
186
|
+
"align-self-",
|
|
187
|
+
"order-",
|
|
188
|
+
"m-",
|
|
189
|
+
"mx-",
|
|
190
|
+
"my-",
|
|
191
|
+
"mt-",
|
|
192
|
+
"me-",
|
|
193
|
+
"mb-",
|
|
194
|
+
"ms-",
|
|
195
|
+
"p-",
|
|
196
|
+
"px-",
|
|
197
|
+
"py-",
|
|
198
|
+
"pt-",
|
|
199
|
+
"pe-",
|
|
200
|
+
"pb-",
|
|
201
|
+
"ps-",
|
|
202
|
+
"gap-",
|
|
203
|
+
"row-gap-",
|
|
204
|
+
"column-gap-",
|
|
205
|
+
"font-monospace",
|
|
206
|
+
"fs-",
|
|
207
|
+
"fst-",
|
|
208
|
+
"fw-",
|
|
209
|
+
"lh-",
|
|
210
|
+
"text-decoration-",
|
|
211
|
+
"text-",
|
|
212
|
+
"text-opacity-",
|
|
213
|
+
"link-opacity-",
|
|
214
|
+
"link-offset-",
|
|
215
|
+
"link-underline",
|
|
216
|
+
"link-underline-",
|
|
217
|
+
"bg-",
|
|
218
|
+
"bg-opacity-",
|
|
219
|
+
"bg-gradient",
|
|
220
|
+
"user-select-",
|
|
221
|
+
"pe-none",
|
|
222
|
+
"pe-auto",
|
|
223
|
+
"rounded",
|
|
224
|
+
"rounded-",
|
|
225
|
+
"visible",
|
|
226
|
+
"invisible",
|
|
227
|
+
"z-"
|
|
228
|
+
];
|
|
229
|
+
function buildOrderMap() {
|
|
230
|
+
const map = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const [index, prefix] of CLASS_ORDER.entries()) map.set(prefix, index);
|
|
232
|
+
return map;
|
|
233
|
+
}
|
|
234
|
+
const ORDER_MAP = buildOrderMap();
|
|
235
|
+
function classKey(className) {
|
|
236
|
+
let base = className;
|
|
237
|
+
let breakpointIdx = 0;
|
|
238
|
+
const match = className.match(RESPONSIVE_RE);
|
|
239
|
+
if (match) {
|
|
240
|
+
base = `${match[1]}-${match[3]}`;
|
|
241
|
+
breakpointIdx = BREAKPOINTS.indexOf(match[2]) + 1;
|
|
242
|
+
}
|
|
243
|
+
let bestIdx = -1;
|
|
244
|
+
let bestLen = 0;
|
|
245
|
+
for (const [prefix, idx] of ORDER_MAP) if (base === prefix || prefix.endsWith("-") && base.startsWith(prefix)) {
|
|
246
|
+
if (prefix.length > bestLen) {
|
|
247
|
+
bestLen = prefix.length;
|
|
248
|
+
bestIdx = idx;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return [bestIdx === -1 ? Infinity : bestIdx, breakpointIdx];
|
|
252
|
+
}
|
|
253
|
+
function sortClasses(classes) {
|
|
254
|
+
const annotated = classes.map((cls, i) => ({
|
|
255
|
+
cls,
|
|
256
|
+
key: classKey(cls),
|
|
257
|
+
orig: i
|
|
258
|
+
}));
|
|
259
|
+
annotated.sort((a, b) => {
|
|
260
|
+
if (a.key[0] !== b.key[0]) return a.key[0] - b.key[0];
|
|
261
|
+
if (a.key[1] !== b.key[1]) return a.key[1] - b.key[1];
|
|
262
|
+
return a.orig - b.orig;
|
|
263
|
+
});
|
|
264
|
+
return annotated.map((entry) => entry.cls);
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/sorting.ts
|
|
268
|
+
function sortClassString(value) {
|
|
269
|
+
if (!value || typeof value !== "string") return value;
|
|
270
|
+
const trimmed = value.trim();
|
|
271
|
+
if (!trimmed) return value;
|
|
272
|
+
const classes = trimmed.split(/\s+/);
|
|
273
|
+
if (classes.length <= 1) return value;
|
|
274
|
+
const sorted = sortClasses(classes);
|
|
275
|
+
const leadingWs = value.match(/^\s*/)[0];
|
|
276
|
+
const trailingWs = value.match(/\s*$/)[0];
|
|
277
|
+
return `${leadingWs}${sorted.join(" ")}${trailingWs}`;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/traversal.ts
|
|
281
|
+
const AST_KEYS = [
|
|
282
|
+
"program",
|
|
283
|
+
"expression",
|
|
284
|
+
"left",
|
|
285
|
+
"right",
|
|
286
|
+
"argument",
|
|
287
|
+
"callee",
|
|
288
|
+
"object",
|
|
289
|
+
"property",
|
|
290
|
+
"consequent",
|
|
291
|
+
"alternate",
|
|
292
|
+
"init",
|
|
293
|
+
"test",
|
|
294
|
+
"update",
|
|
295
|
+
"declaration",
|
|
296
|
+
"declarations",
|
|
297
|
+
"openingElement",
|
|
298
|
+
"closingElement",
|
|
299
|
+
"attributes",
|
|
300
|
+
"value",
|
|
301
|
+
"elements",
|
|
302
|
+
"properties",
|
|
303
|
+
"arguments"
|
|
304
|
+
];
|
|
305
|
+
function walk(node, visitor) {
|
|
306
|
+
if (!node) return;
|
|
307
|
+
visitor(node);
|
|
308
|
+
if (Array.isArray(node.children)) for (const child of node.children) walk(child, visitor);
|
|
309
|
+
if (node.body) {
|
|
310
|
+
if (Array.isArray(node.body)) for (const child of node.body) walk(child, visitor);
|
|
311
|
+
else if (typeof node.body === "object") walk(node.body, visitor);
|
|
312
|
+
}
|
|
313
|
+
for (const key of AST_KEYS) {
|
|
314
|
+
const child = node[key];
|
|
315
|
+
if (child) {
|
|
316
|
+
if (Array.isArray(child)) {
|
|
317
|
+
for (const item of child) if (item && typeof item === "object") walk(item, visitor);
|
|
318
|
+
} else if (typeof child === "object") walk(child, visitor);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function processHtmlAst(ast, targetAttrs) {
|
|
323
|
+
walk(ast, (node) => {
|
|
324
|
+
if (node.attrs && Array.isArray(node.attrs)) {
|
|
325
|
+
for (const attr of node.attrs) if (targetAttrs.includes(attr.name) && typeof attr.value === "string") attr.value = sortClassString(attr.value);
|
|
326
|
+
}
|
|
327
|
+
if (node.attributes && Array.isArray(node.attributes)) for (const attr of node.attributes) {
|
|
328
|
+
const name = attr.name || attr.key && attr.key.value;
|
|
329
|
+
if (targetAttrs.includes(name) && attr.value) {
|
|
330
|
+
if (typeof attr.value === "string") attr.value = sortClassString(attr.value);
|
|
331
|
+
else if (attr.value && typeof attr.value.value === "string") attr.value.value = sortClassString(attr.value.value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return ast;
|
|
336
|
+
}
|
|
337
|
+
function processJsxAst(ast, targetAttrs) {
|
|
338
|
+
walk(ast, (node) => {
|
|
339
|
+
if (node.type === "JSXAttribute" || node.type === "JSXSpreadAttribute") {
|
|
340
|
+
const name = node.name && (node.name.name || node.name.value);
|
|
341
|
+
if (targetAttrs.includes(name) && node.value) {
|
|
342
|
+
if (node.value.type === "StringLiteral" || node.value.type === "Literal") {
|
|
343
|
+
const sorted = sortClassString(node.value.value);
|
|
344
|
+
node.value.value = sorted;
|
|
345
|
+
if (node.value.extra) {
|
|
346
|
+
node.value.extra.rawValue = sorted;
|
|
347
|
+
node.value.extra.raw = `"${sorted}"`;
|
|
348
|
+
}
|
|
349
|
+
if (node.value.raw) {
|
|
350
|
+
const quote = node.value.raw[0];
|
|
351
|
+
node.value.raw = `${quote}${sorted}${quote}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
return ast;
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/index.ts
|
|
361
|
+
const DEFAULT_ATTRIBUTES = ["class", "className"];
|
|
362
|
+
const options = {
|
|
363
|
+
bootstrapAttributes: {
|
|
364
|
+
type: "string",
|
|
365
|
+
array: true,
|
|
366
|
+
default: [{ value: [] }],
|
|
367
|
+
category: "Bootstrap",
|
|
368
|
+
description: "Additional HTML attributes containing Bootstrap class lists to sort."
|
|
369
|
+
},
|
|
370
|
+
bootstrapFunctions: {
|
|
371
|
+
type: "string",
|
|
372
|
+
array: true,
|
|
373
|
+
default: [{ value: [] }],
|
|
374
|
+
category: "Bootstrap",
|
|
375
|
+
description: "Function names whose arguments are Bootstrap class lists (e.g. clsx, classNames)."
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function createParserWrapper(parserName, processAst) {
|
|
379
|
+
const wrapper = {
|
|
380
|
+
astFormat: parserName === "html" || parserName === "vue" || parserName === "angular" ? "html" : "estree",
|
|
381
|
+
async parse(text, options) {
|
|
382
|
+
const plugins = options.plugins || [];
|
|
383
|
+
let originalParser = null;
|
|
384
|
+
for (const plugin of plugins) {
|
|
385
|
+
if (typeof plugin !== "object" || !plugin.parsers) continue;
|
|
386
|
+
const candidate = plugin.parsers[parserName];
|
|
387
|
+
if (!candidate) continue;
|
|
388
|
+
let resolved = candidate;
|
|
389
|
+
if (typeof candidate === "function" && !candidate.parse) resolved = await candidate();
|
|
390
|
+
if (resolved && typeof resolved.parse === "function" && resolved.parse !== wrapper.parse) {
|
|
391
|
+
originalParser = resolved;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (!originalParser) throw new Error(`prettier-plugin-bootstrap: could not find the "${parserName}" parser. Make sure Prettier and the relevant parser plugin are installed.`);
|
|
396
|
+
return processAst(await originalParser.parse(text, options), [...DEFAULT_ATTRIBUTES, ...options.bootstrapAttributes || []]);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
return wrapper;
|
|
400
|
+
}
|
|
401
|
+
const parsers = {
|
|
402
|
+
html: createParserWrapper("html", processHtmlAst),
|
|
403
|
+
vue: createParserWrapper("vue", processHtmlAst),
|
|
404
|
+
angular: createParserWrapper("angular", processHtmlAst),
|
|
405
|
+
babel: createParserWrapper("babel", processJsxAst),
|
|
406
|
+
"babel-ts": createParserWrapper("babel-ts", processJsxAst),
|
|
407
|
+
typescript: createParserWrapper("typescript", processJsxAst),
|
|
408
|
+
acorn: createParserWrapper("acorn", processJsxAst),
|
|
409
|
+
meriyah: createParserWrapper("meriyah", processJsxAst),
|
|
410
|
+
astro: createParserWrapper("astro", processHtmlAst)
|
|
411
|
+
};
|
|
412
|
+
//#endregion
|
|
413
|
+
export { BREAKPOINTS, CLASS_ORDER, classKey, options, parsers, sortClasses };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sortClassString(value: string): string;
|
package/dist/types.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prettier-plugin-bootstrap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Prettier plugin for automatic Bootstrap class sorting",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"prettier",
|
|
21
|
+
"plugin",
|
|
22
|
+
"bootstrap",
|
|
23
|
+
"css",
|
|
24
|
+
"class",
|
|
25
|
+
"sorting",
|
|
26
|
+
"class-order",
|
|
27
|
+
"formatter"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/pierluigilenoci/prettier-plugin-bootstrap.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/pierluigilenoci/prettier-plugin-bootstrap/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/pierluigilenoci/prettier-plugin-bootstrap#readme",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"prettier": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"prettier": "^3.5.0",
|
|
42
|
+
"tsdown": "^0.12.0",
|
|
43
|
+
"typescript": "^5.8.0",
|
|
44
|
+
"vitest": "^3.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown && tsc -p tsconfig.build.json",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|