@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 +150 -36
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +35 -22
- package/dist/html.d.ts +1 -1
- package/dist/html.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +13 -9
- package/dist/islands.js +1 -1
- package/dist/php-strings.d.ts +47 -0
- package/dist/php-strings.js +230 -0
- package/dist/sorter.d.ts +3 -3
- package/dist/sorter.js +3 -4
- package/dist/transform.d.ts +5 -1
- package/dist/transform.js +22 -6
- package/package.json +55 -55
- package/src/cli.ts +122 -106
- package/src/html.ts +119 -119
- package/src/index.ts +1 -0
- package/src/init.ts +102 -96
- package/src/islands.ts +155 -155
- package/src/php-strings.ts +260 -0
- package/src/sorter.ts +17 -18
- package/src/transform.ts +98 -78
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
|
-
#
|
|
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
|
-
#
|
|
40
|
-
#
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const cli = parseArgs(argv);
|
|
110
|
+
const hookBody = cli.fix ? HOOK_FIX : HOOK_CHECK;
|
|
111
|
+
const variant = cli.fix ? 'fix' : 'check';
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
232
|
-
if (nl === -1) return len;
|
|
233
|
-
i = nl + 1;
|
|
234
|
-
}
|
|
235
|
-
return len;
|
|
235
|
+
return len;
|
|
236
236
|
}
|