@runtimestudio/tailwind-sort-php 0.2.1 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 The Trustee for G&J Sevastos Family Trust t/a Runtime Studio
3
+ Copyright (c) 2026 G&J Sevastos Family Trust t/a Runtime Studio
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -18,6 +18,15 @@ values, using a real PHP-aware lexer, and leaves everything else byte-identical.
18
18
  - **Zero-config** — reads `tailwindStylesheet` from your Prettier config, the same source of truth that
19
19
  `prettier-plugin-tailwindcss` uses.
20
20
 
21
+ ## Contents
22
+
23
+ - [Requirements](#requirements) · [Install](#install) · [Setup](#setup) · [Usage](#usage)
24
+ - [Editor integration](#editor-integration) — sort-on-save & pre-commit gate
25
+ - [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations) — the per-file opt-in
26
+ - [WordPress themes & plugins](#wordpress-themes--plugins)
27
+ - [How it handles mixed templates](#how-it-handles-mixed-templates) · [Programmatic API](#programmatic-api) · [Known limitations](#known-limitations)
28
+ - [Development](#development) · [License](#license)
29
+
21
30
  ## Requirements
22
31
 
23
32
  - **Node ≥ 22.18**, or **Bun** — both run the CLI and the programmatic API.
@@ -44,8 +53,8 @@ vocabulary. Any config format Prettier supports works (`.prettierrc`, `prettier.
44
53
 
45
54
  ```js
46
55
  export default {
47
- plugins: ['prettier-plugin-tailwindcss'],
48
- tailwindStylesheet: './resources/css/main.css',
56
+ plugins: ['prettier-plugin-tailwindcss'],
57
+ tailwindStylesheet: './resources/css/main.css',
49
58
  };
50
59
  ```
51
60
 
@@ -75,18 +84,24 @@ npx tailwind-sort-php init
75
84
 
76
85
  ### Options
77
86
 
78
- | Flag | Description |
79
- |-----------------------|---------------------------------------------------------------------------------------------------|
80
- | `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config. |
81
- | `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config. |
82
- | `--check` | Don't write; exit 1 if any file needs sorting. |
83
- | `--no-short-tags` | Don't treat bare `<?` as a PHP open tag. |
87
+ | Flag | Description |
88
+ |-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
89
+ | `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config. |
90
+ | `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config. |
91
+ | `--php-source <glob>` | Also sort class strings in PHP declarations in matching files (repeatable). Merged with `tailwindPhpSources`. See [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations). |
92
+ | `--check` | Don't write; exit 1 if any file needs sorting. |
93
+ | `--no-short-tags` | Don't treat bare `<?` as a PHP open tag. |
94
+ | `-h, --help` | Show usage. |
95
+ | `--version` | Print the version. |
84
96
 
85
97
  Default globs are all `.php` files under the cwd; `node_modules`, `vendor`, `dist`, and `.git` are always skipped.
86
98
 
87
99
  ## Editor integration
88
100
 
89
- No IDE plugin is needed — two small setups cover the common workflows.
101
+ No IDE plugin is needed — two small setups cover the common workflows: sort-on-save and a pre-commit gate.
102
+
103
+ <details>
104
+ <summary><b>Sort on save</b> (PhpStorm / IntelliJ, VS Code) and a <b>pre-commit gate</b> — click to expand</summary>
90
105
 
91
106
  ### Sort on save (PhpStorm / IntelliJ)
92
107
 
@@ -107,14 +122,14 @@ to `.vscode/settings.json`:
107
122
 
108
123
  ```json
109
124
  {
110
- "emeraldwalk.runonsave": {
111
- "commands": [
112
- {
113
- "match": "\\.php$",
114
- "cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
115
- }
116
- ]
117
- }
125
+ "emeraldwalk.runonsave": {
126
+ "commands": [
127
+ {
128
+ "match": "\\.php$",
129
+ "cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
130
+ }
131
+ ]
132
+ }
118
133
  }
119
134
  ```
120
135
 
@@ -146,11 +161,103 @@ git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 ./node
146
161
 
147
162
  In CI there's no staged diff — just sweep the whole project with `npx tailwind-sort-php --check`.
148
163
 
164
+ </details>
165
+
166
+ ## Sorting classes in PHP declarations
167
+
168
+ By default, the tool only sorts classes inside HTML `class="..."` attributes. Class strings declared in **PHP itself** —
169
+ constants, static properties, config arrays — sit inside PHP code the tool treats as opaque, so they're left alone.
170
+
171
+ Opt in **per file** by listing the files whose PHP string values are Tailwind class lists, via `tailwindPhpSources` in
172
+ your Prettier config (or the repeatable `--php-source <glob>` flag):
173
+
174
+ ```js
175
+ export default {
176
+ plugins: ['prettier-plugin-tailwindcss'],
177
+ tailwindStylesheet: './resources/css/main.css',
178
+ tailwindPhpSources: ['src/classes/*.php'],
179
+ };
180
+ ```
181
+
182
+ With this set, every **string value** in a matched file is sorted with the exact same engine and order as the HTML
183
+ side. In `key => value` arrays only the **value** is sorted — keys are never touched:
184
+
185
+ ```php
186
+ // before
187
+ public const array VARIANTS = array(
188
+ 'primary' => 'text-white px-4 bg-blue-600 rounded py-2',
189
+ 'secondary' => 'text-gray-900 px-4 bg-gray-100 rounded py-2',
190
+ );
191
+
192
+ // after
193
+ public const array VARIANTS = array(
194
+ 'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
195
+ 'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
196
+ );
197
+ ```
198
+
199
+ Scalar declarations work the same way (`const string CARD = '...'`, `static $x = '...'`, `$x = '...'`), as do nested
200
+ and list-style arrays.
201
+
202
+ > **⚠️ Point `tailwindPhpSources` only at files whose string values are all Tailwind class lists.** The tool does
203
+ > **not** guess whether a string "looks like" classes — within a matched file it sorts **every** eligible string value.
204
+ > Aimed at a general file, it **will** reorder the words inside non-class strings (labels, URLs, SQL). This is a
205
+ > deliberate design contract, not a bug: safety comes from your file-level opt-in, which is why a dedicated
206
+ > directory of class-holder files (e.g. `src/classes/`) is the intended target.
207
+
208
+ The opt-in is **inert at runtime** — it lives in formatter config only, never in your source. Your PHP stays vanilla,
209
+ with zero coupling to this tool (no marker comments, no helper functions, no attributes).
210
+
211
+ **Skipped automatically** (left byte-identical, even in a matched file):
212
+
213
+ - Concatenated literals (`'btn-' . $variant`) — a fragment joined to dynamic code, unsafe to reorder.
214
+ - Interpolated double-quoted strings (`"p-4 {$dynamic} flex"`).
215
+ - Heredoc/nowdoc and backtick (shell-exec) strings, and strings containing escape sequences.
216
+
217
+ **Off by default:** without `tailwindPhpSources` (and `--php-source`), behavior is identical to 0.2.x — no PHP
218
+ declaration is ever touched.
219
+
220
+ ## WordPress themes & plugins
221
+
222
+ Most WordPress sorting needs **no opt-in at all**. Template files and partials output markup, and the `class="..."`
223
+ in that markup is sorted by the default HTML pass — even when the value is interrupted by PHP:
224
+
225
+ ```php
226
+ <article <?php post_class( 'z-10 flex' ); ?>>
227
+ <h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight">
228
+ ```
229
+
230
+ `tailwindPhpSources` is only for classes you store in **PHP values** — a variant map, a config array, theme defaults.
231
+ For that, **don't opt in a general partial.** Partials are full of non-class strings — `__()` translations,
232
+ `get_template_part()` names, query args, URLs — and an opted-in file sorts _every_ multi-word string value. The Tailwind
233
+ sorter leaves most prose alone (unknown words keep their order), but it **will** reorder any string containing words
234
+ that are also utilities (`grid`, `block`, `flex`, `hidden`, `container`, `table`, …), so `'Switch to grid view'`
235
+ becomes `'Switch to view grid'`.
236
+
237
+ Instead, keep class maps in a **dedicated file** whose every value is a class list, and opt in only that file:
238
+
239
+ ```php
240
+ // inc/ui-classes.php → tailwindPhpSources: ['inc/ui-classes.php']
241
+ return array(
242
+ 'button' => array(
243
+ 'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
244
+ 'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
245
+ ),
246
+ 'card' => 'rounded-lg border bg-white p-6 shadow-sm',
247
+ );
248
+ ```
249
+
250
+ `require` that map from your partials. The map file is 100% class strings (safe to sort); the partials stay out of
251
+ `tailwindPhpSources` and get their markup sorted by the HTML pass as usual.
252
+
149
253
  ## How it handles mixed templates
150
254
 
151
255
  PHP islands inside a class attribute are treated as opaque atoms that never move. Static text between islands is sorted
152
256
  independently — the same model the official plugin uses for `${}` interpolations in template literals.
153
257
 
258
+ <details>
259
+ <summary>Glued-fragment pinning, whitespace handling, and the full edge-case list — click to expand</summary>
260
+
154
261
  ```php
155
262
  <!-- before -->
156
263
  <h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight leading-snug">
@@ -173,8 +280,12 @@ Also handled correctly:
173
280
  - `?>` inside `//` and `#` line comments (island ends — genuine PHP behavior)
174
281
  - `#[Attributes]`, `<?PHP` case-insensitivity, `<?xml` exclusion, files ending in PHP mode
175
282
  - PHP islands as standalone attributes: `<div <?php post_class(); ?> class="...">`
176
- - `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (sorting
177
- `class="..."` inside PHP string literals could be added later as an opt-in)
283
+ - With `tailwindPhpSources`, string values inside an island in a class attribute
284
+ (`class="p-4 <?= $on ? 'flex z-10' : '' ?>"`) sort in the same single pass
285
+ - `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (to sort class
286
+ strings declared in PHP, see [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations))
287
+
288
+ </details>
178
289
 
179
290
  ## Programmatic API
180
291
 
@@ -190,6 +301,9 @@ const out = transform(source, sortFn);
190
301
 
191
302
  ## Known limitations
192
303
 
304
+ <details>
305
+ <summary>Edge cases and unsupported syntax — click to expand</summary>
306
+
193
307
  - Complex string interpolation containing double quotes (`"{$arr["key"]}"`) can desync the PHP string lexer in rare
194
308
  cases. Use `{$arr['key']}` style or extract to a variable.
195
309
  - Unquoted attribute values (`class=foo`) are skipped.
@@ -197,6 +311,8 @@ const out = transform(source, sortFn);
197
311
  classes).
198
312
  - Whitespace inside multi-line class attributes is normalized to single spaces (matches Prettier behavior).
199
313
 
314
+ </details>
315
+
200
316
  ## Development
201
317
 
202
318
  ```sh
@@ -204,10 +320,11 @@ bun test # or: node --test "test/*.test.ts"
204
320
  bun run build # compile src → dist (tsc); the published artifact
205
321
  ```
206
322
 
207
- 54 tests: 41 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`), 5
208
- integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the Tailwind
209
- toolchain isn't installed, and 8 `init` tests that run against throwaway git repositories and skip when `git` is
210
- unavailable.
323
+ 84 tests: 63 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`),
324
+ 7 integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the
325
+ Tailwind toolchain isn't installed, 8 `init` tests that run against throwaway git repositories and skip when
326
+ `git` is unavailable, and 6 CLI tests covering argument validation, help/version output, and file-scanning rules
327
+ (the scan test also skips without the Tailwind toolchain).
211
328
 
212
329
  ## License
213
330
 
package/dist/cli.d.ts CHANGED
@@ -1,18 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * tailwind-sort-php CLI
3
+ * tailwind-sort-php CLI — see `USAGE` for the flag reference.
4
4
  *
5
- * Usage:
6
- * tailwind-sort-php [options] [glob ...]
7
- * tailwind-sort-php init [--fix] [--force] [--dry-run]
8
- *
9
- * Options:
10
- * --stylesheet <path> Tailwind v4 CSS entry
11
- * --attr <name> Extra attribute to sort (repeatable)
12
- * --check Don't write; exit 1 if any file needs sorting
13
- * --no-short-tags Don't treat bare `<?` as a PHP open tag
14
- *
15
- * Defaults to all `.php` files under `cwd` when no globs are given.
16
- * Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
5
+ * Option values fall back to the resolved Prettier config — the same source of truth `prettier-plugin-tailwindcss`
6
+ * uses; see `fromPrettierConfig()`.
7
+ * The `init` subcommand installs the pre-commit hook; see `init.ts`.
17
8
  */
18
9
  export {};
package/dist/cli.js CHANGED
@@ -1,24 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * tailwind-sort-php CLI
3
+ * tailwind-sort-php CLI — see `USAGE` for the flag reference.
4
4
  *
5
- * Usage:
6
- * tailwind-sort-php [options] [glob ...]
7
- * tailwind-sort-php init [--fix] [--force] [--dry-run]
8
- *
9
- * Options:
10
- * --stylesheet <path> Tailwind v4 CSS entry
11
- * --attr <name> Extra attribute to sort (repeatable)
12
- * --check Don't write; exit 1 if any file needs sorting
13
- * --no-short-tags Don't treat bare `<?` as a PHP open tag
14
- *
15
- * Defaults to all `.php` files under `cwd` when no globs are given.
16
- * Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
5
+ * Option values fall back to the resolved Prettier config — the same source of truth `prettier-plugin-tailwindcss`
6
+ * uses; see `fromPrettierConfig()`.
7
+ * The `init` subcommand installs the pre-commit hook; see `init.ts`.
17
8
  */
18
9
  import { readFile, writeFile } from 'node:fs/promises';
19
10
  import { transform } from "./transform.js";
20
11
  import { createTailwindSortFn } from "./sorter.js";
21
12
  import { runInit } from "./init.js";
13
+ const USAGE = `Usage:
14
+ tailwind-sort-php [options] [glob ...]
15
+ tailwind-sort-php init [--fix] [--force] [--dry-run]
16
+
17
+ Options:
18
+ --stylesheet <path> Tailwind v4 CSS entry (default: tailwindStylesheet from your Prettier config)
19
+ --attr <name> Extra attribute to sort (repeatable; merged with tailwindAttributes)
20
+ --php-source <glob> Also sort class strings in PHP declarations in matching files
21
+ (repeatable; merged with tailwindPhpSources)
22
+ --check Don't write; exit 1 if any file needs sorting
23
+ --no-short-tags Don't treat bare <? as a PHP open tag
24
+ -h, --help Show this help
25
+ --version Print the version
26
+
27
+ Defaults to all .php files under the cwd when no globs are given;
28
+ node_modules, vendor, dist and .git are always skipped.`;
22
29
  const IGNORE = ['node_modules', 'vendor', 'dist', '.git'];
23
30
  /**
24
31
  * Parse command-line arguments.
@@ -30,21 +37,24 @@ function parseArgs(argv) {
30
37
  const cli = {
31
38
  globs: [],
32
39
  attrs: ['class', 'className'],
40
+ phpSources: [],
33
41
  check: false,
34
42
  shortTags: true,
35
43
  };
36
44
  for (let i = 0; i < argv.length; i++) {
37
45
  const a = argv[i];
38
- if (a === '--check')
46
+ if (a === '--stylesheet')
47
+ cli.stylesheet = requireValue(argv, ++i, a);
48
+ else if (a === '--attr')
49
+ cli.attrs.push(requireValue(argv, ++i, a));
50
+ else if (a === '--php-source')
51
+ cli.phpSources.push(requireValue(argv, ++i, a));
52
+ else if (a === '--check')
39
53
  cli.check = true;
40
54
  else if (a === '--no-short-tags')
41
55
  cli.shortTags = false;
42
- else if (a === '--stylesheet')
43
- cli.stylesheet = argv[++i];
44
- else if (a === '--attr')
45
- cli.attrs.push(argv[++i]);
46
56
  else if (a.startsWith('--')) {
47
- console.error(`Unknown option: ${a}`);
57
+ console.error(`Unknown option: ${a}\n\n${USAGE}`);
48
58
  process.exit(2);
49
59
  }
50
60
  else
@@ -54,6 +64,17 @@ function parseArgs(argv) {
54
64
  cli.globs.push('**/*.php');
55
65
  return cli;
56
66
  }
67
+ /**
68
+ * Return a flag's value argument or exit with a usage error when it is missing or is itself a flag.
69
+ */
70
+ function requireValue(argv, i, flag) {
71
+ const v = argv[i];
72
+ if (v === undefined || v.startsWith('--')) {
73
+ console.error(`Missing value for ${flag}`);
74
+ process.exit(2);
75
+ }
76
+ return v;
77
+ }
57
78
  /**
58
79
  * Yield file paths matching the given globs, using `Bun.Glob` under Bun and `node:fs` glob (Node >= 22) otherwise.
59
80
  *
@@ -69,8 +90,10 @@ async function* scanFiles(globs) {
69
90
  }
70
91
  else {
71
92
  const { glob } = await import('node:fs/promises');
93
+ // Prunes ignored directories during traversal; newer Node versions pass a Dirent instead of a path string.
94
+ const exclude = (entry) => ignored(typeof entry === 'string' ? entry : entry.name);
72
95
  for (const pattern of globs) {
73
- for await (const f of glob(pattern))
96
+ for await (const f of glob(pattern, { exclude }))
74
97
  yield f;
75
98
  }
76
99
  }
@@ -81,13 +104,12 @@ async function* scanFiles(globs) {
81
104
  * @param file Path to test, relative to `cwd`.
82
105
  * @returns True when the path is inside an ignored directory and should be skipped.
83
106
  */
84
- const ignored = (file) => IGNORE.some((d) => file.includes(`${d}/`) || file.startsWith(d));
107
+ const ignored = (file) => file.split(/[\\/]/).some((seg) => IGNORE.includes(seg));
85
108
  /**
86
- * Best-effort read of the project's resolved Prettier config, so this tool shares one source of truth with
87
- * `prettier-plugin-tailwindcss`. Picks up `tailwindStylesheet` (resolved relative to the config file) and
88
- * `tailwindAttributes` (merged into the attribute list).
109
+ * Best-effort read of the resolved Prettier config the shared source of truth with `prettier-plugin-tailwindcss`.
110
+ * Picks up `tailwindStylesheet`, `tailwindAttributes`, and `tailwindPhpSources`.
89
111
  *
90
- * @returns The resolved stylesheet path and attributes, or an empty object if none are available.
112
+ * @returns The found settings, or an empty object if none are available.
91
113
  */
92
114
  async function fromPrettierConfig() {
93
115
  try {
@@ -106,6 +128,9 @@ async function fromPrettierConfig() {
106
128
  if (Array.isArray(cfg.tailwindAttributes)) {
107
129
  out.attributes = cfg.tailwindAttributes.filter((a) => typeof a === 'string' && !a.startsWith('/'));
108
130
  }
131
+ if (Array.isArray(cfg.tailwindPhpSources)) {
132
+ out.phpSources = cfg.tailwindPhpSources.filter((p) => typeof p === 'string');
133
+ }
109
134
  return out;
110
135
  }
111
136
  catch {
@@ -114,6 +139,16 @@ async function fromPrettierConfig() {
114
139
  }
115
140
  async function main() {
116
141
  const argv = process.argv.slice(2);
142
+ // Help/version take precedence everywhere, including after `init`.
143
+ if (argv.includes('--help') || argv.includes('-h')) {
144
+ console.log(USAGE);
145
+ return;
146
+ }
147
+ if (argv.includes('--version')) {
148
+ const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
149
+ console.log(pkg.version);
150
+ return;
151
+ }
117
152
  if (argv[0] === 'init')
118
153
  return runInit(argv.slice(1));
119
154
  const cli = parseArgs(argv);
@@ -135,24 +170,32 @@ async function main() {
135
170
  attributes: cli.attrs,
136
171
  shortOpenTags: cli.shortTags,
137
172
  };
173
+ // Pre-scan the opt-in `tailwindPhpSources` globs; a file gets `sortPhpStrings` only if it matches one.
174
+ const phpSources = [...cli.phpSources, ...(pc.phpSources ?? [])];
175
+ const phpSourceFiles = new Set();
176
+ for await (const file of scanFiles(phpSources)) {
177
+ if (!ignored(file))
178
+ phpSourceFiles.add(file);
179
+ }
138
180
  let scanned = 0;
139
181
  let changed = 0;
140
- for (const pattern of cli.globs) {
141
- for await (const file of scanFiles([pattern])) {
142
- if (ignored(file))
143
- continue;
144
- scanned++;
145
- const src = await readFile(file, 'utf8');
146
- const out = transform(src, sortFn, opts);
147
- if (out !== src) {
148
- changed++;
149
- if (cli.check) {
150
- console.log(`needs sorting: ${file}`);
151
- }
152
- else {
153
- await writeFile(file, out);
154
- console.log(`sorted: ${file}`);
155
- }
182
+ const seen = new Set();
183
+ // Include php-source files so a designated holder is sorted even outside the main globs; `seen` dedupes.
184
+ for await (const file of scanFiles([...cli.globs, ...phpSources])) {
185
+ if (ignored(file) || seen.has(file))
186
+ continue;
187
+ seen.add(file);
188
+ scanned++;
189
+ const src = await readFile(file, 'utf8');
190
+ const out = transform(src, sortFn, { ...opts, sortPhpStrings: phpSourceFiles.has(file) });
191
+ if (out !== src) {
192
+ changed++;
193
+ if (cli.check) {
194
+ console.log(`needs sorting: ${file}`);
195
+ }
196
+ else {
197
+ await writeFile(file, out);
198
+ console.log(`sorted: ${file}`);
156
199
  }
157
200
  }
158
201
  }
package/dist/html.js CHANGED
@@ -47,6 +47,7 @@ export function findClassAttributes(masked, opts = {}) {
47
47
  const wanted = new Set((opts.attributes ?? ['class', 'classname']).map((a) => a.toLowerCase()));
48
48
  const out = [];
49
49
  const len = masked.length;
50
+ const lower = masked.toLowerCase();
50
51
  let i = 0;
51
52
  while (i < len) {
52
53
  const lt = masked.indexOf('<', i);
@@ -75,12 +76,12 @@ export function findClassAttributes(masked, opts = {}) {
75
76
  let j = lt + 1;
76
77
  while (j < len && /[A-Za-z0-9:-]/.test(masked[j]))
77
78
  j++;
78
- const tagName = masked.slice(lt + 1, j).toLowerCase();
79
+ const tagName = lower.slice(lt + 1, j);
79
80
  j = scanTagAttributes(masked, j, wanted, out);
80
81
  // Skip raw-text element content up to its closing tag.
81
82
  if (RAW_TEXT_TAGS.has(tagName)) {
82
83
  const closer = `</${tagName}`;
83
- const idx = masked.toLowerCase().indexOf(closer, j);
84
+ const idx = lower.indexOf(closer, j);
84
85
  j = idx === -1 ? len : idx;
85
86
  }
86
87
  i = j;
package/dist/index.d.ts CHANGED
@@ -10,4 +10,5 @@
10
10
  export { transform, type SortFn, type TransformOptions } from './transform.ts';
11
11
  export { findIslands, type Island, type IslandOptions } from './islands.ts';
12
12
  export { maskIslands, findClassAttributes, type ClassAttr, type HtmlScanOptions } from './html.ts';
13
+ export { findSortablePhpStrings, type PhpStringRange } from './php-strings.ts';
13
14
  export { createTailwindSortFn, type SorterOptions } from './sorter.ts';
package/dist/index.js CHANGED
@@ -10,4 +10,5 @@
10
10
  export { transform } from "./transform.js";
11
11
  export { findIslands } from "./islands.js";
12
12
  export { maskIslands, findClassAttributes } from "./html.js";
13
+ export { findSortablePhpStrings } from "./php-strings.js";
13
14
  export { createTailwindSortFn } from "./sorter.js";
package/dist/init.js CHANGED
@@ -132,7 +132,11 @@ export async function runInit(argv) {
132
132
  let repairMode = false;
133
133
  if (current !== null && current !== hookBody) {
134
134
  if (!cli.force) {
135
- const installed = current === HOOK_CHECK ? 'the check variant' : current === HOOK_FIX ? 'the --fix variant' : 'a custom hook';
135
+ const installed = current === HOOK_CHECK
136
+ ? 'the check variant'
137
+ : current === HOOK_FIX
138
+ ? 'the --fix variant'
139
+ : 'a custom hook';
136
140
  fail(`${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`);
137
141
  }
138
142
  writeHook = true;
@@ -148,7 +152,9 @@ export async function runInit(argv) {
148
152
  await writeFile(hookAbs, hookBody);
149
153
  await chmod(hookAbs, 0o755);
150
154
  }
151
- done.push(cli.dryRun ? `would install ${HOOK_PATH} (${variant} variant)` : `installed ${HOOK_PATH} (${variant} variant)`);
155
+ done.push(cli.dryRun
156
+ ? `would install ${HOOK_PATH} (${variant} variant)`
157
+ : `installed ${HOOK_PATH} (${variant} variant)`);
152
158
  }
153
159
  if (repairMode) {
154
160
  if (!cli.dryRun)