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 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
+ [![CI](https://github.com/pierluigilenoci/prettier-plugin-bootstrap/actions/workflows/ci.yml/badge.svg)](https://github.com/pierluigilenoci/prettier-plugin-bootstrap/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/prettier-plugin-bootstrap)](https://www.npmjs.com/package/prettier-plugin-bootstrap)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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[];
@@ -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;
@@ -0,0 +1,2 @@
1
+ export declare function processHtmlAst(ast: any, targetAttrs: string[]): any;
2
+ export declare function processJsxAst(ast: any, targetAttrs: string[]): any;
@@ -0,0 +1,6 @@
1
+ import type { Options } from 'prettier';
2
+ export interface BootstrapPluginOptions extends Options {
3
+ bootstrapAttributes?: string[];
4
+ bootstrapFunctions?: string[];
5
+ }
6
+ export type SortKey = [categoryIndex: number, breakpointIndex: number];
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
+ }