@runtimestudio/tailwind-sort-php 0.2.0 → 0.3.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/README.md CHANGED
@@ -18,19 +18,28 @@ 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
- - **Bun**, or **Node ≥ 22.18** (native TypeScript type-stripping) — both run the CLI and the programmatic API.
32
+ - **Node ≥ 22.18**, or **Bun** — both run the CLI and the programmatic API.
24
33
  - `prettier` ≥ 3 and `prettier-plugin-tailwindcss` ≥ 0.8 (peer dependencies).
25
34
 
26
35
  ## Install
27
36
 
28
37
  ```sh
29
- # Bun
30
- bun add -D @runtimestudio/tailwind-sort-php prettier prettier-plugin-tailwindcss
31
-
32
38
  # npm
33
39
  npm install -D @runtimestudio/tailwind-sort-php prettier prettier-plugin-tailwindcss
40
+
41
+ # Bun
42
+ bun add -D @runtimestudio/tailwind-sort-php prettier prettier-plugin-tailwindcss
34
43
  ```
35
44
 
36
45
  pnpm and yarn work the same (`pnpm add -D …` / `yarn add -D …`).
@@ -38,12 +47,14 @@ pnpm and yarn work the same (`pnpm add -D …` / `yarn add -D …`).
38
47
  ## Setup
39
48
 
40
49
  Point Prettier at your Tailwind v4 entry stylesheet so both `prettier-plugin-tailwindcss` and this tool share one
41
- vocabulary. In `prettier.config.mjs`:
50
+ vocabulary. Any config format Prettier supports works (`.prettierrc`, `prettier.config.js`, a `package.json`
51
+ `"prettier"` key, …) — the tool reads the resolved config, exactly like the plugin does. For example, in
52
+ `prettier.config.mjs`:
42
53
 
43
54
  ```js
44
55
  export default {
45
- plugins: ['prettier-plugin-tailwindcss'],
46
- tailwindStylesheet: './resources/css/main.css',
56
+ plugins: ['prettier-plugin-tailwindcss'],
57
+ tailwindStylesheet: './resources/css/main.css',
47
58
  };
48
59
  ```
49
60
 
@@ -52,39 +63,43 @@ this in place, the CLI needs no flags.
52
63
 
53
64
  ## Usage
54
65
 
55
- Run with `bunx` (Bun) or `npx` (Node ≥ 22.18):
66
+ Run with `npx` (Node ≥ 22.18) or `bunx` (Bun):
56
67
 
57
68
  ```sh
58
69
  # sort every .php file under the cwd (stylesheet read from your Prettier config)
59
- bunx tailwind-sort-php
70
+ npx tailwind-sort-php
60
71
 
61
72
  # specific globs
62
- bunx tailwind-sort-php "template-parts/**/*.php" "*.php"
73
+ npx tailwind-sort-php "template-parts/**/*.php" "*.php"
63
74
 
64
75
  # CI / pre-commit — write nothing, exit 1 if anything is unsorted
65
- bunx tailwind-sort-php --check
76
+ npx tailwind-sort-php --check
66
77
 
67
78
  # explicit stylesheet (overrides the Prettier config)
68
- bunx tailwind-sort-php --stylesheet ./resources/css/main.css
79
+ npx tailwind-sort-php --stylesheet ./resources/css/main.css
69
80
 
70
81
  # one-time: install the pre-commit hook (see "Pre-commit gate" below)
71
- bunx tailwind-sort-php init
82
+ npx tailwind-sort-php init
72
83
  ```
73
84
 
74
85
  ### Options
75
86
 
76
- | Flag | Description |
77
- |-----------------------|---------------------------------------------------------------------------------------------------|
78
- | `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config. |
79
- | `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config. |
80
- | `--check` | Don't write; exit 1 if any file needs sorting. |
81
- | `--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. |
82
94
 
83
95
  Default globs are all `.php` files under the cwd; `node_modules`, `vendor`, `dist`, and `.git` are always skipped.
84
96
 
85
97
  ## Editor integration
86
98
 
87
- No IDE plugin is needed — two small setups cover the common workflows.
99
+ No IDE plugin is needed — two small setups cover the common workflows: sort-on-save and a pre-commit gate.
100
+
101
+ <details>
102
+ <summary><b>Sort on save</b> (PhpStorm / IntelliJ, VS Code) and a <b>pre-commit gate</b> — click to expand</summary>
88
103
 
89
104
  ### Sort on save (PhpStorm / IntelliJ)
90
105
 
@@ -105,14 +120,14 @@ to `.vscode/settings.json`:
105
120
 
106
121
  ```json
107
122
  {
108
- "emeraldwalk.runonsave": {
109
- "commands": [
110
- {
111
- "match": "\\.php$",
112
- "cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
113
- }
114
- ]
115
- }
123
+ "emeraldwalk.runonsave": {
124
+ "commands": [
125
+ {
126
+ "match": "\\.php$",
127
+ "cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
128
+ }
129
+ ]
130
+ }
116
131
  }
117
132
  ```
118
133
 
@@ -144,11 +159,103 @@ git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 ./node
144
159
 
145
160
  In CI there's no staged diff — just sweep the whole project with `npx tailwind-sort-php --check`.
146
161
 
162
+ </details>
163
+
164
+ ## Sorting classes in PHP declarations
165
+
166
+ By default, the tool only sorts classes inside HTML `class="..."` attributes. Class strings declared in **PHP itself** —
167
+ constants, static properties, config arrays — sit inside PHP code the tool treats as opaque, so they're left alone.
168
+
169
+ Opt in **per file** by listing the files whose PHP string values are Tailwind class lists, via `tailwindPhpSources` in
170
+ your Prettier config (or the repeatable `--php-source <glob>` flag):
171
+
172
+ ```js
173
+ export default {
174
+ plugins: ['prettier-plugin-tailwindcss'],
175
+ tailwindStylesheet: './resources/css/main.css',
176
+ tailwindPhpSources: ['src/classes/*.php'],
177
+ };
178
+ ```
179
+
180
+ With this set, every **string value** in a matched file is sorted with the exact same engine and order as the HTML
181
+ side. In `key => value` arrays only the **value** is sorted — keys are never touched:
182
+
183
+ ```php
184
+ // before
185
+ public const array VARIANTS = array(
186
+ 'primary' => 'text-white px-4 bg-blue-600 rounded py-2',
187
+ 'secondary' => 'text-gray-900 px-4 bg-gray-100 rounded py-2',
188
+ );
189
+
190
+ // after
191
+ public const array VARIANTS = array(
192
+ 'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
193
+ 'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
194
+ );
195
+ ```
196
+
197
+ Scalar declarations work the same way (`const string CARD = '...'`, `static $x = '...'`, `$x = '...'`), as do nested
198
+ and list-style arrays.
199
+
200
+ > **⚠️ Point `tailwindPhpSources` only at files whose string values are all Tailwind class lists.** The tool does
201
+ > **not** guess whether a string "looks like" classes — within a matched file it sorts **every** eligible string value.
202
+ > Aimed at a general file, it **will** reorder the words inside non-class strings (labels, URLs, SQL). This is a
203
+ > deliberate design contract, not a bug: safety comes from your file-level opt-in, which is why a dedicated
204
+ > directory of class-holder files (e.g. `src/classes/`) is the intended target.
205
+
206
+ The opt-in is **inert at runtime** — it lives in formatter config only, never in your source. Your PHP stays vanilla,
207
+ with zero coupling to this tool (no marker comments, no helper functions, no attributes).
208
+
209
+ **Skipped automatically** (left byte-identical, even in a matched file):
210
+
211
+ - Concatenated literals (`'btn-' . $variant`) — a fragment joined to dynamic code, unsafe to reorder.
212
+ - Interpolated double-quoted strings (`"p-4 {$dynamic} flex"`).
213
+ - Heredoc/nowdoc and backtick (shell-exec) strings, and strings containing escape sequences.
214
+
215
+ **Off by default:** without `tailwindPhpSources` (and `--php-source`), behavior is identical to 0.2.x — no PHP
216
+ declaration is ever touched.
217
+
218
+ ## WordPress themes & plugins
219
+
220
+ Most WordPress sorting needs **no opt-in at all**. Template files and partials output markup, and the `class="..."`
221
+ in that markup is sorted by the default HTML pass — even when the value is interrupted by PHP:
222
+
223
+ ```php
224
+ <article <?php post_class( 'z-10 flex' ); ?>>
225
+ <h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight">
226
+ ```
227
+
228
+ `tailwindPhpSources` is only for classes you store in **PHP values** — a variant map, a config array, theme defaults.
229
+ For that, **don't opt in a general partial.** Partials are full of non-class strings — `__()` translations,
230
+ `get_template_part()` names, query args, URLs — and an opted-in file sorts _every_ multi-word string value. The Tailwind
231
+ sorter leaves most prose alone (unknown words keep their order), but it **will** reorder any string containing words
232
+ that are also utilities (`grid`, `block`, `flex`, `hidden`, `container`, `table`, …), so `'Switch to grid view'`
233
+ becomes `'Switch to view grid'`.
234
+
235
+ Instead, keep class maps in a **dedicated file** whose every value is a class list, and opt in only that file:
236
+
237
+ ```php
238
+ // inc/ui-classes.php → tailwindPhpSources: ['inc/ui-classes.php']
239
+ return array(
240
+ 'button' => array(
241
+ 'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
242
+ 'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
243
+ ),
244
+ 'card' => 'rounded-lg border bg-white p-6 shadow-sm',
245
+ );
246
+ ```
247
+
248
+ `require` that map from your partials. The map file is 100% class strings (safe to sort); the partials stay out of
249
+ `tailwindPhpSources` and get their markup sorted by the HTML pass as usual.
250
+
147
251
  ## How it handles mixed templates
148
252
 
149
253
  PHP islands inside a class attribute are treated as opaque atoms that never move. Static text between islands is sorted
150
254
  independently — the same model the official plugin uses for `${}` interpolations in template literals.
151
255
 
256
+ <details>
257
+ <summary>Glued-fragment pinning, whitespace handling, and the full edge-case list — click to expand</summary>
258
+
152
259
  ```php
153
260
  <!-- before -->
154
261
  <h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight leading-snug">
@@ -171,8 +278,10 @@ Also handled correctly:
171
278
  - `?>` inside `//` and `#` line comments (island ends — genuine PHP behavior)
172
279
  - `#[Attributes]`, `<?PHP` case-insensitivity, `<?xml` exclusion, files ending in PHP mode
173
280
  - PHP islands as standalone attributes: `<div <?php post_class(); ?> class="...">`
174
- - `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (sorting
175
- `class="..."` inside PHP string literals could be added later as an opt-in)
281
+ - `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (to sort class
282
+ strings declared in PHP, see [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations))
283
+
284
+ </details>
176
285
 
177
286
  ## Programmatic API
178
287
 
@@ -188,6 +297,9 @@ const out = transform(source, sortFn);
188
297
 
189
298
  ## Known limitations
190
299
 
300
+ <details>
301
+ <summary>Edge cases and unsupported syntax — click to expand</summary>
302
+
191
303
  - Complex string interpolation containing double quotes (`"{$arr["key"]}"`) can desync the PHP string lexer in rare
192
304
  cases. Use `{$arr['key']}` style or extract to a variable.
193
305
  - Unquoted attribute values (`class=foo`) are skipped.
@@ -195,17 +307,19 @@ const out = transform(source, sortFn);
195
307
  classes).
196
308
  - Whitespace inside multi-line class attributes is normalized to single spaces (matches Prettier behavior).
197
309
 
310
+ </details>
311
+
198
312
  ## Development
199
313
 
200
314
  ```sh
201
- bun test # or: node --test "test/*.test.ts"
202
- bun run build # compile src → dist (tsc); the published artifact
315
+ bun test # or: node --test "test/*.test.ts"
316
+ bun run build # compile src → dist (tsc); the published artifact
203
317
  ```
204
318
 
205
- 54 tests: 41 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`), 5
206
- integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the Tailwind
207
- toolchain isn't installed, and 8 `init` tests that run against throwaway git repositories and skip when `git` is
208
- unavailable.
319
+ 74 tests: 59 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`),
320
+ 7 integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the
321
+ Tailwind toolchain isn't installed, and 8 `init` tests that run against throwaway git repositories and skip when
322
+ `git` is unavailable.
209
323
 
210
324
  ## License
211
325
 
package/dist/cli.d.ts CHANGED
@@ -9,10 +9,11 @@
9
9
  * Options:
10
10
  * --stylesheet <path> Tailwind v4 CSS entry
11
11
  * --attr <name> Extra attribute to sort (repeatable)
12
+ * --php-source <glob> Also sort class strings in PHP declarations in matching files (repeatable)
12
13
  * --check Don't write; exit 1 if any file needs sorting
13
14
  * --no-short-tags Don't treat bare `<?` as a PHP open tag
14
15
  *
15
- * Defaults to all `.php` files under `cwd` (`"**" + "/*.php"`) when no globs are given.
16
+ * Defaults to all `.php` files under `cwd` when no globs are given.
16
17
  * Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
17
18
  */
18
19
  export {};
package/dist/cli.js CHANGED
@@ -9,10 +9,11 @@
9
9
  * Options:
10
10
  * --stylesheet <path> Tailwind v4 CSS entry
11
11
  * --attr <name> Extra attribute to sort (repeatable)
12
+ * --php-source <glob> Also sort class strings in PHP declarations in matching files (repeatable)
12
13
  * --check Don't write; exit 1 if any file needs sorting
13
14
  * --no-short-tags Don't treat bare `<?` as a PHP open tag
14
15
  *
15
- * Defaults to all `.php` files under `cwd` (`"**" + "/*.php"`) when no globs are given.
16
+ * Defaults to all `.php` files under `cwd` when no globs are given.
16
17
  * Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
17
18
  */
18
19
  import { readFile, writeFile } from 'node:fs/promises';
@@ -30,6 +31,7 @@ function parseArgs(argv) {
30
31
  const cli = {
31
32
  globs: [],
32
33
  attrs: ['class', 'className'],
34
+ phpSources: [],
33
35
  check: false,
34
36
  shortTags: true,
35
37
  };
@@ -43,6 +45,8 @@ function parseArgs(argv) {
43
45
  cli.stylesheet = argv[++i];
44
46
  else if (a === '--attr')
45
47
  cli.attrs.push(argv[++i]);
48
+ else if (a === '--php-source')
49
+ cli.phpSources.push(argv[++i]);
46
50
  else if (a.startsWith('--')) {
47
51
  console.error(`Unknown option: ${a}`);
48
52
  process.exit(2);
@@ -60,7 +64,6 @@ function parseArgs(argv) {
60
64
  * @param globs Glob patterns relative to `cwd`.
61
65
  */
62
66
  async function* scanFiles(globs) {
63
- // Use `Bun.Glob` when available, fall back to `node:fs` glob (Node 22+).
64
67
  if (typeof globalThis.Bun !== 'undefined') {
65
68
  const { Glob } = await import('bun');
66
69
  for (const pattern of globs) {
@@ -84,11 +87,10 @@ async function* scanFiles(globs) {
84
87
  */
85
88
  const ignored = (file) => IGNORE.some((d) => file.includes(`${d}/`) || file.startsWith(d));
86
89
  /**
87
- * Best-effort read of the project's resolved Prettier config, so this tool shares one source of truth with
88
- * `prettier-plugin-tailwindcss`. Picks up `tailwindStylesheet` (resolved relative to the config file) and
89
- * `tailwindAttributes` (merged into the attribute list).
90
+ * Best-effort read of the resolved Prettier config the shared source of truth with `prettier-plugin-tailwindcss`.
91
+ * Picks up `tailwindStylesheet`, `tailwindAttributes`, and `tailwindPhpSources`.
90
92
  *
91
- * @returns The resolved stylesheet path and attributes, or an empty object if none are available.
93
+ * @returns The found settings, or an empty object if none are available.
92
94
  */
93
95
  async function fromPrettierConfig() {
94
96
  try {
@@ -107,6 +109,9 @@ async function fromPrettierConfig() {
107
109
  if (Array.isArray(cfg.tailwindAttributes)) {
108
110
  out.attributes = cfg.tailwindAttributes.filter((a) => typeof a === 'string' && !a.startsWith('/'));
109
111
  }
112
+ if (Array.isArray(cfg.tailwindPhpSources)) {
113
+ out.phpSources = cfg.tailwindPhpSources.filter((p) => typeof p === 'string');
114
+ }
110
115
  return out;
111
116
  }
112
117
  catch {
@@ -136,24 +141,32 @@ async function main() {
136
141
  attributes: cli.attrs,
137
142
  shortOpenTags: cli.shortTags,
138
143
  };
144
+ // Pre-scan the opt-in `tailwindPhpSources` globs; a file gets `sortPhpStrings` only if it matches one.
145
+ const phpSources = [...cli.phpSources, ...(pc.phpSources ?? [])];
146
+ const phpSourceFiles = new Set();
147
+ for await (const file of scanFiles(phpSources)) {
148
+ if (!ignored(file))
149
+ phpSourceFiles.add(file);
150
+ }
139
151
  let scanned = 0;
140
152
  let changed = 0;
141
- for (const pattern of cli.globs) {
142
- for await (const file of scanFiles([pattern])) {
143
- if (ignored(file))
144
- continue;
145
- scanned++;
146
- const src = await readFile(file, 'utf8');
147
- const out = transform(src, sortFn, opts);
148
- if (out !== src) {
149
- changed++;
150
- if (cli.check) {
151
- console.log(`needs sorting: ${file}`);
152
- }
153
- else {
154
- await writeFile(file, out);
155
- console.log(`sorted: ${file}`);
156
- }
153
+ const seen = new Set();
154
+ // Include php-source files so a designated holder is sorted even outside the main globs; `seen` dedupes.
155
+ for await (const file of scanFiles([...cli.globs, ...phpSources])) {
156
+ if (ignored(file) || seen.has(file))
157
+ continue;
158
+ seen.add(file);
159
+ scanned++;
160
+ const src = await readFile(file, 'utf8');
161
+ const out = transform(src, sortFn, { ...opts, sortPhpStrings: phpSourceFiles.has(file) });
162
+ if (out !== src) {
163
+ changed++;
164
+ if (cli.check) {
165
+ console.log(`needs sorting: ${file}`);
166
+ }
167
+ else {
168
+ await writeFile(file, out);
169
+ console.log(`sorted: ${file}`);
157
170
  }
158
171
  }
159
172
  }
package/dist/html.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * - islands inside a quoted attribute value read as opaque atoms
9
9
  *
10
10
  * Skips HTML comments, doctype/CDATA, and the raw-text content of `script`/`style`/`textarea`/`title` elements —
11
- * their `script`/`style` content may contain strings like `class="..."` that must not be touched.
11
+ * their content may contain strings like `class="..."` that must not be touched.
12
12
  *
13
13
  * @see islands.ts - first pass; produces the islands consumed here.
14
14
  */
package/dist/html.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - islands inside a quoted attribute value read as opaque atoms
9
9
  *
10
10
  * Skips HTML comments, doctype/CDATA, and the raw-text content of `script`/`style`/`textarea`/`title` elements —
11
- * their `script`/`style` content may contain strings like `class="..."` that must not be touched.
11
+ * their content may contain strings like `class="..."` that must not be touched.
12
12
  *
13
13
  * @see islands.ts - first pass; produces the islands consumed here.
14
14
  */
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
@@ -13,9 +13,8 @@ const HOOK_PATH = `${HOOKS_DIR}/pre-commit`;
13
13
  * Check-and-fail hook (default): names the unsorted files and blocks the commit; never writes.
14
14
  */
15
15
  const HOOK_CHECK = `#!/bin/sh
16
- # Block commits with unsorted Tailwind classes in staged PHP files.
17
- # Installed by \`tailwind-sort-php init\`. Note: checks working-tree file
18
- # contents, so partial staging (git add -p) can mis-report; see README.
16
+ # Block commits with unsorted Tailwind classes in staged PHP files. Installed by \`tailwind-sort-php init\`.
17
+ # Note: checks working-tree file contents, so partial staging (\`git add -p\`) can mis-report; see README.
19
18
  sorter=./node_modules/.bin/tailwind-sort-php
20
19
  [ -x "$sorter" ] || exit 0
21
20
  git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
@@ -31,10 +30,9 @@ exit 1
31
30
  * Auto-fix hook (--fix): sorts the staged files in place, then blocks the commit for review.
32
31
  */
33
32
  const HOOK_FIX = `#!/bin/sh
34
- # Sort Tailwind classes in staged PHP files, then abort the commit so the
35
- # changes can be reviewed and re-staged. Rewrites working-tree files.
36
- # Installed by \`tailwind-sort-php init --fix\`. Note: with partial staging
37
- # (git add -p), re-staging can pull in unrelated unstaged hunks; see README.
33
+ # Sort Tailwind classes in staged PHP files, then abort the commit so the changes can be reviewed and re-staged.
34
+ # Rewrites working-tree files. Installed by \`tailwind-sort-php init --fix\`.
35
+ # Note: with partial staging (\`git add -p\`), re-staging can pull in unrelated unstaged hunks; see README.
38
36
  sorter=./node_modules/.bin/tailwind-sort-php
39
37
  [ -x "$sorter" ] || exit 0
40
38
  git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
@@ -134,7 +132,11 @@ export async function runInit(argv) {
134
132
  let repairMode = false;
135
133
  if (current !== null && current !== hookBody) {
136
134
  if (!cli.force) {
137
- 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';
138
140
  fail(`${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`);
139
141
  }
140
142
  writeHook = true;
@@ -150,7 +152,9 @@ export async function runInit(argv) {
150
152
  await writeFile(hookAbs, hookBody);
151
153
  await chmod(hookAbs, 0o755);
152
154
  }
153
- 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)`);
154
158
  }
155
159
  if (repairMode) {
156
160
  if (!cli.dryRun)
package/dist/islands.js CHANGED
@@ -73,7 +73,7 @@ function scanPhpBody(src, i) {
73
73
  continue;
74
74
  }
75
75
  // Double-quoted string.
76
- // Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer (documented).
76
+ // Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer; see README.
77
77
  if (c === '"') {
78
78
  i = scanQuoted(src, i + 1, '"');
79
79
  continue;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PHP string-literal harvester — the optional third pass.
3
+ *
4
+ * Given the source and the PHP islands already found by `findIslands()`, this locates the byte ranges of every
5
+ * string literal *value* inside PHP code that is eligible for class sorting, applying these structural rules:
6
+ *
7
+ * - Only the *value* of a `key => value` pair is eligible; array keys are never sorted.
8
+ * - Bare list-style elements (`['a', 'b']`) and scalar assignments (`const X = '...'`) are values.
9
+ * - Strings that are part of a concatenation expression (`'btn-' . $v`) are skipped — a literal joined to dynamic
10
+ * code may be a partial class fragment, and reordering it would corrupt the rendered string.
11
+ * - Double-quoted strings containing interpolation (`"p-4 {$x}"`) are skipped, mirroring the HTML side's
12
+ * conservatism around dynamic content.
13
+ * - Strings whose body contains a backslash escape are skipped, so escape sequences can never be mangled.
14
+ * - Heredoc/nowdoc and backtick (shell-exec) strings are never harvested.
15
+ *
16
+ * This pass is opt-in per file (the caller decides which files are class-string holders); it does NOT judge whether
17
+ * a given string "looks like" Tailwind classes — within a matched file, every eligible value is sorted.
18
+ *
19
+ * @see islands.ts - first pass; produces the islands consumed here.
20
+ * @see transform.ts - splices the sorted strings back via the shared byte-replacement path.
21
+ */
22
+ import type { Island } from './islands.ts';
23
+ /**
24
+ * Inner byte range of a sortable string literal — the span *between* the quotes, in original-source offsets.
25
+ */
26
+ export interface PhpStringRange {
27
+ /**
28
+ * Offset of the first character inside the quotes.
29
+ */
30
+ start: number;
31
+ /**
32
+ * Offset just past the last character inside the quotes (exclusive).
33
+ */
34
+ end: number;
35
+ }
36
+ /**
37
+ * Find every sortable string-literal value within the given PHP islands.
38
+ *
39
+ * @param src Original template source.
40
+ * @param islands Island ranges from `findIslands()`.
41
+ * @returns Inner ranges of eligible string values, in document order.
42
+ *
43
+ * @example
44
+ * const islands = findIslands(`<?php $x = 'z a'; ?>`);
45
+ * findSortablePhpStrings(`<?php $x = 'z a'; ?>`, islands); // [{ start: 11, end: 14 }]
46
+ */
47
+ export declare function findSortablePhpStrings(src: string, islands: Island[]): PhpStringRange[];