@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/src/init.ts CHANGED
@@ -16,9 +16,8 @@ const HOOK_PATH = `${HOOKS_DIR}/pre-commit`;
16
16
  * Check-and-fail hook (default): names the unsorted files and blocks the commit; never writes.
17
17
  */
18
18
  const HOOK_CHECK = `#!/bin/sh
19
- # Block commits with unsorted Tailwind classes in staged PHP files.
20
- # Installed by \`tailwind-sort-php init\`. Note: checks working-tree file
21
- # contents, so partial staging (git add -p) can mis-report; see README.
19
+ # Block commits with unsorted Tailwind classes in staged PHP files. Installed by \`tailwind-sort-php init\`.
20
+ # Note: checks working-tree file contents, so partial staging (\`git add -p\`) can mis-report; see README.
22
21
  sorter=./node_modules/.bin/tailwind-sort-php
23
22
  [ -x "$sorter" ] || exit 0
24
23
  git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
@@ -35,10 +34,9 @@ exit 1
35
34
  * Auto-fix hook (--fix): sorts the staged files in place, then blocks the commit for review.
36
35
  */
37
36
  const HOOK_FIX = `#!/bin/sh
38
- # Sort Tailwind classes in staged PHP files, then abort the commit so the
39
- # changes can be reviewed and re-staged. Rewrites working-tree files.
40
- # Installed by \`tailwind-sort-php init --fix\`. Note: with partial staging
41
- # (git add -p), re-staging can pull in unrelated unstaged hunks; see README.
37
+ # Sort Tailwind classes in staged PHP files, then abort the commit so the changes can be reviewed and re-staged.
38
+ # Rewrites working-tree files. Installed by \`tailwind-sort-php init --fix\`.
39
+ # Note: with partial staging (\`git add -p\`), re-staging can pull in unrelated unstaged hunks; see README.
42
40
  sorter=./node_modules/.bin/tailwind-sort-php
43
41
  [ -x "$sorter" ] || exit 0
44
42
  git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
@@ -53,9 +51,9 @@ exit 1
53
51
  `;
54
52
 
55
53
  interface InitCli {
56
- fix: boolean;
57
- force: boolean;
58
- dryRun: boolean;
54
+ fix: boolean;
55
+ force: boolean;
56
+ dryRun: boolean;
59
57
  }
60
58
 
61
59
  /**
@@ -65,17 +63,17 @@ interface InitCli {
65
63
  * @returns Parsed flags with defaults applied.
66
64
  */
67
65
  function parseArgs(argv: string[]): InitCli {
68
- const cli: InitCli = { fix: false, force: false, dryRun: false };
69
- for (const a of argv) {
70
- if (a === '--fix') cli.fix = true;
71
- else if (a === '--force') cli.force = true;
72
- else if (a === '--dry-run') cli.dryRun = true;
73
- else {
74
- console.error(`Unknown init option: ${a}`);
75
- 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
+ }
76
75
  }
77
- }
78
- return cli;
76
+ return cli;
79
77
  }
80
78
 
81
79
  /**
@@ -85,11 +83,11 @@ function parseArgs(argv: string[]): InitCli {
85
83
  * @returns Trimmed stdout, or `null` when git exits non-zero (unset config, not a repo, …).
86
84
  */
87
85
  function git(args: string[]): string | null {
88
- try {
89
- return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
90
- } catch {
91
- return null;
92
- }
86
+ try {
87
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
88
+ } catch {
89
+ return null;
90
+ }
93
91
  }
94
92
 
95
93
  /**
@@ -98,8 +96,8 @@ function git(args: string[]): string | null {
98
96
  * @param message Error text explaining why init refused to proceed.
99
97
  */
100
98
  function fail(message: string): never {
101
- console.error(message);
102
- process.exit(1);
99
+ console.error(message);
100
+ process.exit(1);
103
101
  }
104
102
 
105
103
  /**
@@ -108,81 +106,89 @@ function fail(message: string): never {
108
106
  * @param argv Arguments after the `init` subcommand name.
109
107
  */
110
108
  export async function runInit(argv: string[]): Promise<void> {
111
- const cli = parseArgs(argv);
112
- const hookBody = cli.fix ? HOOK_FIX : HOOK_CHECK;
113
- 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';
114
112
 
115
- // Anchor everything at the repository root: a relative `core.hooksPath` resolves there,
116
- // and the hook's `./node_modules/...` path assumes hooks run from it (they do, for pre-commit).
117
- const top = git(['rev-parse', '--show-toplevel']);
118
- if (top === null) fail('Not a git repository (or a bare one) — run init from inside a working tree.');
119
- const gitDir = git(['rev-parse', '--absolute-git-dir'])!;
120
- 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);
121
119
 
122
- // Decide whether `core.hooksPath` needs to change. Repointing it makes git ignore `.git/hooks` entirely,
123
- // so refuse to silently disable hooks that already live there.
124
- const hooksPath = git(['config', '--get', 'core.hooksPath']);
125
- let setConfig = false;
126
- if (hooksPath === null) {
127
- const live = (await readdir(join(gitDir, 'hooks')).catch(() => [])).filter((f) => !f.endsWith('.sample'));
128
- if (live.length > 0 && !cli.force) {
129
- fail(
130
- `Found existing hook(s) in .git/hooks: ${live.join(', ')}.\n` +
131
- `Setting core.hooksPath would disable them. Move them into ${HOOKS_DIR}/ first, ` +
132
- 'or re-run with --force to proceed anyway.',
133
- );
134
- }
135
- setConfig = true;
136
- } else if (hooksPath !== HOOKS_DIR) {
137
- if (!cli.force) {
138
- fail(
139
- `core.hooksPath is already set to "${hooksPath}" — add the hook there yourself, or re-run ` +
140
- `with --force to repoint it to ${HOOKS_DIR}. Hook body:\n\n${hookBody}`,
141
- );
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;
142
142
  }
143
- setConfig = true;
144
- }
145
143
 
146
- // Decide whether the hook file needs writing; overwriting a differing hook needs --force.
147
- const current = await readFile(hookAbs, 'utf8').catch(() => null);
148
- let writeHook = current === null;
149
- let repairMode = false;
150
- if (current !== null && current !== hookBody) {
151
- if (!cli.force) {
152
- const installed =
153
- current === HOOK_CHECK ? 'the check variant' : current === HOOK_FIX ? 'the --fix variant' : 'a custom hook';
154
- 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;
155
165
  }
156
- writeHook = true;
157
- }
158
- if (current === hookBody) {
159
- // Content is already right; still repair a missing executable bit, or git silently skips the hook.
160
- repairMode = ((await stat(hookAbs)).mode & 0o111) === 0;
161
- }
162
166
 
163
- const done: string[] = [];
164
- if (writeHook) {
165
- if (!cli.dryRun) {
166
- await mkdir(join(top, HOOKS_DIR), { recursive: true });
167
- await writeFile(hookAbs, hookBody);
168
- 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}`);
169
187
  }
170
- done.push(
171
- cli.dryRun ? `would install ${HOOK_PATH} (${variant} variant)` : `installed ${HOOK_PATH} (${variant} variant)`,
172
- );
173
- }
174
- if (repairMode) {
175
- if (!cli.dryRun) await chmod(hookAbs, 0o755);
176
- done.push(cli.dryRun ? `would make ${HOOK_PATH} executable` : `made ${HOOK_PATH} executable`);
177
- }
178
- if (setConfig) {
179
- if (!cli.dryRun) git(['config', 'core.hooksPath', HOOKS_DIR]);
180
- done.push(cli.dryRun ? `would set core.hooksPath = ${HOOKS_DIR}` : `set core.hooksPath = ${HOOKS_DIR}`);
181
- }
182
188
 
183
- if (done.length === 0) {
184
- console.log(`Nothing to do — ${HOOK_PATH} (${variant} variant) is already installed.`);
185
- return;
186
- }
187
- 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);
188
194
  }
package/src/islands.ts CHANGED
@@ -14,25 +14,25 @@
14
14
  * A contiguous region of PHP code within a mixed template source.
15
15
  */
16
16
  export interface Island {
17
- /**
18
- * Inclusive start offset of `<?`.
19
- */
20
- start: number;
21
- /**
22
- * Exclusive end offset (just past `?>`, or EOF).
23
- */
24
- end: number;
17
+ /**
18
+ * Inclusive start offset of `<?`.
19
+ */
20
+ start: number;
21
+ /**
22
+ * Exclusive end offset (just past `?>`, or EOF).
23
+ */
24
+ end: number;
25
25
  }
26
26
 
27
27
  /**
28
28
  * Options controlling PHP open-tag recognition.
29
29
  */
30
30
  export interface IslandOptions {
31
- /**
32
- * Treat bare `<?` as a PHP open tag (short_open_tag).
33
- * Default true, with a guard so `<?xml` is never treated as PHP.
34
- */
35
- shortOpenTags?: boolean;
31
+ /**
32
+ * Treat bare `<?` as a PHP open tag (short_open_tag).
33
+ * Default true, with a guard so `<?xml` is never treated as PHP.
34
+ */
35
+ shortOpenTags?: boolean;
36
36
  }
37
37
 
38
38
  const isIdentStart = (c: string) => /[A-Za-z_\u0080-\uffff]/.test(c);
@@ -53,35 +53,35 @@ const isIdent = (c: string) => /[A-Za-z0-9_\u0080-\uffff]/.test(c);
53
53
  * // [{ start: 3, end: 12 }]
54
54
  */
55
55
  export function findIslands(src: string, opts: IslandOptions = {}): Island[] {
56
- const shortTags = opts.shortOpenTags !== false;
57
- const islands: Island[] = [];
58
- const len = src.length;
59
- let i = 0;
60
-
61
- while (i < len) {
62
- const open = src.indexOf('<?', i);
63
- if (open === -1) break;
64
-
65
- // Classify the open tag.
66
- const after = src.slice(open + 2, open + 6).toLowerCase();
67
- let bodyStart: number;
68
- if (after.startsWith('php') && (open + 5 >= len || !isIdent(src[open + 5]))) {
69
- bodyStart = open + 5;
70
- } else if (src[open + 2] === '=') {
71
- bodyStart = open + 3;
72
- } else if (shortTags && !after.startsWith('xml')) {
73
- bodyStart = open + 2;
74
- } else {
75
- i = open + 2; // not a PHP tag (e.g. `<?xml`) — keep scanning
76
- continue;
56
+ const shortTags = opts.shortOpenTags !== false;
57
+ const islands: Island[] = [];
58
+ const len = src.length;
59
+ let i = 0;
60
+
61
+ while (i < len) {
62
+ const open = src.indexOf('<?', i);
63
+ if (open === -1) break;
64
+
65
+ // Classify the open tag.
66
+ const after = src.slice(open + 2, open + 6).toLowerCase();
67
+ let bodyStart: number;
68
+ if (after.startsWith('php') && (open + 5 >= len || !isIdent(src[open + 5]))) {
69
+ bodyStart = open + 5;
70
+ } else if (src[open + 2] === '=') {
71
+ bodyStart = open + 3;
72
+ } else if (shortTags && !after.startsWith('xml')) {
73
+ bodyStart = open + 2;
74
+ } else {
75
+ i = open + 2; // not a PHP tag (e.g. `<?xml`) — keep scanning
76
+ continue;
77
+ }
78
+
79
+ const end = scanPhpBody(src, bodyStart);
80
+ islands.push({ start: open, end });
81
+ i = end;
77
82
  }
78
83
 
79
- const end = scanPhpBody(src, bodyStart);
80
- islands.push({ start: open, end });
81
- i = end;
82
- }
83
-
84
- return islands;
84
+ return islands;
85
85
  }
86
86
 
87
87
  /**
@@ -89,68 +89,68 @@ export function findIslands(src: string, opts: IslandOptions = {}): Island[] {
89
89
  * or `src.length` if the file ends in PHP mode.
90
90
  */
91
91
  function scanPhpBody(src: string, i: number): number {
92
- const len = src.length;
93
-
94
- while (i < len) {
95
- const c = src[i];
96
-
97
- // Possible close tag.
98
- if (c === '?' && src[i + 1] === '>') return i + 2;
99
-
100
- // Single-quoted string.
101
- if (c === "'") {
102
- i = scanQuoted(src, i + 1, "'");
103
- continue;
104
- }
105
-
106
- // Double-quoted string.
107
- // Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer (documented).
108
- if (c === '"') {
109
- i = scanQuoted(src, i + 1, '"');
110
- continue;
111
- }
112
-
113
- // Backtick (shell exec) string — same escaping rules.
114
- if (c === '`') {
115
- i = scanQuoted(src, i + 1, '`');
116
- continue;
117
- }
118
-
119
- // Comments.
120
- if (c === '/' && src[i + 1] === '/') {
121
- i = scanLineComment(src, i + 2);
122
- if (src.startsWith('?>', i)) return i + 2;
123
- continue;
124
- }
125
- if (c === '#') {
126
- // PHP 8 attribute `#[...]` is not a comment.
127
- if (src[i + 1] === '[') {
128
- i += 2;
129
- continue;
130
- }
131
- i = scanLineComment(src, i + 1);
132
- if (src.startsWith('?>', i)) return i + 2;
133
- continue;
134
- }
135
- if (c === '/' && src[i + 1] === '*') {
136
- const close = src.indexOf('*/', i + 2);
137
- i = close === -1 ? len : close + 2;
138
- continue;
92
+ const len = src.length;
93
+
94
+ while (i < len) {
95
+ const c = src[i];
96
+
97
+ // Possible close tag.
98
+ if (c === '?' && src[i + 1] === '>') return i + 2;
99
+
100
+ // Single-quoted string.
101
+ if (c === "'") {
102
+ i = scanQuoted(src, i + 1, "'");
103
+ continue;
104
+ }
105
+
106
+ // Double-quoted string.
107
+ // Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer; see README.
108
+ if (c === '"') {
109
+ i = scanQuoted(src, i + 1, '"');
110
+ continue;
111
+ }
112
+
113
+ // Backtick (shell exec) string — same escaping rules.
114
+ if (c === '`') {
115
+ i = scanQuoted(src, i + 1, '`');
116
+ continue;
117
+ }
118
+
119
+ // Comments.
120
+ if (c === '/' && src[i + 1] === '/') {
121
+ i = scanLineComment(src, i + 2);
122
+ if (src.startsWith('?>', i)) return i + 2;
123
+ continue;
124
+ }
125
+ if (c === '#') {
126
+ // PHP 8 attribute `#[...]` is not a comment.
127
+ if (src[i + 1] === '[') {
128
+ i += 2;
129
+ continue;
130
+ }
131
+ i = scanLineComment(src, i + 1);
132
+ if (src.startsWith('?>', i)) return i + 2;
133
+ continue;
134
+ }
135
+ if (c === '/' && src[i + 1] === '*') {
136
+ const close = src.indexOf('*/', i + 2);
137
+ i = close === -1 ? len : close + 2;
138
+ continue;
139
+ }
140
+
141
+ // Heredoc / nowdoc.
142
+ if (c === '<' && src[i + 1] === '<' && src[i + 2] === '<') {
143
+ const here = scanHeredoc(src, i + 3);
144
+ if (here !== -1) {
145
+ i = here;
146
+ continue;
147
+ }
148
+ }
149
+
150
+ i++;
139
151
  }
140
152
 
141
- // Heredoc / nowdoc.
142
- if (c === '<' && src[i + 1] === '<' && src[i + 2] === '<') {
143
- const here = scanHeredoc(src, i + 3);
144
- if (here !== -1) {
145
- i = here;
146
- continue;
147
- }
148
- }
149
-
150
- i++;
151
- }
152
-
153
- return len; // file ends while still in PHP mode
153
+ return len; // file ends while still in PHP mode
154
154
  }
155
155
 
156
156
  /**
@@ -158,17 +158,17 @@ function scanPhpBody(src: string, i: number): number {
158
158
  * Returns offset just past the closing quote (or EOF).
159
159
  */
160
160
  function scanQuoted(src: string, i: number, quote: string): number {
161
- const len = src.length;
162
- while (i < len) {
163
- const c = src[i];
164
- if (c === '\\') {
165
- i += 2;
166
- continue;
161
+ const len = src.length;
162
+ while (i < len) {
163
+ const c = src[i];
164
+ if (c === '\\') {
165
+ i += 2;
166
+ continue;
167
+ }
168
+ if (c === quote) return i + 1;
169
+ i++;
167
170
  }
168
- if (c === quote) return i + 1;
169
- i++;
170
- }
171
- return len;
171
+ return len;
172
172
  }
173
173
 
174
174
  /**
@@ -177,13 +177,13 @@ function scanQuoted(src: string, i: number, quote: string): number {
177
177
  * (left for the caller, since `?>` closes both the comment and the island).
178
178
  */
179
179
  function scanLineComment(src: string, i: number): number {
180
- const len = src.length;
181
- while (i < len) {
182
- if (src[i] === '\n') return i + 1;
183
- if (src[i] === '?' && src[i + 1] === '>') return i;
184
- i++;
185
- }
186
- return len;
180
+ const len = src.length;
181
+ while (i < len) {
182
+ if (src[i] === '\n') return i + 1;
183
+ if (src[i] === '?' && src[i + 1] === '>') return i;
184
+ i++;
185
+ }
186
+ return len;
187
187
  }
188
188
 
189
189
  /**
@@ -191,46 +191,46 @@ function scanLineComment(src: string, i: number): number {
191
191
  * Returns the offset past the closing identifier line, or -1 if `<<<` isn't followed by a valid heredoc identifier.
192
192
  */
193
193
  function scanHeredoc(src: string, i: number): number {
194
- const len = src.length;
195
- // Optional whitespace (PHP allows spaces/tabs after `<<<`).
196
- while (i < len && (src[i] === ' ' || src[i] === '\t')) i++;
197
-
198
- // Optional quote around the identifier.
199
- let quote = '';
200
- if (src[i] === "'" || src[i] === '"') {
201
- quote = src[i];
202
- i++;
203
- }
194
+ const len = src.length;
195
+ // Optional whitespace (PHP allows spaces/tabs after `<<<`).
196
+ while (i < len && (src[i] === ' ' || src[i] === '\t')) i++;
197
+
198
+ // Optional quote around the identifier.
199
+ let quote = '';
200
+ if (src[i] === "'" || src[i] === '"') {
201
+ quote = src[i];
202
+ i++;
203
+ }
204
204
 
205
- if (i >= len || !isIdentStart(src[i])) return -1;
206
- const idStart = i;
207
- while (i < len && isIdent(src[i])) i++;
208
- const id = src.slice(idStart, i);
205
+ if (i >= len || !isIdentStart(src[i])) return -1;
206
+ const idStart = i;
207
+ while (i < len && isIdent(src[i])) i++;
208
+ const id = src.slice(idStart, i);
209
209
 
210
- if (quote) {
211
- if (src[i] !== quote) return -1;
210
+ if (quote) {
211
+ if (src[i] !== quote) return -1;
212
+ i++;
213
+ }
214
+
215
+ // After the identifier, the line must end immediately — PHP disallows trailing whitespace,
216
+ // but we tolerate `\r` so `\r\n` endings still work.
217
+ while (i < len && src[i] === '\r') i++;
218
+ if (src[i] !== '\n') return -1;
212
219
  i++;
213
- }
214
-
215
- // After the identifier, the line must end immediately — PHP disallows trailing whitespace,
216
- // but we tolerate `\r` so `\r\n` endings still work.
217
- while (i < len && src[i] === '\r') i++;
218
- if (src[i] !== '\n') return -1;
219
- i++;
220
-
221
- // Find a line that starts with optional indentation,
222
- // then the identifier followed by a non-identifier character
223
- // (PHP 7.3 flexible syntax).
224
- while (i < len) {
225
- let j = i;
226
- while (j < len && (src[j] === ' ' || src[j] === '\t')) j++;
227
- if (src.startsWith(id, j)) {
228
- const k = j + id.length;
229
- if (k >= len || !isIdent(src[k])) return k;
220
+
221
+ // Find a line that starts with optional indentation,
222
+ // then the identifier followed by a non-identifier character
223
+ // (PHP 7.3 flexible syntax).
224
+ while (i < len) {
225
+ let j = i;
226
+ while (j < len && (src[j] === ' ' || src[j] === '\t')) j++;
227
+ if (src.startsWith(id, j)) {
228
+ const k = j + id.length;
229
+ if (k >= len || !isIdent(src[k])) return k;
230
+ }
231
+ const nl = src.indexOf('\n', i);
232
+ if (nl === -1) return len;
233
+ i = nl + 1;
230
234
  }
231
- const nl = src.indexOf('\n', i);
232
- if (nl === -1) return len;
233
- i = nl + 1;
234
- }
235
- return len;
235
+ return len;
236
236
  }