@runtimestudio/tailwind-sort-php 0.2.1 → 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/src/html.ts CHANGED
@@ -19,28 +19,28 @@ import type { Island } from './islands.ts';
19
19
  * Location of a sortable class attribute value within the source.
20
20
  */
21
21
  export interface ClassAttr {
22
- /**
23
- * Attribute name as written (e.g. `class`, `className`).
24
- */
25
- name: string;
26
- /**
27
- * Offset of the first character inside the quotes.
28
- */
29
- valueStart: number;
30
- /**
31
- * Offset just past the last character inside the quotes (exclusive).
32
- */
33
- valueEnd: number;
22
+ /**
23
+ * Attribute name as written (e.g. `class`, `className`).
24
+ */
25
+ name: string;
26
+ /**
27
+ * Offset of the first character inside the quotes.
28
+ */
29
+ valueStart: number;
30
+ /**
31
+ * Offset just past the last character inside the quotes (exclusive).
32
+ */
33
+ valueEnd: number;
34
34
  }
35
35
 
36
36
  /**
37
37
  * Options controlling which attributes are collected.
38
38
  */
39
39
  export interface HtmlScanOptions {
40
- /**
41
- * Lowercase attribute names to collect; default `['class', 'classname']`.
42
- */
43
- attributes?: string[];
40
+ /**
41
+ * Lowercase attribute names to collect; default `['class', 'classname']`.
42
+ */
43
+ attributes?: string[];
44
44
  }
45
45
 
46
46
  const RAW_TEXT_TAGS = new Set(['script', 'style', 'textarea', 'title']);
@@ -56,16 +56,16 @@ const NUL = '\x00';
56
56
  * @returns Masked source of identical length.
57
57
  */
58
58
  export function maskIslands(src: string, islands: Island[]): string {
59
- if (islands.length === 0) return src;
60
- let out = '';
61
- let pos = 0;
62
- for (const isl of islands) {
63
- out += src.slice(pos, isl.start);
64
- out += NUL.repeat(isl.end - isl.start);
65
- pos = isl.end;
66
- }
67
- out += src.slice(pos);
68
- return out;
59
+ if (islands.length === 0) return src;
60
+ let out = '';
61
+ let pos = 0;
62
+ for (const isl of islands) {
63
+ out += src.slice(pos, isl.start);
64
+ out += NUL.repeat(isl.end - isl.start);
65
+ pos = isl.end;
66
+ }
67
+ out += src.slice(pos);
68
+ return out;
69
69
  }
70
70
 
71
71
  /**
@@ -76,58 +76,58 @@ export function maskIslands(src: string, islands: Island[]): string {
76
76
  * @returns Attribute value locations in document order. Offsets index into the original source.
77
77
  */
78
78
  export function findClassAttributes(masked: string, opts: HtmlScanOptions = {}): ClassAttr[] {
79
- const wanted = new Set((opts.attributes ?? ['class', 'classname']).map((a) => a.toLowerCase()));
80
- const out: ClassAttr[] = [];
81
- const len = masked.length;
82
- let i = 0;
83
-
84
- while (i < len) {
85
- const lt = masked.indexOf('<', i);
86
- if (lt === -1) break;
87
-
88
- // HTML comment.
89
- if (masked.startsWith('<!--', lt)) {
90
- const close = masked.indexOf('-->', lt + 4);
91
- i = close === -1 ? len : close + 3;
92
- continue;
93
- }
94
-
95
- // Doctype / CDATA / other declarations.
96
- if (masked[lt + 1] === '!') {
97
- const close = masked.indexOf('>', lt + 2);
98
- i = close === -1 ? len : close + 1;
99
- continue;
100
- }
101
-
102
- // Closing tag.
103
- if (masked[lt + 1] === '/') {
104
- const close = masked.indexOf('>', lt + 2);
105
- i = close === -1 ? len : close + 1;
106
- continue;
107
- }
108
-
109
- // Opening tag?
110
- if (lt + 1 < len && /[A-Za-z]/.test(masked[lt + 1])) {
111
- let j = lt + 1;
112
- while (j < len && /[A-Za-z0-9:-]/.test(masked[j])) j++;
113
- const tagName = masked.slice(lt + 1, j).toLowerCase();
114
-
115
- j = scanTagAttributes(masked, j, wanted, out);
116
-
117
- // Skip raw-text element content up to its closing tag.
118
- if (RAW_TEXT_TAGS.has(tagName)) {
119
- const closer = `</${tagName}`;
120
- const idx = masked.toLowerCase().indexOf(closer, j);
121
- j = idx === -1 ? len : idx;
122
- }
123
- i = j;
124
- continue;
79
+ const wanted = new Set((opts.attributes ?? ['class', 'classname']).map((a) => a.toLowerCase()));
80
+ const out: ClassAttr[] = [];
81
+ const len = masked.length;
82
+ let i = 0;
83
+
84
+ while (i < len) {
85
+ const lt = masked.indexOf('<', i);
86
+ if (lt === -1) break;
87
+
88
+ // HTML comment.
89
+ if (masked.startsWith('<!--', lt)) {
90
+ const close = masked.indexOf('-->', lt + 4);
91
+ i = close === -1 ? len : close + 3;
92
+ continue;
93
+ }
94
+
95
+ // Doctype / CDATA / other declarations.
96
+ if (masked[lt + 1] === '!') {
97
+ const close = masked.indexOf('>', lt + 2);
98
+ i = close === -1 ? len : close + 1;
99
+ continue;
100
+ }
101
+
102
+ // Closing tag.
103
+ if (masked[lt + 1] === '/') {
104
+ const close = masked.indexOf('>', lt + 2);
105
+ i = close === -1 ? len : close + 1;
106
+ continue;
107
+ }
108
+
109
+ // Opening tag?
110
+ if (lt + 1 < len && /[A-Za-z]/.test(masked[lt + 1])) {
111
+ let j = lt + 1;
112
+ while (j < len && /[A-Za-z0-9:-]/.test(masked[j])) j++;
113
+ const tagName = masked.slice(lt + 1, j).toLowerCase();
114
+
115
+ j = scanTagAttributes(masked, j, wanted, out);
116
+
117
+ // Skip raw-text element content up to its closing tag.
118
+ if (RAW_TEXT_TAGS.has(tagName)) {
119
+ const closer = `</${tagName}`;
120
+ const idx = masked.toLowerCase().indexOf(closer, j);
121
+ j = idx === -1 ? len : idx;
122
+ }
123
+ i = j;
124
+ continue;
125
+ }
126
+
127
+ i = lt + 1;
125
128
  }
126
129
 
127
- i = lt + 1;
128
- }
129
-
130
- return out;
130
+ return out;
131
131
  }
132
132
 
133
133
  const isTagWs = (c: string) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === NUL;
@@ -137,48 +137,48 @@ const isTagWs = (c: string) => c === ' ' || c === '\t' || c === '\n' || c === '\
137
137
  * Returns the offset after `>` (or EOF). Pushes matches into `out`.
138
138
  */
139
139
  function scanTagAttributes(masked: string, i: number, wanted: Set<string>, out: ClassAttr[]): number {
140
- const len = masked.length;
141
-
142
- while (i < len) {
143
- while (i < len && isTagWs(masked[i])) i++;
144
- if (i >= len) return len;
145
-
146
- const c = masked[i];
147
- if (c === '>') return i + 1;
148
- if (c === '/') {
149
- i++;
150
- continue;
151
- }
152
-
153
- // Attribute name.
154
- const nameStart = i;
155
- while (i < len && !isTagWs(masked[i]) && masked[i] !== '=' && masked[i] !== '>' && masked[i] !== '/') i++;
156
- const name = masked.slice(nameStart, i);
157
- if (name.length === 0) {
158
- i++;
159
- continue;
160
- }
161
-
162
- while (i < len && isTagWs(masked[i])) i++;
163
- if (masked[i] !== '=') continue; // boolean attribute
164
-
165
- i++;
166
- while (i < len && isTagWs(masked[i])) i++;
167
-
168
- const q = masked[i];
169
- if (q === '"' || q === "'") {
170
- const valueStart = i + 1;
171
- const close = masked.indexOf(q, valueStart);
172
- const valueEnd = close === -1 ? len : close;
173
- if (wanted.has(name.toLowerCase())) {
174
- out.push({ name, valueStart, valueEnd });
175
- }
176
- i = valueEnd + 1;
177
- } else {
178
- // Unquoted value — read it but never rewrite (too risky to widen).
179
- while (i < len && !isTagWs(masked[i]) && masked[i] !== '>') i++;
140
+ const len = masked.length;
141
+
142
+ while (i < len) {
143
+ while (i < len && isTagWs(masked[i])) i++;
144
+ if (i >= len) return len;
145
+
146
+ const c = masked[i];
147
+ if (c === '>') return i + 1;
148
+ if (c === '/') {
149
+ i++;
150
+ continue;
151
+ }
152
+
153
+ // Attribute name.
154
+ const nameStart = i;
155
+ while (i < len && !isTagWs(masked[i]) && masked[i] !== '=' && masked[i] !== '>' && masked[i] !== '/') i++;
156
+ const name = masked.slice(nameStart, i);
157
+ if (name.length === 0) {
158
+ i++;
159
+ continue;
160
+ }
161
+
162
+ while (i < len && isTagWs(masked[i])) i++;
163
+ if (masked[i] !== '=') continue; // boolean attribute
164
+
165
+ i++;
166
+ while (i < len && isTagWs(masked[i])) i++;
167
+
168
+ const q = masked[i];
169
+ if (q === '"' || q === "'") {
170
+ const valueStart = i + 1;
171
+ const close = masked.indexOf(q, valueStart);
172
+ const valueEnd = close === -1 ? len : close;
173
+ if (wanted.has(name.toLowerCase())) {
174
+ out.push({ name, valueStart, valueEnd });
175
+ }
176
+ i = valueEnd + 1;
177
+ } else {
178
+ // Unquoted value — read it but never rewrite (too risky to widen).
179
+ while (i < len && !isTagWs(masked[i]) && masked[i] !== '>') i++;
180
+ }
180
181
  }
181
- }
182
182
 
183
- return len;
183
+ return len;
184
184
  }
package/src/index.ts CHANGED
@@ -11,4 +11,5 @@
11
11
  export { transform, type SortFn, type TransformOptions } from './transform.ts';
12
12
  export { findIslands, type Island, type IslandOptions } from './islands.ts';
13
13
  export { maskIslands, findClassAttributes, type ClassAttr, type HtmlScanOptions } from './html.ts';
14
+ export { findSortablePhpStrings, type PhpStringRange } from './php-strings.ts';
14
15
  export { createTailwindSortFn, type SorterOptions } from './sorter.ts';
package/src/init.ts CHANGED
@@ -51,9 +51,9 @@ exit 1
51
51
  `;
52
52
 
53
53
  interface InitCli {
54
- fix: boolean;
55
- force: boolean;
56
- dryRun: boolean;
54
+ fix: boolean;
55
+ force: boolean;
56
+ dryRun: boolean;
57
57
  }
58
58
 
59
59
  /**
@@ -63,17 +63,17 @@ interface InitCli {
63
63
  * @returns Parsed flags with defaults applied.
64
64
  */
65
65
  function parseArgs(argv: string[]): InitCli {
66
- const cli: InitCli = { fix: false, force: false, dryRun: false };
67
- for (const a of argv) {
68
- if (a === '--fix') cli.fix = true;
69
- else if (a === '--force') cli.force = true;
70
- else if (a === '--dry-run') cli.dryRun = true;
71
- else {
72
- console.error(`Unknown init option: ${a}`);
73
- process.exit(2);
66
+ const cli: InitCli = { fix: false, force: false, dryRun: false };
67
+ for (const a of argv) {
68
+ if (a === '--fix') cli.fix = true;
69
+ else if (a === '--force') cli.force = true;
70
+ else if (a === '--dry-run') cli.dryRun = true;
71
+ else {
72
+ console.error(`Unknown init option: ${a}`);
73
+ process.exit(2);
74
+ }
74
75
  }
75
- }
76
- return cli;
76
+ return cli;
77
77
  }
78
78
 
79
79
  /**
@@ -83,11 +83,11 @@ function parseArgs(argv: string[]): InitCli {
83
83
  * @returns Trimmed stdout, or `null` when git exits non-zero (unset config, not a repo, …).
84
84
  */
85
85
  function git(args: string[]): string | null {
86
- try {
87
- return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
88
- } catch {
89
- return null;
90
- }
86
+ try {
87
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
88
+ } catch {
89
+ return null;
90
+ }
91
91
  }
92
92
 
93
93
  /**
@@ -96,8 +96,8 @@ function git(args: string[]): string | null {
96
96
  * @param message Error text explaining why init refused to proceed.
97
97
  */
98
98
  function fail(message: string): never {
99
- console.error(message);
100
- process.exit(1);
99
+ console.error(message);
100
+ process.exit(1);
101
101
  }
102
102
 
103
103
  /**
@@ -106,81 +106,89 @@ function fail(message: string): never {
106
106
  * @param argv Arguments after the `init` subcommand name.
107
107
  */
108
108
  export async function runInit(argv: string[]): Promise<void> {
109
- const cli = parseArgs(argv);
110
- const hookBody = cli.fix ? HOOK_FIX : HOOK_CHECK;
111
- const variant = cli.fix ? 'fix' : 'check';
109
+ const cli = parseArgs(argv);
110
+ const hookBody = cli.fix ? HOOK_FIX : HOOK_CHECK;
111
+ const variant = cli.fix ? 'fix' : 'check';
112
112
 
113
- // Anchor everything at the repository root: a relative `core.hooksPath` resolves there,
114
- // and the hook's `./node_modules/...` path assumes hooks run from it (they do, for pre-commit).
115
- const top = git(['rev-parse', '--show-toplevel']);
116
- if (top === null) fail('Not a git repository (or a bare one) — run init from inside a working tree.');
117
- const gitDir = git(['rev-parse', '--absolute-git-dir'])!;
118
- const hookAbs = join(top, HOOK_PATH);
113
+ // Anchor everything at the repository root: a relative `core.hooksPath` resolves there,
114
+ // and the hook's `./node_modules/...` path assumes hooks run from it (they do, for pre-commit).
115
+ const top = git(['rev-parse', '--show-toplevel']);
116
+ if (top === null) fail('Not a git repository (or a bare one) — run init from inside a working tree.');
117
+ const gitDir = git(['rev-parse', '--absolute-git-dir'])!;
118
+ const hookAbs = join(top, HOOK_PATH);
119
119
 
120
- // Decide whether `core.hooksPath` needs to change. Repointing it makes git ignore `.git/hooks` entirely,
121
- // so refuse to silently disable hooks that already live there.
122
- const hooksPath = git(['config', '--get', 'core.hooksPath']);
123
- let setConfig = false;
124
- if (hooksPath === null) {
125
- const live = (await readdir(join(gitDir, 'hooks')).catch(() => [])).filter((f) => !f.endsWith('.sample'));
126
- if (live.length > 0 && !cli.force) {
127
- fail(
128
- `Found existing hook(s) in .git/hooks: ${live.join(', ')}.\n` +
129
- `Setting core.hooksPath would disable them. Move them into ${HOOKS_DIR}/ first, ` +
130
- 'or re-run with --force to proceed anyway.',
131
- );
132
- }
133
- setConfig = true;
134
- } else if (hooksPath !== HOOKS_DIR) {
135
- if (!cli.force) {
136
- fail(
137
- `core.hooksPath is already set to "${hooksPath}" — add the hook there yourself, or re-run ` +
138
- `with --force to repoint it to ${HOOKS_DIR}. Hook body:\n\n${hookBody}`,
139
- );
120
+ // Decide whether `core.hooksPath` needs to change. Repointing it makes git ignore `.git/hooks` entirely,
121
+ // so refuse to silently disable hooks that already live there.
122
+ const hooksPath = git(['config', '--get', 'core.hooksPath']);
123
+ let setConfig = false;
124
+ if (hooksPath === null) {
125
+ const live = (await readdir(join(gitDir, 'hooks')).catch(() => [])).filter((f) => !f.endsWith('.sample'));
126
+ if (live.length > 0 && !cli.force) {
127
+ fail(
128
+ `Found existing hook(s) in .git/hooks: ${live.join(', ')}.\n` +
129
+ `Setting core.hooksPath would disable them. Move them into ${HOOKS_DIR}/ first, ` +
130
+ 'or re-run with --force to proceed anyway.',
131
+ );
132
+ }
133
+ setConfig = true;
134
+ } else if (hooksPath !== HOOKS_DIR) {
135
+ if (!cli.force) {
136
+ fail(
137
+ `core.hooksPath is already set to "${hooksPath}" — add the hook there yourself, or re-run ` +
138
+ `with --force to repoint it to ${HOOKS_DIR}. Hook body:\n\n${hookBody}`,
139
+ );
140
+ }
141
+ setConfig = true;
140
142
  }
141
- setConfig = true;
142
- }
143
143
 
144
- // Decide whether the hook file needs writing; overwriting a differing hook needs --force.
145
- const current = await readFile(hookAbs, 'utf8').catch(() => null);
146
- let writeHook = current === null;
147
- let repairMode = false;
148
- if (current !== null && current !== hookBody) {
149
- if (!cli.force) {
150
- const installed =
151
- current === HOOK_CHECK ? 'the check variant' : current === HOOK_FIX ? 'the --fix variant' : 'a custom hook';
152
- fail(`${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`);
144
+ // Decide whether the hook file needs writing; overwriting a differing hook needs --force.
145
+ const current = await readFile(hookAbs, 'utf8').catch(() => null);
146
+ let writeHook = current === null;
147
+ let repairMode = false;
148
+ if (current !== null && current !== hookBody) {
149
+ if (!cli.force) {
150
+ const installed =
151
+ current === HOOK_CHECK
152
+ ? 'the check variant'
153
+ : current === HOOK_FIX
154
+ ? 'the --fix variant'
155
+ : 'a custom hook';
156
+ fail(
157
+ `${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`,
158
+ );
159
+ }
160
+ writeHook = true;
161
+ }
162
+ if (current === hookBody) {
163
+ // Content is already right; still repair a missing executable bit, or git silently skips the hook.
164
+ repairMode = ((await stat(hookAbs)).mode & 0o111) === 0;
153
165
  }
154
- writeHook = true;
155
- }
156
- if (current === hookBody) {
157
- // Content is already right; still repair a missing executable bit, or git silently skips the hook.
158
- repairMode = ((await stat(hookAbs)).mode & 0o111) === 0;
159
- }
160
166
 
161
- const done: string[] = [];
162
- if (writeHook) {
163
- if (!cli.dryRun) {
164
- await mkdir(join(top, HOOKS_DIR), { recursive: true });
165
- await writeFile(hookAbs, hookBody);
166
- await chmod(hookAbs, 0o755);
167
+ const done: string[] = [];
168
+ if (writeHook) {
169
+ if (!cli.dryRun) {
170
+ await mkdir(join(top, HOOKS_DIR), { recursive: true });
171
+ await writeFile(hookAbs, hookBody);
172
+ await chmod(hookAbs, 0o755);
173
+ }
174
+ done.push(
175
+ cli.dryRun
176
+ ? `would install ${HOOK_PATH} (${variant} variant)`
177
+ : `installed ${HOOK_PATH} (${variant} variant)`,
178
+ );
179
+ }
180
+ if (repairMode) {
181
+ if (!cli.dryRun) await chmod(hookAbs, 0o755);
182
+ done.push(cli.dryRun ? `would make ${HOOK_PATH} executable` : `made ${HOOK_PATH} executable`);
183
+ }
184
+ if (setConfig) {
185
+ if (!cli.dryRun) git(['config', 'core.hooksPath', HOOKS_DIR]);
186
+ done.push(cli.dryRun ? `would set core.hooksPath = ${HOOKS_DIR}` : `set core.hooksPath = ${HOOKS_DIR}`);
167
187
  }
168
- done.push(
169
- cli.dryRun ? `would install ${HOOK_PATH} (${variant} variant)` : `installed ${HOOK_PATH} (${variant} variant)`,
170
- );
171
- }
172
- if (repairMode) {
173
- if (!cli.dryRun) await chmod(hookAbs, 0o755);
174
- done.push(cli.dryRun ? `would make ${HOOK_PATH} executable` : `made ${HOOK_PATH} executable`);
175
- }
176
- if (setConfig) {
177
- if (!cli.dryRun) git(['config', 'core.hooksPath', HOOKS_DIR]);
178
- done.push(cli.dryRun ? `would set core.hooksPath = ${HOOKS_DIR}` : `set core.hooksPath = ${HOOKS_DIR}`);
179
- }
180
188
 
181
- if (done.length === 0) {
182
- console.log(`Nothing to do — ${HOOK_PATH} (${variant} variant) is already installed.`);
183
- return;
184
- }
185
- for (const line of done) console.log(line);
189
+ if (done.length === 0) {
190
+ console.log(`Nothing to do — ${HOOK_PATH} (${variant} variant) is already installed.`);
191
+ return;
192
+ }
193
+ for (const line of done) console.log(line);
186
194
  }